@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/README.md +31 -20
- package/dist/app.d.ts +25 -0
- package/dist/app.js +502 -0
- package/dist/commands.d.ts +25 -0
- package/dist/commands.js +79 -0
- package/dist/components.d.ts +55 -0
- package/dist/components.js +183 -0
- package/dist/index.d.ts +1 -8
- package/dist/index.js +64 -367
- package/package.json +22 -4
package/dist/commands.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// ── Slash command resolution ────────────────────────────────────
|
|
2
|
+
export const SLASH_COMMANDS = [
|
|
3
|
+
'agents', 'tools', 'runners', 'code', 'code-stop', 'esc', 'help', 'clear', 'quit',
|
|
4
|
+
];
|
|
5
|
+
export const ALIASES = {
|
|
6
|
+
agent: 'agents', ag: 'agents', switch: 'agents', sw: 'agents',
|
|
7
|
+
tool: 'tools', t: 'tools',
|
|
8
|
+
runner: 'runners',
|
|
9
|
+
stop: 'code-stop',
|
|
10
|
+
detach: 'esc',
|
|
11
|
+
h: 'help', '?': 'help',
|
|
12
|
+
cls: 'clear', c: 'clear',
|
|
13
|
+
exit: 'quit', q: 'quit',
|
|
14
|
+
};
|
|
15
|
+
export const COMMAND_DESCS = {
|
|
16
|
+
agents: 'browse and switch agents',
|
|
17
|
+
tools: 'show available tools',
|
|
18
|
+
runners: 'list your runners + coding CLIs',
|
|
19
|
+
code: 'run a coding task on a runner',
|
|
20
|
+
'code-stop': 'stop the active coding session',
|
|
21
|
+
esc: 'leave coding mode (session keeps running)',
|
|
22
|
+
help: 'show commands',
|
|
23
|
+
clear: 'clear conversation',
|
|
24
|
+
quit: 'exit',
|
|
25
|
+
};
|
|
26
|
+
export function resolveSlash(input) {
|
|
27
|
+
const name = input.toLowerCase();
|
|
28
|
+
if (SLASH_COMMANDS.includes(name))
|
|
29
|
+
return name;
|
|
30
|
+
if (ALIASES[name])
|
|
31
|
+
return ALIASES[name];
|
|
32
|
+
const prefixed = SLASH_COMMANDS.filter(c => c.startsWith(name));
|
|
33
|
+
if (prefixed.length === 1)
|
|
34
|
+
return prefixed[0];
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
export function getSuggestion(partial) {
|
|
38
|
+
if (!partial.startsWith('/') || partial.includes(' '))
|
|
39
|
+
return '';
|
|
40
|
+
const p = partial.slice(1).toLowerCase();
|
|
41
|
+
if (!p)
|
|
42
|
+
return '';
|
|
43
|
+
const match = SLASH_COMMANDS.find(c => c.startsWith(p) && c !== p);
|
|
44
|
+
return match ? `/${match}` : '';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Where a submitted line goes: slash commands are always commands; when a
|
|
48
|
+
* coding session is active, everything else routes to the session's stdin;
|
|
49
|
+
* otherwise it's a normal chat message.
|
|
50
|
+
*/
|
|
51
|
+
export function classifyInput(value, codingActive) {
|
|
52
|
+
if (value.trim().startsWith('/'))
|
|
53
|
+
return 'command';
|
|
54
|
+
return codingActive ? 'coding' : 'chat';
|
|
55
|
+
}
|
|
56
|
+
// ── /code target selection ──────────────────────────────────────
|
|
57
|
+
/** Runner states that can accept a coding dispatch. */
|
|
58
|
+
const DISPATCHABLE_STATES = new Set(['online', 'busy']);
|
|
59
|
+
/**
|
|
60
|
+
* Expand online runners x detected coding CLIs into a flat choice list for
|
|
61
|
+
* the selector. Offline/stale runners and runners without any detected
|
|
62
|
+
* coding CLI are excluded.
|
|
63
|
+
*/
|
|
64
|
+
export function buildCodeChoices(runners) {
|
|
65
|
+
const choices = [];
|
|
66
|
+
for (const runner of runners) {
|
|
67
|
+
if (!DISPATCHABLE_STATES.has(runner.state ?? ''))
|
|
68
|
+
continue;
|
|
69
|
+
for (const agent of runner.codingAgents ?? []) {
|
|
70
|
+
choices.push({
|
|
71
|
+
runnerId: runner.id,
|
|
72
|
+
runnerName: runner.name,
|
|
73
|
+
agentId: agent.id,
|
|
74
|
+
agentName: agent.displayName || agent.id,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return choices;
|
|
79
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { AgentInfo } from '@almyty/client';
|
|
3
|
+
export interface Message {
|
|
4
|
+
role: 'user' | 'agent' | 'error' | 'info' | 'tool' | 'coding';
|
|
5
|
+
text: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function MarkdownText({ children, prefix, prefixColor }: {
|
|
8
|
+
children: string;
|
|
9
|
+
prefix?: string;
|
|
10
|
+
prefixColor?: string;
|
|
11
|
+
}): React.JSX.Element;
|
|
12
|
+
export declare function InlineFormat({ text }: {
|
|
13
|
+
text: string;
|
|
14
|
+
}): React.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
|
+
}): React.JSX.Element;
|
|
24
|
+
export declare function Header({ agent, conversationId }: {
|
|
25
|
+
agent: AgentInfo;
|
|
26
|
+
conversationId?: string | null;
|
|
27
|
+
}): React.JSX.Element;
|
|
28
|
+
export declare function MessageView({ msg }: {
|
|
29
|
+
msg: Message;
|
|
30
|
+
}): React.JSX.Element | null;
|
|
31
|
+
export declare function LoadingIndicator({ label }: {
|
|
32
|
+
label: string;
|
|
33
|
+
}): React.JSX.Element;
|
|
34
|
+
export declare function AgentSelector({ agents, onSelect }: {
|
|
35
|
+
agents: AgentInfo[];
|
|
36
|
+
onSelect: (a: AgentInfo) => void;
|
|
37
|
+
}): React.JSX.Element;
|
|
38
|
+
/**
|
|
39
|
+
* Visible mode banner while a coding session is active: input routes to the
|
|
40
|
+
* session, /esc detaches, /code-stop kills it.
|
|
41
|
+
*/
|
|
42
|
+
export declare function CodingModeIndicator({ agent, runner }: {
|
|
43
|
+
agent: string;
|
|
44
|
+
runner: string;
|
|
45
|
+
}): React.JSX.Element;
|
|
46
|
+
export interface Choice {
|
|
47
|
+
key: string;
|
|
48
|
+
label: string;
|
|
49
|
+
hint?: string;
|
|
50
|
+
}
|
|
51
|
+
export declare function ChoiceSelector({ title, choices, onSelect }: {
|
|
52
|
+
title: string;
|
|
53
|
+
choices: Choice[];
|
|
54
|
+
onSelect: (c: Choice) => void;
|
|
55
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,183 @@
|
|
|
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
|
+
case 'coding':
|
|
141
|
+
// Coding-session output: distinct cyan gutter so streamed CLI output
|
|
142
|
+
// reads apart from agent/chat messages.
|
|
143
|
+
return (_jsx(Box, { paddingLeft: 2, flexDirection: "column", children: msg.text.replace(/\n+$/, '').split('\n').map((line, i) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: "#22d3ee", children: "\u258D " }), _jsx(Text, { dimColor: true, children: line || ' ' })] }, i))) }));
|
|
144
|
+
default:
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export function LoadingIndicator({ label }) {
|
|
149
|
+
return (_jsxs(Box, { marginTop: 1, paddingLeft: 2, children: [_jsx(Text, { color: "#8b5cf6", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { dimColor: true, children: [" ", label] })] }));
|
|
150
|
+
}
|
|
151
|
+
// ── Agent selector ──────────────────────────────────────────────
|
|
152
|
+
export function AgentSelector({ agents, onSelect }) {
|
|
153
|
+
const [cursor, setCursor] = useState(0);
|
|
154
|
+
useInput((input, key) => {
|
|
155
|
+
if (key.upArrow)
|
|
156
|
+
setCursor(c => (c - 1 + agents.length) % agents.length);
|
|
157
|
+
if (key.downArrow)
|
|
158
|
+
setCursor(c => (c + 1) % agents.length);
|
|
159
|
+
if (key.return)
|
|
160
|
+
onSelect(agents[cursor]);
|
|
161
|
+
});
|
|
162
|
+
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 }) }))] }));
|
|
163
|
+
}
|
|
164
|
+
// ── Coding mode ─────────────────────────────────────────────────
|
|
165
|
+
/**
|
|
166
|
+
* Visible mode banner while a coding session is active: input routes to the
|
|
167
|
+
* session, /esc detaches, /code-stop kills it.
|
|
168
|
+
*/
|
|
169
|
+
export function CodingModeIndicator({ agent, runner }) {
|
|
170
|
+
return (_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { backgroundColor: "#0891B2", color: "black", children: " coding " }), _jsxs(Text, { color: "#22d3ee", children: [" ", agent, "@", runner] }), _jsx(Text, { dimColor: true, children: " input goes to the session \u00B7 /esc detach \u00B7 /code-stop stop" })] }));
|
|
171
|
+
}
|
|
172
|
+
export function ChoiceSelector({ title, choices, onSelect }) {
|
|
173
|
+
const [cursor, setCursor] = useState(0);
|
|
174
|
+
useInput((input, key) => {
|
|
175
|
+
if (key.upArrow)
|
|
176
|
+
setCursor(c => (c - 1 + choices.length) % choices.length);
|
|
177
|
+
if (key.downArrow)
|
|
178
|
+
setCursor(c => (c + 1) % choices.length);
|
|
179
|
+
if (key.return)
|
|
180
|
+
onSelect(choices[cursor]);
|
|
181
|
+
});
|
|
182
|
+
return (_jsxs(Box, { flexDirection: "column", paddingTop: 1, children: [_jsxs(Text, { dimColor: true, children: [" ", title] }), _jsx(Text, { children: " " }), choices.map((choice, i) => (_jsxs(Box, { paddingLeft: 2, children: [_jsxs(Text, { color: i === cursor ? '#8b5cf6' : undefined, children: [i === cursor ? '❯' : ' ', ' '] }), _jsx(Text, { bold: i === cursor, children: choice.label }), choice.hint && _jsxs(Text, { dimColor: true, children: [" ", choice.hint] })] }, choice.key)))] }));
|
|
183
|
+
}
|
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";
|