@almyty/chat 0.1.3 → 0.2.0
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 +23 -16
- package/dist/app.d.ts +17 -0
- package/dist/app.js +319 -0
- package/dist/commands.d.ts +5 -0
- package/dist/commands.js +36 -0
- package/dist/components.d.ts +37 -0
- package/dist/components.js +159 -0
- package/dist/index.d.ts +1 -19
- package/dist/index.js +65 -288
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -1,33 +1,40 @@
|
|
|
1
1
|
# @almyty/chat
|
|
2
2
|
|
|
3
|
-
Interactive chat REPL for almyty agents.
|
|
3
|
+
Interactive chat REPL for almyty agents. Built with [ink](https://github.com/vadimdemedes/ink) (React for CLI).
|
|
4
4
|
|
|
5
5
|
## Quick start
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
$ npx @almyty/auth login
|
|
9
|
-
$ npx @almyty/chat
|
|
10
|
-
$ npx @almyty/chat my-research-bot
|
|
9
|
+
$ npx @almyty/chat acme/support-bot
|
|
11
10
|
```
|
|
12
11
|
|
|
13
12
|
## Usage
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
```
|
|
15
|
+
Usage:
|
|
16
|
+
npx @almyty/chat <org>/<agent-slug>
|
|
17
|
+
npx @almyty/chat <org>/<agent-slug> --resume <conversation-id>
|
|
18
|
+
npx @almyty/chat # interactive agent picker
|
|
19
|
+
|
|
20
|
+
Commands:
|
|
21
|
+
/agents browse and switch agents
|
|
22
|
+
/tools show available tools
|
|
23
|
+
/help show commands
|
|
24
|
+
/clear clear conversation
|
|
25
|
+
/quit exit (shows resume command)
|
|
20
26
|
```
|
|
21
27
|
|
|
22
|
-
##
|
|
28
|
+
## Features
|
|
23
29
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
- **Gateway routing** -- agents addressed as `<org>/<agent-slug>`
|
|
31
|
+
- **Resume conversations** -- `--resume <conversation-id>` picks up where you left off; `/quit` prints the resume command
|
|
32
|
+
- **Arrow-key agent picker** -- run without arguments to browse and select
|
|
33
|
+
- **Slash commands** -- tab autocomplete with fuzzy prefix matching
|
|
34
|
+
- **Command palette** -- arrow-key navigation through matching commands
|
|
35
|
+
- **Input history** -- up/down arrows cycle through previous messages (derived from conversation, including resumed history)
|
|
36
|
+
- **SSE streaming** -- real-time tool calls and agent responses via server-sent events
|
|
37
|
+
- **Markdown rendering** -- bold, inline code, code blocks, lists, headers
|
|
31
38
|
|
|
32
39
|
## Authentication
|
|
33
40
|
|
package/dist/app.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AlmytyClient, GatewayClient, AgentInfo } from '@almyty/client';
|
|
2
|
+
import type { Message } from './components.js';
|
|
3
|
+
export interface AppState {
|
|
4
|
+
agent: AgentInfo;
|
|
5
|
+
messages: Message[];
|
|
6
|
+
loading: boolean;
|
|
7
|
+
loadingLabel: string;
|
|
8
|
+
conversationId: string | null;
|
|
9
|
+
pendingRunId: string | null;
|
|
10
|
+
}
|
|
11
|
+
export declare let exitMessage: string;
|
|
12
|
+
export declare function ChatApp({ client, initialAgent, gw, resumeConversationId }: {
|
|
13
|
+
client: AlmytyClient;
|
|
14
|
+
initialAgent: AgentInfo;
|
|
15
|
+
gw: GatewayClient;
|
|
16
|
+
resumeConversationId?: string;
|
|
17
|
+
}): import("react/jsx-runtime").JSX.Element;
|
package/dist/app.js
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
3
|
+
import { Box, Text, useApp, useInput } from 'ink';
|
|
4
|
+
import TextInput from 'ink-text-input';
|
|
5
|
+
import { Header, MessageView, LoadingIndicator, AgentSelector } from './components.js';
|
|
6
|
+
import { SLASH_COMMANDS, COMMAND_DESCS, resolveSlash, getSuggestion, ALIASES } from './commands.js';
|
|
7
|
+
// Mutable module-level variable; written by ChatApp, read by main() after exit
|
|
8
|
+
export let exitMessage = '';
|
|
9
|
+
// ── Chat app ────────────────────────────────────────────────────
|
|
10
|
+
export function ChatApp({ client, initialAgent, gw, resumeConversationId }) {
|
|
11
|
+
const { exit } = useApp();
|
|
12
|
+
const [state, setState] = useState({
|
|
13
|
+
agent: initialAgent,
|
|
14
|
+
messages: [],
|
|
15
|
+
loading: false,
|
|
16
|
+
loadingLabel: 'Thinking',
|
|
17
|
+
conversationId: resumeConversationId ?? null,
|
|
18
|
+
pendingRunId: null,
|
|
19
|
+
});
|
|
20
|
+
// Load conversation history on resume
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!resumeConversationId)
|
|
23
|
+
return;
|
|
24
|
+
(async () => {
|
|
25
|
+
setState(s => ({ ...s, loading: true, loadingLabel: 'Loading history' }));
|
|
26
|
+
try {
|
|
27
|
+
const history = await gw.getConversationMessages(resumeConversationId);
|
|
28
|
+
const msgs = history
|
|
29
|
+
.filter(m => m.role === 'user' || m.role === 'assistant')
|
|
30
|
+
.map(m => ({
|
|
31
|
+
role: m.role === 'user' ? 'user' : 'agent',
|
|
32
|
+
text: m.content,
|
|
33
|
+
}));
|
|
34
|
+
setState(s => ({ ...s, messages: msgs, loading: false }));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
setState(s => ({ ...s, loading: false }));
|
|
38
|
+
}
|
|
39
|
+
})();
|
|
40
|
+
}, [resumeConversationId]);
|
|
41
|
+
const [input, setInput] = useState('');
|
|
42
|
+
const [paletteCursor, setPaletteCursor] = useState(0);
|
|
43
|
+
// Input history derived from conversation — includes resumed messages
|
|
44
|
+
const inputHistory = state.messages.filter(m => m.role === 'user').map(m => m.text);
|
|
45
|
+
const [historyIdx, setHistoryIdx] = useState(-1);
|
|
46
|
+
const [showPicker, setShowPicker] = useState(false);
|
|
47
|
+
const [pickerAgents, setPickerAgents] = useState([]);
|
|
48
|
+
// Command palette matches
|
|
49
|
+
const slashMatches = input.startsWith('/') && !input.includes(' ')
|
|
50
|
+
? SLASH_COMMANDS.filter(c => c.startsWith(input.slice(1).toLowerCase()))
|
|
51
|
+
: [];
|
|
52
|
+
const paletteOpen = input.startsWith('/') && slashMatches.length > 0;
|
|
53
|
+
useInput((ch, key) => {
|
|
54
|
+
if (state.loading)
|
|
55
|
+
return;
|
|
56
|
+
// Command palette navigation
|
|
57
|
+
if (paletteOpen) {
|
|
58
|
+
if (key.upArrow) {
|
|
59
|
+
setPaletteCursor(c => (c - 1 + slashMatches.length) % slashMatches.length);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (key.downArrow) {
|
|
63
|
+
setPaletteCursor(c => (c + 1) % slashMatches.length);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (key.tab) {
|
|
67
|
+
setInput(`/${slashMatches[paletteCursor]}`);
|
|
68
|
+
setPaletteCursor(0);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// Input history — up/down arrows always, like Claude Code
|
|
74
|
+
if (key.upArrow && inputHistory.length > 0) {
|
|
75
|
+
const newIdx = Math.min(historyIdx + 1, inputHistory.length - 1);
|
|
76
|
+
setHistoryIdx(newIdx);
|
|
77
|
+
setInput(inputHistory[inputHistory.length - 1 - newIdx]);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (key.downArrow) {
|
|
81
|
+
if (historyIdx > 0) {
|
|
82
|
+
const newIdx = historyIdx - 1;
|
|
83
|
+
setHistoryIdx(newIdx);
|
|
84
|
+
setInput(inputHistory[inputHistory.length - 1 - newIdx]);
|
|
85
|
+
}
|
|
86
|
+
else if (historyIdx === 0) {
|
|
87
|
+
setHistoryIdx(-1);
|
|
88
|
+
setInput('');
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
// Reset palette cursor when input changes
|
|
94
|
+
const handleInputChange = useCallback((val) => {
|
|
95
|
+
setInput(val);
|
|
96
|
+
setPaletteCursor(0);
|
|
97
|
+
}, []);
|
|
98
|
+
const addMessage = useCallback((msg) => {
|
|
99
|
+
setState(s => ({ ...s, messages: [...s.messages, msg] }));
|
|
100
|
+
}, []);
|
|
101
|
+
const handleSubmit = useCallback(async (value) => {
|
|
102
|
+
const trimmed = value.trim();
|
|
103
|
+
if (!trimmed)
|
|
104
|
+
return;
|
|
105
|
+
setInput('');
|
|
106
|
+
setHistoryIdx(-1);
|
|
107
|
+
// Slash commands
|
|
108
|
+
if (trimmed.startsWith('/')) {
|
|
109
|
+
const [raw, ...args] = trimmed.slice(1).split(/\s+/);
|
|
110
|
+
const cmd = resolveSlash(raw);
|
|
111
|
+
if (!cmd) {
|
|
112
|
+
addMessage({ role: 'error', text: `Unknown command: /${raw}` });
|
|
113
|
+
addMessage({ role: 'info', text: `Commands: /help /agents /clear /quit` });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (cmd !== raw.toLowerCase() && !Object.entries(ALIASES).some(([k, v]) => k === raw.toLowerCase() && v === cmd)) {
|
|
117
|
+
addMessage({ role: 'info', text: `→ /${cmd}` });
|
|
118
|
+
}
|
|
119
|
+
switch (cmd) {
|
|
120
|
+
case 'quit':
|
|
121
|
+
const agentRef = `${gw.orgSlug}/${gw.agentSlug}`;
|
|
122
|
+
exitMessage = state.conversationId
|
|
123
|
+
? `\nTo resume: npx @almyty/chat ${agentRef} --resume ${state.conversationId}\n`
|
|
124
|
+
: '';
|
|
125
|
+
exit();
|
|
126
|
+
return;
|
|
127
|
+
case 'clear':
|
|
128
|
+
setState(s => ({ ...s, messages: [] }));
|
|
129
|
+
return;
|
|
130
|
+
case 'help':
|
|
131
|
+
addMessage({ role: 'info', text: '/agents browse and switch agents' });
|
|
132
|
+
addMessage({ role: 'info', text: '/tools show available tools' });
|
|
133
|
+
addMessage({ role: 'info', text: '/clear clear conversation' });
|
|
134
|
+
addMessage({ role: 'info', text: '/help show this help' });
|
|
135
|
+
addMessage({ role: 'info', text: '/quit exit' });
|
|
136
|
+
addMessage({ role: 'info', text: 'Tab to autocomplete commands.' });
|
|
137
|
+
return;
|
|
138
|
+
case 'tools': {
|
|
139
|
+
const tools = state.agent.tools;
|
|
140
|
+
if (!tools?.length) {
|
|
141
|
+
addMessage({ role: 'info', text: 'No tools configured for this agent.' });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
addMessage({ role: 'info', text: `${tools.length} tool${tools.length > 1 ? 's' : ''} available:` });
|
|
145
|
+
for (const tool of tools) {
|
|
146
|
+
const desc = tool.description ? ` — ${tool.description}` : '';
|
|
147
|
+
addMessage({ role: 'tool', text: `${tool.name}${desc}` });
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
case 'agents': {
|
|
152
|
+
const target = args.join(' ').trim();
|
|
153
|
+
if (target) {
|
|
154
|
+
setState(s => ({ ...s, loading: true, loadingLabel: 'Switching' }));
|
|
155
|
+
const found = await client.findAgentByNameOrId(target);
|
|
156
|
+
setState(s => ({ ...s, loading: false }));
|
|
157
|
+
if (!found) {
|
|
158
|
+
addMessage({ role: 'error', text: `Agent "${target}" not found` });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
setState(s => ({
|
|
162
|
+
...s,
|
|
163
|
+
agent: found,
|
|
164
|
+
messages: [],
|
|
165
|
+
conversationId: null,
|
|
166
|
+
pendingRunId: null,
|
|
167
|
+
}));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
setState(s => ({ ...s, loading: true, loadingLabel: 'Loading' }));
|
|
171
|
+
const agents = await client.listAgents();
|
|
172
|
+
setState(s => ({ ...s, loading: false }));
|
|
173
|
+
setPickerAgents(agents);
|
|
174
|
+
setShowPicker(true);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// Regular message
|
|
181
|
+
addMessage({ role: 'user', text: trimmed });
|
|
182
|
+
setState(s => ({ ...s, loading: true, loadingLabel: 'Thinking' }));
|
|
183
|
+
try {
|
|
184
|
+
if (state.agent.mode === 'autonomous') {
|
|
185
|
+
let runId;
|
|
186
|
+
if (state.pendingRunId) {
|
|
187
|
+
await gw.sendRunInput(state.pendingRunId, trimmed);
|
|
188
|
+
runId = state.pendingRunId;
|
|
189
|
+
setState(s => ({ ...s, pendingRunId: null }));
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
const run = await gw.startRun(trimmed, {
|
|
193
|
+
conversationId: state.conversationId ?? undefined,
|
|
194
|
+
});
|
|
195
|
+
runId = run.id;
|
|
196
|
+
if (run.conversationId) {
|
|
197
|
+
setState(s => ({ ...s, conversationId: run.conversationId }));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Stream events — show tool calls, sub-agents, chunks in real time
|
|
201
|
+
let streamedContent = '';
|
|
202
|
+
const result = await gw.streamRun(runId, (event) => {
|
|
203
|
+
switch (event.type) {
|
|
204
|
+
case 'llm.started':
|
|
205
|
+
setState(s => ({ ...s, loadingLabel: 'Thinking' }));
|
|
206
|
+
break;
|
|
207
|
+
case 'llm.chunk': {
|
|
208
|
+
const chunk = event.data.content;
|
|
209
|
+
if (chunk)
|
|
210
|
+
streamedContent += chunk;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
case 'llm.response':
|
|
214
|
+
setState(s => ({ ...s, loadingLabel: 'Processing' }));
|
|
215
|
+
break;
|
|
216
|
+
case 'tool.started': {
|
|
217
|
+
const toolName = event.data.tool;
|
|
218
|
+
if (toolName) {
|
|
219
|
+
setState(s => ({ ...s, loading: false }));
|
|
220
|
+
addMessage({ role: 'tool', text: toolName });
|
|
221
|
+
setState(s => ({ ...s, loading: true, loadingLabel: `Running ${toolName}` }));
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
case 'tool.result': {
|
|
226
|
+
const tool = event.data.tool;
|
|
227
|
+
const success = event.data.success;
|
|
228
|
+
if (tool) {
|
|
229
|
+
addMessage({ role: 'info', text: `${tool} ${success ? 'completed' : 'failed'}` });
|
|
230
|
+
}
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
case 'step.completed':
|
|
234
|
+
break;
|
|
235
|
+
case 'run.completed': {
|
|
236
|
+
const output = event.data.output;
|
|
237
|
+
if (output) {
|
|
238
|
+
const text = typeof output === 'string' ? output : JSON.stringify(output, null, 2);
|
|
239
|
+
streamedContent = text;
|
|
240
|
+
}
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
case 'run.failed': {
|
|
244
|
+
const error = event.data.error;
|
|
245
|
+
addMessage({ role: 'error', text: error || 'Run failed' });
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
setState(s => ({ ...s, loading: false }));
|
|
251
|
+
// If still running after stream, poll to completion
|
|
252
|
+
let finalResult = result;
|
|
253
|
+
if (result.status === 'running') {
|
|
254
|
+
finalResult = await gw.pollRun(runId);
|
|
255
|
+
}
|
|
256
|
+
// Show final output
|
|
257
|
+
if (finalResult.status === 'completed') {
|
|
258
|
+
const text = streamedContent
|
|
259
|
+
|| (finalResult.output != null ? (typeof finalResult.output === 'string' ? finalResult.output : JSON.stringify(finalResult.output, null, 2)) : '');
|
|
260
|
+
if (text)
|
|
261
|
+
addMessage({ role: 'agent', text });
|
|
262
|
+
}
|
|
263
|
+
else if (finalResult.status === 'waiting_input') {
|
|
264
|
+
setState(s => ({ ...s, pendingRunId: runId }));
|
|
265
|
+
addMessage({ role: 'info', text: 'Waiting for your input' });
|
|
266
|
+
}
|
|
267
|
+
else if (finalResult.status === 'failed' && !streamedContent) {
|
|
268
|
+
addMessage({ role: 'error', text: finalResult.error || 'Run failed' });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
// Workflow: synchronous invoke
|
|
273
|
+
const result = await gw.invoke({ message: trimmed });
|
|
274
|
+
setState(s => ({ ...s, loading: false }));
|
|
275
|
+
const output = result?.output ?? result?.data?.output ?? result;
|
|
276
|
+
if (output != null) {
|
|
277
|
+
const text = typeof output === 'string' ? output : JSON.stringify(output, null, 2);
|
|
278
|
+
addMessage({ role: 'agent', text });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
setState(s => ({ ...s, loading: false }));
|
|
284
|
+
addMessage({ role: 'error', text: err.message });
|
|
285
|
+
}
|
|
286
|
+
}, [state.agent, state.conversationId, state.pendingRunId, client, addMessage, exit]);
|
|
287
|
+
const handlePickerSelect = useCallback((agent) => {
|
|
288
|
+
setShowPicker(false);
|
|
289
|
+
if (agent.id === state.agent.id)
|
|
290
|
+
return;
|
|
291
|
+
setState(s => ({
|
|
292
|
+
...s,
|
|
293
|
+
agent,
|
|
294
|
+
messages: [],
|
|
295
|
+
conversationId: null,
|
|
296
|
+
pendingRunId: null,
|
|
297
|
+
}));
|
|
298
|
+
}, [state.agent.id]);
|
|
299
|
+
const suggestion = getSuggestion(input);
|
|
300
|
+
if (showPicker) {
|
|
301
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { agent: state.agent, conversationId: state.conversationId }), _jsx(AgentSelector, { agents: pickerAgents, onSelect: handlePickerSelect })] }));
|
|
302
|
+
}
|
|
303
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { agent: state.agent, conversationId: state.conversationId }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, paddingRight: 2, overflow: "hidden", children: [state.messages.map((msg, i) => (_jsx(MessageView, { msg: msg }, i))), state.loading && _jsx(LoadingIndicator, { label: state.loadingLabel })] }), paletteOpen && (_jsx(Box, { flexDirection: "column", paddingLeft: 2, children: slashMatches.map((cmd, i) => {
|
|
304
|
+
const active = i === paletteCursor;
|
|
305
|
+
const padded = `/${cmd}`.padEnd(10);
|
|
306
|
+
const desc = COMMAND_DESCS[cmd] ?? '';
|
|
307
|
+
const line = `${active ? '❯' : ' '} ${padded} ${desc}`;
|
|
308
|
+
return _jsx(Text, { color: active ? '#8b5cf6' : undefined, bold: active, wrap: "truncate", children: line }, cmd);
|
|
309
|
+
}) })), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: '─'.repeat(Math.min(process.stdout.columns || 80, 120)) }) }), _jsxs(Box, { paddingX: 1, paddingY: 1, children: [_jsx(Text, { color: "#8b5cf6", children: "\u276F " }), _jsx(Box, { flexGrow: 1, children: _jsx(TextInput, { value: input, onChange: handleInputChange, onSubmit: (val) => {
|
|
310
|
+
if (paletteOpen && slashMatches.length > 0) {
|
|
311
|
+
const selected = `/${slashMatches[paletteCursor]}`;
|
|
312
|
+
setInput('');
|
|
313
|
+
setPaletteCursor(0);
|
|
314
|
+
handleSubmit(selected);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
handleSubmit(val);
|
|
318
|
+
}, placeholder: "Type a message or / for commands" }) })] }), _jsx(Box, { paddingX: 1, children: _jsxs(Text, { dimColor: true, children: [state.agent.name, state.agent.tools?.length ? ` · ${state.agent.tools.length} tools` : '', state.conversationId ? ` · ${state.conversationId.slice(0, 8)}` : ''] }) })] }));
|
|
319
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const SLASH_COMMANDS: readonly ["agents", "tools", "help", "clear", "quit"];
|
|
2
|
+
export declare const ALIASES: Record<string, string>;
|
|
3
|
+
export declare const COMMAND_DESCS: Record<string, string>;
|
|
4
|
+
export declare function resolveSlash(input: string): string | null;
|
|
5
|
+
export declare function getSuggestion(partial: string): string;
|
package/dist/commands.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// ── Slash command resolution ────────────────────────────────────
|
|
2
|
+
export const SLASH_COMMANDS = ['agents', 'tools', 'help', 'clear', 'quit'];
|
|
3
|
+
export const ALIASES = {
|
|
4
|
+
agent: 'agents', ag: 'agents', switch: 'agents', sw: 'agents',
|
|
5
|
+
tool: 'tools', t: 'tools',
|
|
6
|
+
h: 'help', '?': 'help',
|
|
7
|
+
cls: 'clear', c: 'clear',
|
|
8
|
+
exit: 'quit', q: 'quit',
|
|
9
|
+
};
|
|
10
|
+
export const COMMAND_DESCS = {
|
|
11
|
+
agents: 'browse and switch agents',
|
|
12
|
+
tools: 'show available tools',
|
|
13
|
+
help: 'show commands',
|
|
14
|
+
clear: 'clear conversation',
|
|
15
|
+
quit: 'exit',
|
|
16
|
+
};
|
|
17
|
+
export function resolveSlash(input) {
|
|
18
|
+
const name = input.toLowerCase();
|
|
19
|
+
if (SLASH_COMMANDS.includes(name))
|
|
20
|
+
return name;
|
|
21
|
+
if (ALIASES[name])
|
|
22
|
+
return ALIASES[name];
|
|
23
|
+
const prefixed = SLASH_COMMANDS.filter(c => c.startsWith(name));
|
|
24
|
+
if (prefixed.length === 1)
|
|
25
|
+
return prefixed[0];
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
export function getSuggestion(partial) {
|
|
29
|
+
if (!partial.startsWith('/') || partial.includes(' '))
|
|
30
|
+
return '';
|
|
31
|
+
const p = partial.slice(1).toLowerCase();
|
|
32
|
+
if (!p)
|
|
33
|
+
return '';
|
|
34
|
+
const match = SLASH_COMMANDS.find(c => c.startsWith(p) && c !== p);
|
|
35
|
+
return match ? `/${match}` : '';
|
|
36
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { AgentInfo } from '@almyty/client';
|
|
3
|
+
export interface Message {
|
|
4
|
+
role: 'user' | 'agent' | 'error' | 'info' | 'tool';
|
|
5
|
+
text: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function MarkdownText({ children, prefix, prefixColor }: {
|
|
8
|
+
children: string;
|
|
9
|
+
prefix?: string;
|
|
10
|
+
prefixColor?: string;
|
|
11
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
export declare function InlineFormat({ text }: {
|
|
13
|
+
text: string;
|
|
14
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export declare function boldify(text: string): React.ReactElement;
|
|
16
|
+
export declare function estimateLines(msg: Message, cols: number): number;
|
|
17
|
+
export declare function MessageWindow({ messages, loading, loadingLabel, maxRows, scrollOffset }: {
|
|
18
|
+
messages: Message[];
|
|
19
|
+
loading: boolean;
|
|
20
|
+
loadingLabel: string;
|
|
21
|
+
maxRows: number;
|
|
22
|
+
scrollOffset?: number;
|
|
23
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
export declare function Header({ agent, conversationId }: {
|
|
25
|
+
agent: AgentInfo;
|
|
26
|
+
conversationId?: string | null;
|
|
27
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
28
|
+
export declare function MessageView({ msg }: {
|
|
29
|
+
msg: Message;
|
|
30
|
+
}): import("react/jsx-runtime").JSX.Element | null;
|
|
31
|
+
export declare function LoadingIndicator({ label }: {
|
|
32
|
+
label: string;
|
|
33
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
34
|
+
export declare function AgentSelector({ agents, onSelect }: {
|
|
35
|
+
agents: AgentInfo[];
|
|
36
|
+
onSelect: (a: AgentInfo) => void;
|
|
37
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import Spinner from 'ink-spinner';
|
|
5
|
+
// ── Simple markdown rendering ───────────────────────────────────
|
|
6
|
+
export function MarkdownText({ children, prefix, prefixColor }) {
|
|
7
|
+
const lines = children.split('\n');
|
|
8
|
+
const elements = [];
|
|
9
|
+
let inCode = false;
|
|
10
|
+
let codeLines = [];
|
|
11
|
+
for (let i = 0; i < lines.length; i++) {
|
|
12
|
+
const line = lines[i];
|
|
13
|
+
if (line.trimStart().startsWith('```')) {
|
|
14
|
+
if (!inCode) {
|
|
15
|
+
inCode = true;
|
|
16
|
+
codeLines = [];
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
inCode = false;
|
|
20
|
+
elements.push(_jsx(Box, { flexDirection: "column", paddingLeft: 1, paddingRight: 1, marginTop: 1, marginBottom: 1, children: codeLines.map((cl, j) => (_jsx(Text, { color: "white", backgroundColor: "#333", children: ` ${cl} ` }, j))) }, `code-${i}`));
|
|
21
|
+
}
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (inCode) {
|
|
25
|
+
codeLines.push(line);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
// Headers
|
|
29
|
+
const hm = line.match(/^(#{1,3})\s+(.*)/);
|
|
30
|
+
if (hm) {
|
|
31
|
+
elements.push(_jsx(Text, { bold: true, color: "#8b5cf6", children: hm[2] }, i));
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
// Unordered list
|
|
35
|
+
const ulm = line.match(/^(\s*)[*-]\s+(.*)/);
|
|
36
|
+
if (ulm) {
|
|
37
|
+
elements.push(_jsxs(Text, { children: [ulm[1], _jsx(Text, { color: "#8b5cf6", children: "\u2022" }), " ", _jsx(InlineFormat, { text: ulm[2] })] }, i));
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
// Ordered list
|
|
41
|
+
const olm = line.match(/^(\s*)(\d+)\.\s+(.*)/);
|
|
42
|
+
if (olm) {
|
|
43
|
+
elements.push(_jsxs(Text, { children: [olm[1], _jsxs(Text, { dimColor: true, children: [olm[2], "."] }), " ", _jsx(InlineFormat, { text: olm[3] })] }, i));
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
// Blank
|
|
47
|
+
if (!line.trim()) {
|
|
48
|
+
elements.push(_jsx(Text, { children: " " }, i));
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
elements.push(_jsx(Text, { children: _jsx(InlineFormat, { text: line }) }, i));
|
|
52
|
+
}
|
|
53
|
+
return _jsx(Box, { flexDirection: "column", children: elements });
|
|
54
|
+
}
|
|
55
|
+
export function InlineFormat({ text }) {
|
|
56
|
+
// Simple bold/code rendering via split
|
|
57
|
+
const parts = [];
|
|
58
|
+
let remaining = text;
|
|
59
|
+
let idx = 0;
|
|
60
|
+
while (remaining) {
|
|
61
|
+
// Inline code
|
|
62
|
+
const codeMatch = remaining.match(/^(.*?)`([^`]+)`(.*)/);
|
|
63
|
+
if (codeMatch) {
|
|
64
|
+
if (codeMatch[1])
|
|
65
|
+
parts.push(_jsx(Text, { children: boldify(codeMatch[1]) }, idx++));
|
|
66
|
+
parts.push(_jsx(Text, { backgroundColor: "#333", color: "white", children: ` ${codeMatch[2]} ` }, idx++));
|
|
67
|
+
remaining = codeMatch[3];
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
parts.push(_jsx(Text, { children: boldify(remaining) }, idx++));
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
return _jsx(_Fragment, { children: parts });
|
|
74
|
+
}
|
|
75
|
+
export function boldify(text) {
|
|
76
|
+
const parts = [];
|
|
77
|
+
let remaining = text;
|
|
78
|
+
let idx = 0;
|
|
79
|
+
while (remaining) {
|
|
80
|
+
const bm = remaining.match(/^(.*?)\*\*(.+?)\*\*(.*)/);
|
|
81
|
+
if (bm) {
|
|
82
|
+
if (bm[1])
|
|
83
|
+
parts.push(_jsx(Text, { children: bm[1] }, idx++));
|
|
84
|
+
parts.push(_jsx(Text, { bold: true, children: bm[2] }, idx++));
|
|
85
|
+
remaining = bm[3];
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
parts.push(_jsx(Text, { children: remaining }, idx++));
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
return _jsx(_Fragment, { children: parts });
|
|
92
|
+
}
|
|
93
|
+
// ── Message window (manual scroll) ──────────────────────────────
|
|
94
|
+
export function estimateLines(msg, cols) {
|
|
95
|
+
const textWidth = Math.max(cols - 10, 20);
|
|
96
|
+
const lines = msg.text.split('\n');
|
|
97
|
+
let total = 1; // margin
|
|
98
|
+
for (const line of lines) {
|
|
99
|
+
total += Math.max(1, Math.ceil(Math.max(line.length, 1) / textWidth));
|
|
100
|
+
}
|
|
101
|
+
return total;
|
|
102
|
+
}
|
|
103
|
+
export function MessageWindow({ messages, loading, loadingLabel, maxRows, scrollOffset = 0 }) {
|
|
104
|
+
const cols = process.stdout.columns || 80;
|
|
105
|
+
const available = Math.max(maxRows, 5);
|
|
106
|
+
// Calculate the end of the visible window (shifted by scrollOffset)
|
|
107
|
+
const endIdx = Math.max(0, messages.length - scrollOffset);
|
|
108
|
+
// Walk backwards from endIdx to find how many messages fit
|
|
109
|
+
let usedRows = loading && scrollOffset === 0 ? 2 : 0;
|
|
110
|
+
let startIdx = endIdx;
|
|
111
|
+
for (let i = endIdx - 1; i >= 0; i--) {
|
|
112
|
+
const est = estimateLines(messages[i], cols);
|
|
113
|
+
if (usedRows + est > available && i < endIdx - 1)
|
|
114
|
+
break;
|
|
115
|
+
usedRows += est;
|
|
116
|
+
startIdx = i;
|
|
117
|
+
}
|
|
118
|
+
const visible = messages.slice(startIdx, endIdx);
|
|
119
|
+
const hasEarlier = startIdx > 0;
|
|
120
|
+
const hasLater = endIdx < messages.length;
|
|
121
|
+
return (_jsxs(Box, { flexDirection: "column", children: [hasEarlier && (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["\u2191 ", startIdx, " earlier \u00B7 scroll to see more"] }) })), visible.map((msg, i) => (_jsx(MessageView, { msg: msg }, startIdx + i))), loading && scrollOffset === 0 && _jsx(LoadingIndicator, { label: loadingLabel }), hasLater && (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["\u2193 ", messages.length - endIdx, " newer \u00B7 scroll to see more"] }) }))] }));
|
|
122
|
+
}
|
|
123
|
+
// ── Components ──────────────────────────────────────────────────
|
|
124
|
+
export function Header({ agent, conversationId }) {
|
|
125
|
+
const convSuffix = conversationId ? ` ${conversationId.slice(0, 8)}` : '';
|
|
126
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: "#555", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { wrap: "truncate", children: `⚡ almyty · ${agent.name} ${agent.mode || ''}${convSuffix}` }), agent.description && _jsx(Text, { dimColor: true, wrap: "truncate", children: agent.description })] }));
|
|
127
|
+
}
|
|
128
|
+
export function MessageView({ msg }) {
|
|
129
|
+
switch (msg.role) {
|
|
130
|
+
case 'user':
|
|
131
|
+
return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["\u276F ", msg.text] }) }));
|
|
132
|
+
case 'agent':
|
|
133
|
+
return (_jsxs(Box, { marginTop: 1, paddingLeft: 2, flexDirection: "column", children: [_jsx(Text, { color: "#8b5cf6", children: "\u2502" }), _jsx(Box, { paddingLeft: 2, children: _jsx(MarkdownText, { children: msg.text }) })] }));
|
|
134
|
+
case 'tool':
|
|
135
|
+
return (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["\u2502 \u25B8 ", msg.text] }) }));
|
|
136
|
+
case 'error':
|
|
137
|
+
return (_jsx(Box, { marginTop: 1, paddingLeft: 2, children: _jsxs(Text, { color: "red", children: ["\u2502 ", msg.text] }) }));
|
|
138
|
+
case 'info':
|
|
139
|
+
return (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["\u2502 ", msg.text] }) }));
|
|
140
|
+
default:
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export function LoadingIndicator({ label }) {
|
|
145
|
+
return (_jsxs(Box, { marginTop: 1, paddingLeft: 2, children: [_jsx(Text, { color: "#8b5cf6", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { dimColor: true, children: [" ", label] })] }));
|
|
146
|
+
}
|
|
147
|
+
// ── Agent selector ──────────────────────────────────────────────
|
|
148
|
+
export function AgentSelector({ agents, onSelect }) {
|
|
149
|
+
const [cursor, setCursor] = useState(0);
|
|
150
|
+
useInput((input, key) => {
|
|
151
|
+
if (key.upArrow)
|
|
152
|
+
setCursor(c => (c - 1 + agents.length) % agents.length);
|
|
153
|
+
if (key.downArrow)
|
|
154
|
+
setCursor(c => (c + 1) % agents.length);
|
|
155
|
+
if (key.return)
|
|
156
|
+
onSelect(agents[cursor]);
|
|
157
|
+
});
|
|
158
|
+
return (_jsxs(Box, { flexDirection: "column", paddingTop: 1, children: [_jsx(Text, { dimColor: true, children: " Select an agent:" }), _jsx(Text, { children: " " }), agents.map((ag, i) => (_jsxs(Box, { paddingLeft: 2, children: [_jsxs(Text, { color: i === cursor ? '#8b5cf6' : undefined, children: [i === cursor ? '❯' : ' ', ' '] }), _jsx(Text, { bold: i === cursor, children: ag.name }), _jsxs(Text, { dimColor: true, children: [" ", ag.mode] })] }, ag.id))), agents[cursor]?.description && (_jsx(Box, { paddingLeft: 4, paddingTop: 1, children: _jsx(Text, { dimColor: true, children: agents[cursor].description }) }))] }));
|
|
159
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,20 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
* @almyty/chat — interactive REPL with almyty agents.
|
|
4
|
-
*
|
|
5
|
-
* npx @almyty/chat # pick from a menu
|
|
6
|
-
* npx @almyty/chat <agent> # start chatting with that agent
|
|
7
|
-
* npx @almyty/chat <agent> --resume <id> # resume a conversation
|
|
8
|
-
*
|
|
9
|
-
* Reads credentials from ~/.almyty/credentials.json (created by
|
|
10
|
-
* `npx @almyty/auth login`).
|
|
11
|
-
*
|
|
12
|
-
* Slash commands inside the REPL:
|
|
13
|
-
*
|
|
14
|
-
* /switch <agent> switch to a different agent
|
|
15
|
-
* /agents list agents in your organization
|
|
16
|
-
* /clear clear the screen
|
|
17
|
-
* /help show command list
|
|
18
|
-
* /quit, /exit leave the REPL
|
|
19
|
-
*/
|
|
20
|
-
export {};
|
|
2
|
+
export declare const VERSION = "0.2.0";
|
package/dist/index.js
CHANGED
|
@@ -1,272 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* Reads credentials from ~/.almyty/credentials.json (created by
|
|
10
|
-
* `npx @almyty/auth login`).
|
|
11
|
-
*
|
|
12
|
-
* Slash commands inside the REPL:
|
|
13
|
-
*
|
|
14
|
-
* /switch <agent> switch to a different agent
|
|
15
|
-
* /agents list agents in your organization
|
|
16
|
-
* /clear clear the screen
|
|
17
|
-
* /help show command list
|
|
18
|
-
* /quit, /exit leave the REPL
|
|
19
|
-
*/
|
|
20
|
-
import { createInterface } from 'readline';
|
|
21
|
-
import { AlmytyClient, resolveCredentialsOrExit } from '@almyty/client';
|
|
22
|
-
const VERSION = '0.1.0';
|
|
23
|
-
function printHelp() {
|
|
24
|
-
console.log(`
|
|
25
|
-
@almyty/chat v${VERSION}
|
|
26
|
-
|
|
27
|
-
Usage:
|
|
28
|
-
npx @almyty/chat Pick an agent from a menu
|
|
29
|
-
npx @almyty/chat <name|id> Start chatting with that agent
|
|
30
|
-
npx @almyty/chat <name|id> --resume <conversation-id>
|
|
31
|
-
Resume an existing conversation
|
|
32
|
-
|
|
33
|
-
Inside the REPL:
|
|
34
|
-
/switch <agent> switch to a different agent
|
|
35
|
-
/agents list agents in your organization
|
|
36
|
-
/clear clear the screen
|
|
37
|
-
/help show command list
|
|
38
|
-
/quit, /exit leave the REPL
|
|
39
|
-
|
|
40
|
-
Login:
|
|
41
|
-
npx @almyty/auth login Browser-based login
|
|
42
|
-
`);
|
|
43
|
-
}
|
|
44
|
-
function printAgentBanner(agent) {
|
|
45
|
-
console.log('');
|
|
46
|
-
console.log(` 💬 ${agent.name}${agent.mode ? ` [${agent.mode}]` : ''}`);
|
|
47
|
-
if (agent.description)
|
|
48
|
-
console.log(` ${agent.description}`);
|
|
49
|
-
console.log('');
|
|
50
|
-
console.log(' Type your message, or /help for commands. /quit to exit.');
|
|
51
|
-
console.log('');
|
|
52
|
-
}
|
|
53
|
-
function formatStep(step) {
|
|
54
|
-
if (!step)
|
|
55
|
-
return null;
|
|
56
|
-
switch (step.type) {
|
|
57
|
-
case 'llm_call':
|
|
58
|
-
if (step.output?.content)
|
|
59
|
-
return ` ${step.output.content}`;
|
|
60
|
-
return ` · llm_call (${step.tokens?.input ?? 0}↓ / ${step.tokens?.output ?? 0}↑)`;
|
|
61
|
-
case 'tool_call':
|
|
62
|
-
return ` · tool_call → ${step.input?.tool ?? '?'}`;
|
|
63
|
-
case 'sub_agent_call':
|
|
64
|
-
return ` · sub_agent_call → ${step.input?.agentId ?? '?'}`;
|
|
65
|
-
case 'error':
|
|
66
|
-
return ` ! error: ${step.error}`;
|
|
67
|
-
default:
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
async function pickAgent(client) {
|
|
72
|
-
const agents = await client.listAgents();
|
|
73
|
-
if (agents.length === 0) {
|
|
74
|
-
console.error('No agents in your organization. Create one at https://app.almyty.com/agents');
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
if (agents.length === 1) {
|
|
78
|
-
return agents[0];
|
|
79
|
-
}
|
|
80
|
-
console.log('');
|
|
81
|
-
console.log(' Pick an agent:');
|
|
82
|
-
agents.forEach((a, i) => {
|
|
83
|
-
const mode = a.mode ? ` [${a.mode}]` : '';
|
|
84
|
-
console.log(` ${i + 1}. ${a.name}${mode}`);
|
|
85
|
-
});
|
|
86
|
-
console.log('');
|
|
87
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
88
|
-
const answer = await new Promise((resolve) => {
|
|
89
|
-
rl.question(' Enter number: ', (input) => {
|
|
90
|
-
rl.close();
|
|
91
|
-
resolve(input.trim());
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
const idx = parseInt(answer, 10) - 1;
|
|
95
|
-
if (Number.isNaN(idx) || idx < 0 || idx >= agents.length) {
|
|
96
|
-
console.error('Invalid selection.');
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
return agents[idx];
|
|
100
|
-
}
|
|
101
|
-
async function handleSlashCommand(command, state) {
|
|
102
|
-
const [cmd, ...rest] = command.slice(1).trim().split(/\s+/);
|
|
103
|
-
switch (cmd) {
|
|
104
|
-
case 'quit':
|
|
105
|
-
case 'exit':
|
|
106
|
-
return { exit: true };
|
|
107
|
-
case 'help':
|
|
108
|
-
console.log('');
|
|
109
|
-
console.log(' /switch <agent> switch to a different agent');
|
|
110
|
-
console.log(' /agents list agents in your organization');
|
|
111
|
-
console.log(' /clear clear the screen');
|
|
112
|
-
console.log(' /quit, /exit leave the REPL');
|
|
113
|
-
console.log(' /help this help');
|
|
114
|
-
console.log('');
|
|
115
|
-
return {};
|
|
116
|
-
case 'clear':
|
|
117
|
-
process.stdout.write('\x1b[2J\x1b[H');
|
|
118
|
-
printAgentBanner(state.agent);
|
|
119
|
-
return {};
|
|
120
|
-
case 'agents': {
|
|
121
|
-
const agents = await state.client.listAgents();
|
|
122
|
-
if (agents.length === 0) {
|
|
123
|
-
console.log(' (no agents in this organization)');
|
|
124
|
-
return {};
|
|
125
|
-
}
|
|
126
|
-
console.log('');
|
|
127
|
-
for (const a of agents) {
|
|
128
|
-
const marker = a.id === state.agent.id ? '→' : ' ';
|
|
129
|
-
const mode = a.mode ? ` [${a.mode}]` : '';
|
|
130
|
-
console.log(` ${marker} ${a.name}${mode}${a.description ? ' — ' + a.description : ''}`);
|
|
131
|
-
}
|
|
132
|
-
console.log('');
|
|
133
|
-
return {};
|
|
134
|
-
}
|
|
135
|
-
case 'switch': {
|
|
136
|
-
const target = rest.join(' ').trim();
|
|
137
|
-
if (!target) {
|
|
138
|
-
console.log(' Usage: /switch <agent name or id>');
|
|
139
|
-
return {};
|
|
140
|
-
}
|
|
141
|
-
const next = await state.client.findAgentByNameOrId(target);
|
|
142
|
-
if (!next) {
|
|
143
|
-
console.log(` Agent "${target}" not found.`);
|
|
144
|
-
return {};
|
|
145
|
-
}
|
|
146
|
-
state.agent = next;
|
|
147
|
-
state.pendingRunId = null;
|
|
148
|
-
state.conversationId = null;
|
|
149
|
-
printAgentBanner(state.agent);
|
|
150
|
-
return {};
|
|
151
|
-
}
|
|
152
|
-
default:
|
|
153
|
-
console.log(` Unknown command: /${cmd}. Type /help for available commands.`);
|
|
154
|
-
return {};
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
async function handleAutonomousTurn(state, message) {
|
|
158
|
-
let runId;
|
|
159
|
-
if (state.pendingRunId) {
|
|
160
|
-
await state.client.sendRunInput(state.agent.id, state.pendingRunId, message);
|
|
161
|
-
runId = state.pendingRunId;
|
|
162
|
-
state.pendingRunId = null;
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
const run = await state.client.startRun(state.agent.id, message, {
|
|
166
|
-
conversationId: state.conversationId ?? undefined,
|
|
167
|
-
});
|
|
168
|
-
runId = run.id;
|
|
169
|
-
if (run.conversationId) {
|
|
170
|
-
state.conversationId = run.conversationId;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
let printed = 0;
|
|
174
|
-
const final = await state.client.pollRun(state.agent.id, runId, {
|
|
175
|
-
onStep: (run) => {
|
|
176
|
-
const newSteps = (run.steps ?? []).slice(printed);
|
|
177
|
-
for (const step of newSteps) {
|
|
178
|
-
const line = formatStep(step);
|
|
179
|
-
if (line)
|
|
180
|
-
console.log(line);
|
|
181
|
-
}
|
|
182
|
-
printed = run.steps?.length ?? printed;
|
|
183
|
-
},
|
|
184
|
-
});
|
|
185
|
-
if (final.status === 'waiting_input') {
|
|
186
|
-
state.pendingRunId = runId;
|
|
187
|
-
console.log('');
|
|
188
|
-
console.log(' (agent is waiting for your input — answer below)');
|
|
189
|
-
console.log('');
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
if (final.status === 'completed') {
|
|
193
|
-
if (final.output != null) {
|
|
194
|
-
console.log('');
|
|
195
|
-
console.log(typeof final.output === 'string' ? ` ${final.output}` : JSON.stringify(final.output, null, 2));
|
|
196
|
-
}
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
console.log('');
|
|
200
|
-
console.log(` Run ${final.status}${final.error ? `: ${final.error}` : ''}`);
|
|
201
|
-
}
|
|
202
|
-
async function handleWorkflowTurn(state, message) {
|
|
203
|
-
const result = await state.client.invokeAgent(state.agent.id, { message });
|
|
204
|
-
const output = result?.output ?? result?.data?.output ?? result;
|
|
205
|
-
if (output == null) {
|
|
206
|
-
console.log(' (no output)');
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
console.log('');
|
|
210
|
-
if (typeof output === 'string') {
|
|
211
|
-
console.log(` ${output}`);
|
|
212
|
-
}
|
|
213
|
-
else {
|
|
214
|
-
console.log(JSON.stringify(output, null, 2).split('\n').map((l) => ' ' + l).join('\n'));
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
async function handleUserTurn(state, message) {
|
|
218
|
-
if (state.agent.mode === 'autonomous') {
|
|
219
|
-
return handleAutonomousTurn(state, message);
|
|
220
|
-
}
|
|
221
|
-
return handleWorkflowTurn(state, message);
|
|
222
|
-
}
|
|
223
|
-
async function startChat(client, agent, resumeConversationId) {
|
|
224
|
-
const state = { client, agent, pendingRunId: null, conversationId: resumeConversationId ?? null };
|
|
225
|
-
if (resumeConversationId) {
|
|
226
|
-
console.log(` Resuming conversation ${resumeConversationId}`);
|
|
227
|
-
}
|
|
228
|
-
printAgentBanner(state.agent);
|
|
229
|
-
const rl = createInterface({
|
|
230
|
-
input: process.stdin,
|
|
231
|
-
output: process.stdout,
|
|
232
|
-
prompt: '> ',
|
|
233
|
-
});
|
|
234
|
-
rl.prompt();
|
|
235
|
-
rl.on('line', async (raw) => {
|
|
236
|
-
const line = raw.trim();
|
|
237
|
-
if (!line) {
|
|
238
|
-
rl.prompt();
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
if (line.startsWith('/')) {
|
|
242
|
-
try {
|
|
243
|
-
const { exit } = await handleSlashCommand(line, state);
|
|
244
|
-
if (exit) {
|
|
245
|
-
rl.close();
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
catch (err) {
|
|
250
|
-
console.error(` Error: ${err.message}`);
|
|
251
|
-
}
|
|
252
|
-
rl.prompt();
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
try {
|
|
256
|
-
await handleUserTurn(state, line);
|
|
257
|
-
}
|
|
258
|
-
catch (err) {
|
|
259
|
-
console.error(` Error: ${err.message}`);
|
|
260
|
-
}
|
|
261
|
-
console.log('');
|
|
262
|
-
rl.prompt();
|
|
263
|
-
});
|
|
264
|
-
rl.on('close', () => {
|
|
265
|
-
console.log('');
|
|
266
|
-
console.log('Chat ended.');
|
|
267
|
-
process.exit(0);
|
|
268
|
-
});
|
|
269
|
-
}
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { render, Box, Text } from 'ink';
|
|
4
|
+
import { AlmytyClient, resolveCredentialsOrExit, getOrgSlugFromToken } from '@almyty/client';
|
|
5
|
+
import { AgentSelector } from './components.js';
|
|
6
|
+
import { ChatApp, exitMessage } from './app.js';
|
|
7
|
+
export const VERSION = '0.2.0';
|
|
8
|
+
// ── Entry point ─────────────────────────────────────────────────
|
|
270
9
|
async function main() {
|
|
271
10
|
const argv = process.argv.slice(2);
|
|
272
11
|
if (argv.includes('--version') || argv.includes('-v')) {
|
|
@@ -274,39 +13,77 @@ async function main() {
|
|
|
274
13
|
return;
|
|
275
14
|
}
|
|
276
15
|
if (argv.includes('--help') || argv.includes('-h')) {
|
|
277
|
-
|
|
16
|
+
console.log(`almyty chat v${VERSION}\n\nUsage:\n npx @almyty/chat <org>/<agent-slug>\n npx @almyty/chat <org>/<agent-slug> --resume <conversation-id>\n\nExamples:\n npx @almyty/chat acme/support-bot\n npx @almyty/chat myorg/my-agent --resume 60d93c85-...\n\nCommands:\n /help /agents /clear /quit\n\nAuth:\n npx @almyty/auth login`);
|
|
278
17
|
return;
|
|
279
18
|
}
|
|
280
19
|
const creds = resolveCredentialsOrExit();
|
|
281
20
|
const client = new AlmytyClient(creds.url, creds.token);
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
console.error('Usage: --resume <conversation-id>');
|
|
21
|
+
let resumeId;
|
|
22
|
+
const ri = Math.max(argv.indexOf('--resume'), argv.indexOf('-resume'));
|
|
23
|
+
if (ri !== -1) {
|
|
24
|
+
resumeId = argv[ri + 1];
|
|
25
|
+
if (!resumeId || resumeId.startsWith('-')) {
|
|
26
|
+
console.error('--resume requires a conversation id');
|
|
289
27
|
process.exit(1);
|
|
290
28
|
}
|
|
291
|
-
resumeConversationId = nextArg;
|
|
292
29
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
30
|
+
const ref = argv.find(arg => !arg.startsWith('-') && arg !== resumeId);
|
|
31
|
+
// Resolve org slug — from ref (org/slug) or JWT token
|
|
32
|
+
const defaultOrg = getOrgSlugFromToken(creds.token);
|
|
33
|
+
let orgSlug;
|
|
34
|
+
let agentSlug;
|
|
35
|
+
if (ref && ref.includes('/')) {
|
|
36
|
+
[orgSlug, agentSlug] = ref.split('/', 2);
|
|
37
|
+
}
|
|
38
|
+
else if (ref) {
|
|
39
|
+
// Bare slug — use org from JWT
|
|
40
|
+
if (!defaultOrg) {
|
|
41
|
+
console.error('Cannot determine org. Use org/agent-slug format or log in: npx @almyty/auth login');
|
|
299
42
|
process.exit(1);
|
|
300
43
|
}
|
|
44
|
+
orgSlug = defaultOrg;
|
|
45
|
+
agentSlug = ref;
|
|
301
46
|
}
|
|
302
47
|
else {
|
|
303
|
-
|
|
304
|
-
if (!
|
|
48
|
+
// No arg — interactive picker
|
|
49
|
+
if (!defaultOrg) {
|
|
50
|
+
console.error('Usage: npx @almyty/chat <org>/<agent-slug>');
|
|
305
51
|
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
orgSlug = defaultOrg;
|
|
54
|
+
const agents = await client.listAgents();
|
|
55
|
+
if (!agents.length) {
|
|
56
|
+
console.error('No agents found. Create one at https://app.almyty.com/agents');
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
if (agents.length === 1) {
|
|
60
|
+
agentSlug = agents[0].slug || agents[0].name.toLowerCase().replace(/\s+/g, '-');
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const picked = await new Promise((resolve) => {
|
|
64
|
+
const { unmount } = render(_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { paddingTop: 1, paddingLeft: 2, children: [_jsx(Text, { color: "#22d3ee", children: "\u26A1" }), _jsx(Text, { color: "#8b5cf6", bold: true, children: " almyty chat" })] }), _jsx(AgentSelector, { agents: agents, onSelect: (a) => { unmount(); resolve(a); } })] }), { exitOnCtrlC: true });
|
|
65
|
+
});
|
|
66
|
+
if (!picked)
|
|
67
|
+
process.exit(0);
|
|
68
|
+
agentSlug = picked.slug || picked.name.toLowerCase().replace(/\s+/g, '-');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const gw = client.gateway(orgSlug, agentSlug);
|
|
72
|
+
let agent;
|
|
73
|
+
try {
|
|
74
|
+
agent = await gw.getInfo();
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
console.error(`Agent not found: ${orgSlug}/${agentSlug}`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
const { waitUntilExit } = render(_jsx(ChatApp, { client: client, initialAgent: agent, gw: gw, resumeConversationId: resumeId }), { exitOnCtrlC: true });
|
|
81
|
+
await waitUntilExit();
|
|
82
|
+
if (exitMessage) {
|
|
83
|
+
process.stdout.write(exitMessage);
|
|
306
84
|
}
|
|
307
|
-
await startChat(client, agent, resumeConversationId);
|
|
308
85
|
}
|
|
309
|
-
main().catch(
|
|
310
|
-
console.error(
|
|
86
|
+
main().catch(err => {
|
|
87
|
+
console.error(err.message);
|
|
311
88
|
process.exit(1);
|
|
312
89
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@almyty/chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Interactive chat REPL with almyty agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -27,7 +27,13 @@
|
|
|
27
27
|
"author": "almyty",
|
|
28
28
|
"license": "BSL-1.1",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@almyty/client": "^0.1.0"
|
|
30
|
+
"@almyty/client": "^0.1.0",
|
|
31
|
+
"@types/react": "^19.2.14",
|
|
32
|
+
"ink": "^7.0.1",
|
|
33
|
+
"ink-spinner": "^5.0.0",
|
|
34
|
+
"ink-testing-library": "^4.0.0",
|
|
35
|
+
"ink-text-input": "^6.0.0",
|
|
36
|
+
"react": "^19.2.5"
|
|
31
37
|
},
|
|
32
38
|
"devDependencies": {
|
|
33
39
|
"@types/node": "^25.4.0",
|