@almyty/chat 0.1.4 → 1.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/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,7 +1,10 @@
1
1
  {
2
2
  "name": "@almyty/chat",
3
- "version": "0.1.4",
4
- "description": "Interactive chat REPL with almyty agents",
3
+ "version": "1.2.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "description": "Chat with your almyty agents in an interactive terminal REPL \u2014 multi-turn and streaming, with tool calls and autonomous runs.",
5
8
  "type": "module",
6
9
  "main": "dist/index.js",
7
10
  "bin": {
@@ -25,14 +28,29 @@
25
28
  "cli"
26
29
  ],
27
30
  "author": "almyty",
28
- "license": "BSL-1.1",
31
+ "license": "Apache-2.0",
29
32
  "dependencies": {
30
- "@almyty/client": "^0.1.0"
33
+ "@almyty/client": "^1.2.0",
34
+ "@types/react": "^19.2.14",
35
+ "ink": "^7.0.1",
36
+ "ink-spinner": "^5.0.0",
37
+ "ink-testing-library": "^4.0.0",
38
+ "ink-text-input": "^6.0.0",
39
+ "react": "^19.2.5"
31
40
  },
32
41
  "devDependencies": {
33
42
  "@types/node": "^25.4.0",
34
43
  "tsx": "^4.7.0",
35
44
  "typescript": "^5.3.0",
36
45
  "vitest": "^4.1.0"
46
+ },
47
+ "homepage": "https://almyty.com",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/almyty-inc/almyty.git",
51
+ "directory": "packages/chat-cli"
52
+ },
53
+ "bugs": {
54
+ "url": "https://github.com/almyty-inc/almyty/issues"
37
55
  }
38
56
  }