@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 CHANGED
@@ -1,33 +1,40 @@
1
1
  # @almyty/chat
2
2
 
3
- Interactive chat REPL for almyty agents. Supports both workflow and autonomous agent modes.
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
- Run without arguments to pick an agent from an interactive menu, or pass a name or ID to connect directly.
16
-
17
- ```bash
18
- $ npx @almyty/chat # interactive agent picker
19
- $ npx @almyty/chat <name|id> # connect to a specific agent
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
- ## REPL commands
28
+ ## Features
23
29
 
24
- | Command | Description |
25
- |---------|-------------|
26
- | `/switch <agent>` | Switch to a different agent |
27
- | `/agents` | List agents (current one marked) |
28
- | `/clear` | Clear the screen |
29
- | `/help` | Show available commands |
30
- | `/quit` | Exit the REPL |
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;
@@ -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
- * @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
- 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 resumeConversationId;
361
- const resumeIdx = argv.indexOf('--resume');
362
- if (resumeIdx !== -1) {
363
- const nextArg = argv[resumeIdx + 1];
364
- if (!nextArg || nextArg.startsWith('-')) {
365
- printError('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');
366
27
  process.exit(1);
367
28
  }
368
- resumeConversationId = nextArg;
369
29
  }
370
- let agent = null;
371
- const ref = argv.find((a) => !a.startsWith('-') && a !== resumeConversationId);
372
- if (ref) {
373
- const spinner = new Spinner();
374
- spinner.start('Connecting');
375
- agent = await client.findAgentByNameOrId(ref);
376
- spinner.stop();
377
- if (!agent) {
378
- printError(`Agent not found: ${ref}`);
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
- agent = await pickAgent(client);
384
- if (!agent)
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((err) => {
390
- printError(err.message);
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.1.4",
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",