@almyty/chat 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,20 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * @almyty/chat — interactive REPL with almyty agents.
3
+ * @almyty/chat — interactive agent chat.
4
4
  *
5
5
  * npx @almyty/chat # pick from a menu
6
- * npx @almyty/chat <agent> # start chatting with that agent
6
+ * npx @almyty/chat <agent> # start chatting
7
7
  * npx @almyty/chat <agent> --resume <id> # resume a conversation
8
- *
9
- * Reads credentials from ~/.almyty/credentials.json (created by
10
- * `npx @almyty/auth login`).
11
- *
12
- * Slash commands inside the REPL:
13
- *
14
- * /switch <agent> switch to a different agent
15
- * /agents list agents in your organization
16
- * /clear clear the screen
17
- * /help show command list
18
- * /quit, /exit leave the REPL
19
8
  */
20
9
  export {};
package/dist/index.js CHANGED
@@ -1,133 +1,171 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * @almyty/chat — interactive REPL with almyty agents.
3
+ * @almyty/chat — interactive agent chat.
4
4
  *
5
5
  * npx @almyty/chat # pick from a menu
6
- * npx @almyty/chat <agent> # start chatting with that agent
6
+ * npx @almyty/chat <agent> # start chatting
7
7
  * npx @almyty/chat <agent> --resume <id> # resume a conversation
8
- *
9
- * Reads credentials from ~/.almyty/credentials.json (created by
10
- * `npx @almyty/auth login`).
11
- *
12
- * Slash commands inside the REPL:
13
- *
14
- * /switch <agent> switch to a different agent
15
- * /agents list agents in your organization
16
- * /clear clear the screen
17
- * /help show command list
18
- * /quit, /exit leave the REPL
19
8
  */
20
9
  import { createInterface } from 'readline';
21
10
  import { AlmytyClient, resolveCredentialsOrExit } from '@almyty/client';
22
- const VERSION = '0.1.0';
23
- function printHelp() {
24
- console.log(`
25
- @almyty/chat v${VERSION}
26
-
27
- Usage:
28
- npx @almyty/chat Pick an agent from a menu
29
- npx @almyty/chat <name|id> Start chatting with that agent
30
- npx @almyty/chat <name|id> --resume <conversation-id>
31
- Resume an existing conversation
32
-
33
- Inside the REPL:
34
- /switch <agent> switch to a different agent
35
- /agents list agents in your organization
36
- /clear clear the screen
37
- /help show command list
38
- /quit, /exit leave the REPL
39
-
40
- Login:
41
- npx @almyty/auth login Browser-based login
42
- `);
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
+ }
43
50
  }
44
- function printAgentBanner(agent) {
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)));
45
80
  console.log('');
46
- console.log(` 💬 ${agent.name}${agent.mode ? ` [${agent.mode}]` : ''}`);
81
+ console.log(line);
82
+ console.log(` ${violet(bold(agent.name))} ${dim(agent.mode ?? '')}`);
47
83
  if (agent.description)
48
- console.log(` ${agent.description}`);
49
- console.log('');
50
- console.log(' Type your message, or /help for commands. /quit to exit.');
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.'));
51
87
  console.log('');
52
88
  }
53
- function formatStep(step) {
54
- if (!step)
55
- return null;
56
- switch (step.type) {
57
- case 'llm_call':
58
- if (step.output?.content)
59
- return ` ${step.output.content}`;
60
- return ` · llm_call (${step.tokens?.input ?? 0}↓ / ${step.tokens?.output ?? 0}↑)`;
61
- case 'tool_call':
62
- return ` · tool_call → ${step.input?.tool ?? '?'}`;
63
- case 'sub_agent_call':
64
- return ` · sub_agent_call → ${step.input?.agentId ?? '?'}`;
65
- case 'error':
66
- return ` ! error: ${step.error}`;
67
- default:
68
- return null;
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}`);
69
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`);
70
104
  }
