@almyty/chat 0.1.4 → 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 -8
- package/dist/index.js +64 -367
- 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,9 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
* @almyty/chat — interactive agent chat.
|
|
4
|
-
*
|
|
5
|
-
* npx @almyty/chat # pick from a menu
|
|
6
|
-
* npx @almyty/chat <agent> # start chatting
|
|
7
|
-
* npx @almyty/chat <agent> --resume <id> # resume a conversation
|
|
8
|
-
*/
|
|
9
|
-
export {};
|
|
2
|
+
export declare const VERSION = "0.2.0";
|
package/dist/index.js
CHANGED
|
@@ -1,333 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*/
|
|
9
|
-
import { createInterface } from 'readline';
|
|
10
|
-
import { AlmytyClient, resolveCredentialsOrExit } from '@almyty/client';
|
|
11
|
-
const VERSION = '0.1.3';
|
|
12
|
-
// ── ANSI helpers ────────────────────────────────────────────────
|
|
13
|
-
const DIM = '\x1b[2m';
|
|
14
|
-
const BOLD = '\x1b[1m';
|
|
15
|
-
const RESET = '\x1b[0m';
|
|
16
|
-
const VIOLET = '\x1b[38;5;141m';
|
|
17
|
-
const CYAN = '\x1b[36m';
|
|
18
|
-
const RED = '\x1b[31m';
|
|
19
|
-
const GRAY = '\x1b[90m';
|
|
20
|
-
const CLEAR_LINE = '\x1b[2K\r';
|
|
21
|
-
const isTTY = process.stdout.isTTY ?? false;
|
|
22
|
-
function dim(s) { return isTTY ? `${DIM}${s}${RESET}` : s; }
|
|
23
|
-
function bold(s) { return isTTY ? `${BOLD}${s}${RESET}` : s; }
|
|
24
|
-
function violet(s) { return isTTY ? `${VIOLET}${s}${RESET}` : s; }
|
|
25
|
-
function cyan(s) { return isTTY ? `${CYAN}${s}${RESET}` : s; }
|
|
26
|
-
function red(s) { return isTTY ? `${RED}${s}${RESET}` : s; }
|
|
27
|
-
function gray(s) { return isTTY ? `${GRAY}${s}${RESET}` : s; }
|
|
28
|
-
// ── Spinner ─────────────────────────────────────────────────────
|
|
29
|
-
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
30
|
-
class Spinner {
|
|
31
|
-
interval = null;
|
|
32
|
-
frame = 0;
|
|
33
|
-
start(label = 'Thinking') {
|
|
34
|
-
if (!isTTY)
|
|
35
|
-
return;
|
|
36
|
-
this.frame = 0;
|
|
37
|
-
this.interval = setInterval(() => {
|
|
38
|
-
const f = SPINNER_FRAMES[this.frame % SPINNER_FRAMES.length];
|
|
39
|
-
process.stdout.write(`${CLEAR_LINE}${GRAY}${f} ${label}...${RESET}`);
|
|
40
|
-
this.frame++;
|
|
41
|
-
}, 80);
|
|
42
|
-
}
|
|
43
|
-
stop() {
|
|
44
|
-
if (this.interval) {
|
|
45
|
-
clearInterval(this.interval);
|
|
46
|
-
this.interval = null;
|
|
47
|
-
process.stdout.write(CLEAR_LINE);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
// ── Word wrapping ───────────────────────────────────────────────
|
|
52
|
-
function wordWrap(text, width) {
|
|
53
|
-
if (width <= 0)
|
|
54
|
-
return text;
|
|
55
|
-
const lines = [];
|
|
56
|
-
for (const paragraph of text.split('\n')) {
|
|
57
|
-
if (paragraph.length <= width) {
|
|
58
|
-
lines.push(paragraph);
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
let line = '';
|
|
62
|
-
for (const word of paragraph.split(/\s+/)) {
|
|
63
|
-
if (line.length + word.length + 1 > width && line.length > 0) {
|
|
64
|
-
lines.push(line);
|
|
65
|
-
line = word;
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
line = line ? `${line} ${word}` : word;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
if (line)
|
|
72
|
-
lines.push(line);
|
|
73
|
-
}
|
|
74
|
-
return lines.join('\n');
|
|
75
|
-
}
|
|
76
|
-
// ── Display ─────────────────────────────────────────────────────
|
|
77
|
-
function printBanner(agent) {
|
|
78
|
-
const cols = process.stdout.columns || 80;
|
|
79
|
-
const line = gray('─'.repeat(Math.min(cols, 60)));
|
|
80
|
-
console.log('');
|
|
81
|
-
console.log(line);
|
|
82
|
-
console.log(` ${violet(bold(agent.name))} ${dim(agent.mode ?? '')}`);
|
|
83
|
-
if (agent.description)
|
|
84
|
-
console.log(` ${dim(agent.description)}`);
|
|
85
|
-
console.log(line);
|
|
86
|
-
console.log(dim(' Type a message to chat. /help for commands, /quit to exit.'));
|
|
87
|
-
console.log('');
|
|
88
|
-
}
|
|
89
|
-
function printResponse(text) {
|
|
90
|
-
const cols = process.stdout.columns || 80;
|
|
91
|
-
const maxWidth = Math.min(cols - 2, 78);
|
|
92
|
-
const wrapped = wordWrap(text.trim(), maxWidth);
|
|
93
|
-
console.log('');
|
|
94
|
-
for (const line of wrapped.split('\n')) {
|
|
95
|
-
console.log(` ${line}`);
|
|
96
|
-
}
|
|
97
|
-
console.log('');
|
|
98
|
-
}
|
|
99
|
-
function printToolCall(name) {
|
|
100
|
-
process.stdout.write(`${CLEAR_LINE}${gray(` > ${name}`)}\n`);
|
|
101
|
-
}
|
|
102
|
-
function printError(msg) {
|
|
103
|
-
console.log(`\n ${red('error')} ${msg}\n`);
|
|
104
|
-
}
|
|
105
|
-
// ── Agent picker ────────────────────────────────────────────────
|
|
106
|
-
async function pickAgent(client) {
|
|
107
|
-
const agents = await client.listAgents();
|
|
108
|
-
if (agents.length === 0) {
|
|
109
|
-
printError('No agents found. Create one at https://app.almyty.com/agents');
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
if (agents.length === 1)
|
|
113
|
-
return agents[0];
|
|
114
|
-
console.log('');
|
|
115
|
-
console.log(bold(' Select an agent:'));
|
|
116
|
-
console.log('');
|
|
117
|
-
agents.forEach((a, i) => {
|
|
118
|
-
const mode = a.mode ? dim(` [${a.mode}]`) : '';
|
|
119
|
-
console.log(` ${cyan(`${i + 1}.`)} ${a.name}${mode}`);
|
|
120
|
-
});
|
|
121
|
-
console.log('');
|
|
122
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
123
|
-
const answer = await new Promise((resolve) => {
|
|
124
|
-
rl.question(` ${cyan('>')} `, (input) => {
|
|
125
|
-
rl.close();
|
|
126
|
-
resolve(input.trim());
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
const idx = parseInt(answer, 10) - 1;
|
|
130
|
-
if (Number.isNaN(idx) || idx < 0 || idx >= agents.length) {
|
|
131
|
-
printError('Invalid selection.');
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
return agents[idx];
|
|
135
|
-
}
|
|
136
|
-
// ── Slash commands ──────────────────────────────────────────────
|
|
137
|
-
async function handleSlashCommand(command, state) {
|
|
138
|
-
const [cmd, ...rest] = command.slice(1).trim().split(/\s+/);
|
|
139
|
-
switch (cmd) {
|
|
140
|
-
case 'quit':
|
|
141
|
-
case 'exit':
|
|
142
|
-
case 'q':
|
|
143
|
-
return { exit: true };
|
|
144
|
-
case 'help':
|
|
145
|
-
case 'h':
|
|
146
|
-
console.log('');
|
|
147
|
-
console.log(` ${cyan('/switch')} ${dim('<agent>')} switch to a different agent`);
|
|
148
|
-
console.log(` ${cyan('/agents')} list available agents`);
|
|
149
|
-
console.log(` ${cyan('/clear')} clear the screen`);
|
|
150
|
-
console.log(` ${cyan('/quit')} exit`);
|
|
151
|
-
console.log('');
|
|
152
|
-
return {};
|
|
153
|
-
case 'clear':
|
|
154
|
-
case 'c':
|
|
155
|
-
process.stdout.write('\x1b[2J\x1b[H');
|
|
156
|
-
printBanner(state.agent);
|
|
157
|
-
return {};
|
|
158
|
-
case 'agents': {
|
|
159
|
-
const agents = await state.client.listAgents();
|
|
160
|
-
if (agents.length === 0) {
|
|
161
|
-
console.log(dim(' (no agents)'));
|
|
162
|
-
return {};
|
|
163
|
-
}
|
|
164
|
-
console.log('');
|
|
165
|
-
for (const a of agents) {
|
|
166
|
-
const marker = a.id === state.agent.id ? violet('>') : ' ';
|
|
167
|
-
const mode = a.mode ? dim(` [${a.mode}]`) : '';
|
|
168
|
-
console.log(` ${marker} ${a.name}${mode}`);
|
|
169
|
-
}
|
|
170
|
-
console.log('');
|
|
171
|
-
return {};
|
|
172
|
-
}
|
|
173
|
-
case 'switch': {
|
|
174
|
-
const target = rest.join(' ').trim();
|
|
175
|
-
if (!target) {
|
|
176
|
-
console.log(dim(' Usage: /switch <agent>'));
|
|
177
|
-
return {};
|
|
178
|
-
}
|
|
179
|
-
const next = await state.client.findAgentByNameOrId(target);
|
|
180
|
-
if (!next) {
|
|
181
|
-
printError(`Agent "${target}" not found.`);
|
|
182
|
-
return {};
|
|
183
|
-
}
|
|
184
|
-
state.agent = next;
|
|
185
|
-
state.pendingRunId = null;
|
|
186
|
-
state.conversationId = null;
|
|
187
|
-
printBanner(state.agent);
|
|
188
|
-
return {};
|
|
189
|
-
}
|
|
190
|
-
default:
|
|
191
|
-
console.log(dim(` Unknown command: /${cmd}`));
|
|
192
|
-
return {};
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
// ── Turn handling ───────────────────────────────────────────────
|
|
196
|
-
async function handleAutonomousTurn(state, message) {
|
|
197
|
-
const spinner = new Spinner();
|
|
198
|
-
let runId;
|
|
199
|
-
try {
|
|
200
|
-
spinner.start();
|
|
201
|
-
if (state.pendingRunId) {
|
|
202
|
-
await state.client.sendRunInput(state.agent.id, state.pendingRunId, message);
|
|
203
|
-
runId = state.pendingRunId;
|
|
204
|
-
state.pendingRunId = null;
|
|
205
|
-
}
|
|
206
|
-
else {
|
|
207
|
-
const run = await state.client.startRun(state.agent.id, message, {
|
|
208
|
-
conversationId: state.conversationId ?? undefined,
|
|
209
|
-
});
|
|
210
|
-
runId = run.id;
|
|
211
|
-
if (run.conversationId) {
|
|
212
|
-
state.conversationId = run.conversationId;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
// Poll for completion — show tool calls as they happen
|
|
216
|
-
const seenSteps = new Set();
|
|
217
|
-
const final = await state.client.pollRun(state.agent.id, runId, {
|
|
218
|
-
intervalMs: 800,
|
|
219
|
-
onStep: (run) => {
|
|
220
|
-
const steps = run.steps ?? [];
|
|
221
|
-
for (let i = 0; i < steps.length; i++) {
|
|
222
|
-
if (seenSteps.has(i))
|
|
223
|
-
continue;
|
|
224
|
-
seenSteps.add(i);
|
|
225
|
-
const step = steps[i];
|
|
226
|
-
if (step?.type === 'tool_call' && step?.input?.tool) {
|
|
227
|
-
spinner.stop();
|
|
228
|
-
printToolCall(step.input.tool);
|
|
229
|
-
spinner.start();
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
},
|
|
233
|
-
});
|
|
234
|
-
spinner.stop();
|
|
235
|
-
if (final.status === 'waiting_input') {
|
|
236
|
-
state.pendingRunId = runId;
|
|
237
|
-
console.log(dim('\n (waiting for your input)\n'));
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
if (final.status === 'completed' && final.output != null) {
|
|
241
|
-
const text = typeof final.output === 'string' ? final.output : JSON.stringify(final.output, null, 2);
|
|
242
|
-
printResponse(text);
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
if (final.status === 'failed') {
|
|
246
|
-
printError(final.error || 'Run failed');
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
if (final.status === 'cancelled') {
|
|
250
|
-
console.log(dim('\n (cancelled)\n'));
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
catch (err) {
|
|
255
|
-
spinner.stop();
|
|
256
|
-
throw err;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
async function handleWorkflowTurn(state, message) {
|
|
260
|
-
const spinner = new Spinner();
|
|
261
|
-
try {
|
|
262
|
-
spinner.start();
|
|
263
|
-
const result = await state.client.invokeAgent(state.agent.id, { message });
|
|
264
|
-
spinner.stop();
|
|
265
|
-
const output = result?.output ?? result?.data?.output ?? result;
|
|
266
|
-
if (output == null) {
|
|
267
|
-
console.log(dim('\n (no output)\n'));
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
const text = typeof output === 'string' ? output : JSON.stringify(output, null, 2);
|
|
271
|
-
printResponse(text);
|
|
272
|
-
}
|
|
273
|
-
catch (err) {
|
|
274
|
-
spinner.stop();
|
|
275
|
-
throw err;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
async function handleUserTurn(state, message) {
|
|
279
|
-
if (state.agent.mode === 'autonomous') {
|
|
280
|
-
return handleAutonomousTurn(state, message);
|
|
281
|
-
}
|
|
282
|
-
return handleWorkflowTurn(state, message);
|
|
283
|
-
}
|
|
284
|
-
// ── Main loop ───────────────────────────────────────────────────
|
|
285
|
-
async function startChat(client, agent, resumeConversationId) {
|
|
286
|
-
const state = { client, agent, pendingRunId: null, conversationId: resumeConversationId ?? null };
|
|
287
|
-
if (resumeConversationId) {
|
|
288
|
-
console.log(dim(` Resuming conversation ${resumeConversationId}`));
|
|
289
|
-
}
|
|
290
|
-
printBanner(state.agent);
|
|
291
|
-
const prompt = `${VIOLET}>${RESET} `;
|
|
292
|
-
const rl = createInterface({
|
|
293
|
-
input: process.stdin,
|
|
294
|
-
output: process.stdout,
|
|
295
|
-
prompt: isTTY ? prompt : '> ',
|
|
296
|
-
});
|
|
297
|
-
rl.prompt();
|
|
298
|
-
rl.on('line', async (raw) => {
|
|
299
|
-
const line = raw.trim();
|
|
300
|
-
if (!line) {
|
|
301
|
-
rl.prompt();
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
if (line.startsWith('/')) {
|
|
305
|
-
try {
|
|
306
|
-
const { exit } = await handleSlashCommand(line, state);
|
|
307
|
-
if (exit) {
|
|
308
|
-
rl.close();
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
catch (err) {
|
|
313
|
-
printError(err.message);
|
|
314
|
-
}
|
|
315
|
-
rl.prompt();
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
try {
|
|
319
|
-
await handleUserTurn(state, line);
|
|
320
|
-
}
|
|
321
|
-
catch (err) {
|
|
322
|
-
printError(err.message);
|
|
323
|
-
}
|
|
324
|
-
rl.prompt();
|
|
325
|
-
});
|
|
326
|
-
rl.on('close', () => {
|
|
327
|
-
console.log('');
|
|
328
|
-
process.exit(0);
|
|
329
|
-
});
|
|
330
|
-
}
|
|
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';
|
|
331
8
|
// ── Entry point ─────────────────────────────────────────────────
|
|
332
9
|
async function main() {
|
|
333
10
|
const argv = process.argv.slice(2);
|
|
@@ -336,57 +13,77 @@ async function main() {
|
|
|
336
13
|
return;
|
|
337
14
|
}
|
|
338
15
|
if (argv.includes('--help') || argv.includes('-h')) {
|
|
339
|
-
console.log(`
|
|
340
|
-
${bold('almyty chat')} ${dim(`v${VERSION}`)}
|
|
341
|
-
|
|
342
|
-
${bold('Usage:')}
|
|
343
|
-
npx @almyty/chat Pick an agent
|
|
344
|
-
npx @almyty/chat ${dim('<agent>')} Start chatting
|
|
345
|
-
npx @almyty/chat ${dim('<agent>')} --resume ${dim('<conv-id>')} Resume conversation
|
|
346
|
-
|
|
347
|
-
${bold('Commands:')}
|
|
348
|
-
/switch ${dim('<agent>')} switch agent
|
|
349
|
-
/agents list agents
|
|
350
|
-
/clear clear screen
|
|
351
|
-
/quit exit
|
|
352
|
-
|
|
353
|
-
${bold('Auth:')}
|
|
354
|
-
npx @almyty/auth login
|
|
355
|
-
`);
|
|
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`);
|
|
356
17
|
return;
|
|
357
18
|
}
|
|
358
19
|
const creds = resolveCredentialsOrExit();
|
|
359
20
|
const client = new AlmytyClient(creds.url, creds.token);
|
|
360
|
-
let
|
|
361
|
-
const
|
|
362
|
-
if (
|
|
363
|
-
|
|
364
|
-
if (!
|
|
365
|
-
|
|
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');
|
|
366
27
|
process.exit(1);
|
|
367
28
|
}
|
|
368
|
-
resumeConversationId = nextArg;
|
|
369
29
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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');
|
|
379
42
|
process.exit(1);
|
|
380
43
|
}
|
|
44
|
+
orgSlug = defaultOrg;
|
|
45
|
+
agentSlug = ref;
|
|
381
46
|
}
|
|
382
47
|
else {
|
|
383
|
-
|
|
384
|
-
if (!
|
|
48
|
+
// No arg — interactive picker
|
|
49
|
+
if (!defaultOrg) {
|
|
50
|
+
console.error('Usage: npx @almyty/chat <org>/<agent-slug>');
|
|
385
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);
|
|
386
84
|
}
|
|
387
|
-
await startChat(client, agent, resumeConversationId);
|
|
388
85
|
}
|
|
389
|
-
main().catch(
|
|
390
|
-
|
|
86
|
+
main().catch(err => {
|
|
87
|
+
console.error(err.message);
|
|
391
88
|
process.exit(1);
|
|
392
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",
|