105
+ // ── Agent picker ────────────────────────────────────────────────
71
106
  async function pickAgent(client) {
72
107
  const agents = await client.listAgents();
73
108
  if (agents.length === 0) {
74
- console.error('No agents in your organization. Create one at https://app.almyty.com/agents');
109
+ printError('No agents found. Create one at https://app.almyty.com/agents');
75
110
  return null;
76
111
  }
77
- if (agents.length === 1) {
112
+ if (agents.length === 1)
78
113
  return agents[0];
79
- }
80
114
  console.log('');
81
- console.log(' Pick an agent:');
115
+ console.log(bold(' Select an agent:'));
116
+ console.log('');
82
117
  agents.forEach((a, i) => {
83
- const mode = a.mode ? ` [${a.mode}]` : '';
84
- console.log(` ${i + 1}. ${a.name}${mode}`);
118
+ const mode = a.mode ? dim(` [${a.mode}]`) : '';
119
+ console.log(` ${cyan(`${i + 1}.`)} ${a.name}${mode}`);
85
120
  });
86
121
  console.log('');
87
122
  const rl = createInterface({ input: process.stdin, output: process.stdout });
88
123
  const answer = await new Promise((resolve) => {
89
- rl.question(' Enter number: ', (input) => {
124
+ rl.question(` ${cyan('>')} `, (input) => {
90
125
  rl.close();
91
126
  resolve(input.trim());
92
127
  });
93
128
  });
94
129
  const idx = parseInt(answer, 10) - 1;
95
130
  if (Number.isNaN(idx) || idx < 0 || idx >= agents.length) {
96
- console.error('Invalid selection.');
131
+ printError('Invalid selection.');
97
132
  return null;
98
133
  }
99
134
  return agents[idx];
100
135
  }
136
+ // ── Slash commands ──────────────────────────────────────────────
101
137
  async function handleSlashCommand(command, state) {
102
138
  const [cmd, ...rest] = command.slice(1).trim().split(/\s+/);
103
139
  switch (cmd) {
104
140
  case 'quit':
105
141
  case 'exit':
142
+ case 'q':
106
143
  return { exit: true };
107
144
  case 'help':
145
+ case 'h':
108
146
  console.log('');
109
- console.log(' /switch <agent> switch to a different agent');
110
- console.log(' /agents list agents in your organization');
111
- console.log(' /clear clear the screen');
112
- console.log(' /quit, /exit leave the REPL');
113
- console.log(' /help this help');
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`);
114
151
  console.log('');
115
152
  return {};
116
153
  case 'clear':
154
+ case 'c':
117
155
  process.stdout.write('\x1b[2J\x1b[H');
118
- printAgentBanner(state.agent);
156
+ printBanner(state.agent);
119
157
  return {};
120
158
  case 'agents': {
121
159
  const agents = await state.client.listAgents();
122
160
  if (agents.length === 0) {
123
- console.log(' (no agents in this organization)');
161
+ console.log(dim(' (no agents)'));
124
162
  return {};
125
163
  }
126
164
  console.log('');
127
165
  for (const a of agents) {
128
- const marker = a.id === state.agent.id ? '→' : ' ';
129
- const mode = a.mode ? ` [${a.mode}]` : '';
130
- console.log(` ${marker} ${a.name}${mode}${a.description ? ' — ' + a.description : ''}`);
166
+ const marker = a.id === state.agent.id ? violet('>') : ' ';
167
+ const mode = a.mode ? dim(` [${a.mode}]`) : '';
168
+ console.log(` ${marker} ${a.name}${mode}`);
131
169
  }
132
170
  console.log('');
133
171
  return {};
@@ -135,83 +173,106 @@ async function handleSlashCommand(command, state) {
135
173
  case 'switch': {
136
174
  const target = rest.join(' ').trim();
137
175
  if (!target) {
138
- console.log(' Usage: /switch <agent name or id>');
176
+ console.log(dim(' Usage: /switch <agent>'));
139
177
  return {};
140
178
  }
141
179
  const next = await state.client.findAgentByNameOrId(target);
142
180
  if (!next) {
143
- console.log(` Agent "${target}" not found.`);
181
+ printError(`Agent "${target}" not found.`);
144
182
  return {};
145
183
  }
146
184
  state.agent = next;
147
185
  state.pendingRunId = null;
148
186
  state.conversationId = null;
149
- printAgentBanner(state.agent);
187
+ printBanner(state.agent);
150
188
  return {};
151
189
  }
152
190
  default:
153
- console.log(` Unknown command: /${cmd}. Type /help for available commands.`);
191
+ console.log(dim(` Unknown command: /${cmd}`));
154
192
  return {};
155
193
  }
156
194
  }
195
+ // ── Turn handling ───────────────────────────────────────────────
157
196
  async function handleAutonomousTurn(state, message) {
197
+ const spinner = new Spinner();
158
198
  let runId;
159
- if (state.pendingRunId) {
160
- await state.client.sendRunInput(state.agent.id, state.pendingRunId, message);
161
- runId = state.pendingRunId;
162
- state.pendingRunId = null;
163
- }
164
- else {
165
- const run = await state.client.startRun(state.agent.id, message, {
166
- conversationId: state.conversationId ?? undefined,
167
- });
168
- runId = run.id;
169
- if (run.conversationId) {
170
- state.conversationId = run.conversationId;
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;
171
205
  }
172
- }
173
- let printed = 0;
174
- const final = await state.client.pollRun(state.agent.id, runId, {
175
- onStep: (run) => {
176
- const newSteps = (run.steps ?? []).slice(printed);
177
- for (const step of newSteps) {
178
- const line = formatStep(step);
179
- if (line)
180
- console.log(line);
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;
181
213
  }
182
- printed = run.steps?.length ?? printed;
183
- },
184
- });
185
- if (final.status === 'waiting_input') {
186
- state.pendingRunId = runId;
187
- console.log('');
188
- console.log(' (agent is waiting for your input — answer below)');
189
- console.log('');
190
- return;
191
- }
192
- if (final.status === 'completed') {
193
- if (final.output != null) {
194
- console.log('');
195
- console.log(typeof final.output === 'string' ? ` ${final.output}` : JSON.stringify(final.output, null, 2));
196
214
  }
197
- return;
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;
198
257
  }
199
- console.log('');
200
- console.log(` Run ${final.status}${final.error ? `: ${final.error}` : ''}`);
201
258
  }
202
259
  async function handleWorkflowTurn(state, message) {
203
- const result = await state.client.invokeAgent(state.agent.id, { message });
204
- const output = result?.output ?? result?.data?.output ?? result;
205
- if (output == null) {
206
- console.log(' (no output)');
207
- return;
208
- }
209
- console.log('');
210
- if (typeof output === 'string') {
211
- console.log(` ${output}`);
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);
212
272
  }
213
- else {
214
- console.log(JSON.stringify(output, null, 2).split('\n').map((l) => ' ' + l).join('\n'));
273
+ catch (err) {
274
+ spinner.stop();
275
+ throw err;
215
276
  }
216
277
  }
217
278
  async function handleUserTurn(state, message) {
@@ -220,16 +281,18 @@ async function handleUserTurn(state, message) {
220
281
  }
221
282
  return handleWorkflowTurn(state, message);
222
283
  }
284
+ // ── Main loop ───────────────────────────────────────────────────
223
285
  async function startChat(client, agent, resumeConversationId) {
224
286
  const state = { client, agent, pendingRunId: null, conversationId: resumeConversationId ?? null };
225
287
  if (resumeConversationId) {
226
- console.log(` Resuming conversation ${resumeConversationId}`);
288
+ console.log(dim(` Resuming conversation ${resumeConversationId}`));
227
289
  }
228
- printAgentBanner(state.agent);
290
+ printBanner(state.agent);
291
+ const prompt = `${VIOLET}>${RESET} `;
229
292
  const rl = createInterface({
230
293
  input: process.stdin,
231
294
  output: process.stdout,
232
- prompt: '> ',
295
+ prompt: isTTY ? prompt : '> ',
233
296
  });
234
297
  rl.prompt();
235
298
  rl.on('line', async (raw) => {
@@ -247,7 +310,7 @@ async function startChat(client, agent, resumeConversationId) {
247
310
  }
248
311
  }
249
312
  catch (err) {
250
- console.error(` Error: ${err.message}`);
313
+ printError(err.message);
251
314
  }
252
315
  rl.prompt();
253
316
  return;
@@ -256,17 +319,16 @@ async function startChat(client, agent, resumeConversationId) {
256
319
  await handleUserTurn(state, line);
257
320
  }
258
321
  catch (err) {
259
- console.error(` Error: ${err.message}`);
322
+ printError(err.message);
260
323
  }
261
- console.log('');
262
324
  rl.prompt();
263
325
  });
264
326
  rl.on('close', () => {
265
327
  console.log('');
266
- console.log('Chat ended.');
267
328
  process.exit(0);
268
329
  });
269
330
  }
331
+ // ── Entry point ─────────────────────────────────────────────────
270
332
  async function main() {
271
333
  const argv = process.argv.slice(2);
272
334
  if (argv.includes('--version') || argv.includes('-v')) {
@@ -274,18 +336,33 @@ async function main() {
274
336
  return;
275
337
  }
276
338
  if (argv.includes('--help') || argv.includes('-h')) {
277
- printHelp();
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
+ `);
278
356
  return;
279
357
  }
280
358
  const creds = resolveCredentialsOrExit();
281
359
  const client = new AlmytyClient(creds.url, creds.token);
282
- // Parse --resume <conversation-id>
283
360
  let resumeConversationId;
284
361
  const resumeIdx = argv.indexOf('--resume');
285
362
  if (resumeIdx !== -1) {
286
363
  const nextArg = argv[resumeIdx + 1];
287
364
  if (!nextArg || nextArg.startsWith('-')) {
288
- console.error('Usage: --resume <conversation-id>');
365
+ printError('Usage: --resume <conversation-id>');
289
366
  process.exit(1);
290
367
  }
291
368
  resumeConversationId = nextArg;
@@ -293,9 +370,12 @@ async function main() {
293
370
  let agent = null;
294
371
  const ref = argv.find((a) => !a.startsWith('-') && a !== resumeConversationId);
295
372
  if (ref) {
373
+ const spinner = new Spinner();
374
+ spinner.start('Connecting');
296
375
  agent = await client.findAgentByNameOrId(ref);
376
+ spinner.stop();
297
377
  if (!agent) {
298
- console.error(`Agent not found: ${ref}`);
378
+ printError(`Agent not found: ${ref}`);
299
379
  process.exit(1);
300
380
  }
301
381
  }
@@ -307,6 +387,6 @@ async function main() {
307
387
  await startChat(client, agent, resumeConversationId);
308
388
  }
309
389
  main().catch((err) => {
310
- console.error(`Error: ${err.message}`);
390
+ printError(err.message);
311
391
  process.exit(1);
312
392
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@almyty/chat",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Interactive chat REPL with almyty agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",