@almyty/chat 1.2.0 → 1.3.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 +163 -24
- package/dist/app.d.ts +3 -1
- package/dist/app.js +283 -177
- package/dist/args.d.ts +64 -0
- package/dist/args.js +207 -0
- package/dist/commands.d.ts +21 -1
- package/dist/commands.js +50 -4
- package/dist/components.d.ts +7 -1
- package/dist/components.js +18 -26
- package/dist/errors.d.ts +49 -0
- package/dist/errors.js +144 -0
- package/dist/exit-codes.d.ts +32 -0
- package/dist/exit-codes.js +65 -0
- package/dist/headless.d.ts +44 -0
- package/dist/headless.js +106 -0
- package/dist/history.d.ts +51 -0
- package/dist/history.js +146 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +112 -40
- package/dist/stream.d.ts +95 -0
- package/dist/stream.js +276 -0
- package/dist/turn.d.ts +53 -0
- package/dist/turn.js +147 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +27 -0
- package/dist/viewport.d.ts +61 -0
- package/dist/viewport.js +118 -0
- package/package.json +9 -5
package/dist/args.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command line for `almyty chat`.
|
|
3
|
+
*
|
|
4
|
+
* Parsing lives apart from the entry point so the surface can be tested
|
|
5
|
+
* without a terminal, and so `--help` is generated from the same table
|
|
6
|
+
* the parser reads. A flag that exists and is undocumented, or is
|
|
7
|
+
* documented and does not exist, is a bug either way.
|
|
8
|
+
*/
|
|
9
|
+
export interface ChatArgs {
|
|
10
|
+
help: boolean;
|
|
11
|
+
version: boolean;
|
|
12
|
+
/** `<org>/<agent-slug>`, a bare slug, or undefined for the picker. */
|
|
13
|
+
ref?: string;
|
|
14
|
+
resume?: string;
|
|
15
|
+
/** One-shot message. Answers and exits, no REPL. */
|
|
16
|
+
message?: string;
|
|
17
|
+
/** Read the message from stdin rather than a tty. */
|
|
18
|
+
stdin: boolean;
|
|
19
|
+
json: boolean;
|
|
20
|
+
/** False renders the answer once it is complete instead of token by token. */
|
|
21
|
+
stream: boolean;
|
|
22
|
+
/** Explicit colour choice; undefined means "decide from the terminal". */
|
|
23
|
+
color?: boolean;
|
|
24
|
+
maxSteps?: number;
|
|
25
|
+
maxCostCents?: number;
|
|
26
|
+
/** A parse problem worth exiting on, rather than guessing. */
|
|
27
|
+
error?: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function parseArgs(argv: string[]): ChatArgs;
|
|
30
|
+
/**
|
|
31
|
+
* The agent to open: the argument, else $ALMYTY_AGENT.
|
|
32
|
+
*
|
|
33
|
+
* The environment variable is how a compiled terminal app knows which
|
|
34
|
+
* agent it is, since the build compiles this client unchanged and the
|
|
35
|
+
* recipient should not have to type an agent reference.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveRef(args: ChatArgs, env?: Record<string, string | undefined>): string | undefined;
|
|
38
|
+
/** Split `<org>/<agent>` into its parts; a bare slug leaves org unset. */
|
|
39
|
+
export declare function splitRef(ref: string): {
|
|
40
|
+
orgSlug?: string;
|
|
41
|
+
agentSlug: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Whether to run without a terminal UI.
|
|
45
|
+
*
|
|
46
|
+
* A CLI that only works on a tty is half a CLI: piping in a question or
|
|
47
|
+
* out to jq has to work, and ink cannot draw to a pipe.
|
|
48
|
+
*/
|
|
49
|
+
export declare function isNonInteractive(args: ChatArgs, io?: {
|
|
50
|
+
stdinTty?: boolean;
|
|
51
|
+
stdoutTty?: boolean;
|
|
52
|
+
}): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Whether to emit colour.
|
|
55
|
+
*
|
|
56
|
+
* Honours the NO_COLOR convention and FORCE_COLOR, and never colours a
|
|
57
|
+
* pipe unless asked to.
|
|
58
|
+
*/
|
|
59
|
+
export declare function useColor(args: ChatArgs, env?: Record<string, string | undefined>, stdoutTty?: boolean): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* `--help`, built from the same command table the REPL resolves
|
|
62
|
+
* against, so a new slash command cannot go undocumented.
|
|
63
|
+
*/
|
|
64
|
+
export declare function helpText(version: string): string;
|
package/dist/args.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command line for `almyty chat`.
|
|
3
|
+
*
|
|
4
|
+
* Parsing lives apart from the entry point so the surface can be tested
|
|
5
|
+
* without a terminal, and so `--help` is generated from the same table
|
|
6
|
+
* the parser reads. A flag that exists and is undocumented, or is
|
|
7
|
+
* documented and does not exist, is a bug either way.
|
|
8
|
+
*/
|
|
9
|
+
import { SLASH_COMMANDS, COMMAND_DESCS } from './commands.js';
|
|
10
|
+
import { EXIT_CODE_HELP } from './exit-codes.js';
|
|
11
|
+
/** Flags that take no value. */
|
|
12
|
+
const BOOLEAN_FLAGS = new Set([
|
|
13
|
+
'--help', '-h', '--version', '-v', '--json', '--stdin',
|
|
14
|
+
'--no-stream', '--stream', '--no-color', '--color',
|
|
15
|
+
]);
|
|
16
|
+
/** Flags that consume the next argument. */
|
|
17
|
+
const VALUE_FLAGS = new Set(['--resume', '--message', '-m', '--max-steps', '--max-cost-cents']);
|
|
18
|
+
function positiveInt(raw, flag) {
|
|
19
|
+
const n = Number(raw);
|
|
20
|
+
if (!Number.isFinite(n) || n <= 0 || !Number.isInteger(n)) {
|
|
21
|
+
return { error: `${flag} needs a positive whole number, got "${raw}"` };
|
|
22
|
+
}
|
|
23
|
+
return { value: n };
|
|
24
|
+
}
|
|
25
|
+
export function parseArgs(argv) {
|
|
26
|
+
const args = { help: false, version: false, stdin: false, json: false, stream: true };
|
|
27
|
+
for (let i = 0; i < argv.length; i++) {
|
|
28
|
+
const arg = argv[i];
|
|
29
|
+
if (VALUE_FLAGS.has(arg)) {
|
|
30
|
+
const value = argv[i + 1];
|
|
31
|
+
if (value === undefined || value.startsWith('-')) {
|
|
32
|
+
return { ...args, error: `${arg} needs a value` };
|
|
33
|
+
}
|
|
34
|
+
i++;
|
|
35
|
+
switch (arg) {
|
|
36
|
+
case '--resume':
|
|
37
|
+
args.resume = value;
|
|
38
|
+
break;
|
|
39
|
+
case '--message':
|
|
40
|
+
case '-m':
|
|
41
|
+
args.message = value;
|
|
42
|
+
break;
|
|
43
|
+
case '--max-steps': {
|
|
44
|
+
const { value: n, error } = positiveInt(value, arg);
|
|
45
|
+
if (error)
|
|
46
|
+
return { ...args, error };
|
|
47
|
+
args.maxSteps = n;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
case '--max-cost-cents': {
|
|
51
|
+
const { value: n, error } = positiveInt(value, arg);
|
|
52
|
+
if (error)
|
|
53
|
+
return { ...args, error };
|
|
54
|
+
args.maxCostCents = n;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (BOOLEAN_FLAGS.has(arg)) {
|
|
61
|
+
switch (arg) {
|
|
62
|
+
case '--help':
|
|
63
|
+
case '-h':
|
|
64
|
+
args.help = true;
|
|
65
|
+
break;
|
|
66
|
+
case '--version':
|
|
67
|
+
case '-v':
|
|
68
|
+
args.version = true;
|
|
69
|
+
break;
|
|
70
|
+
case '--json':
|
|
71
|
+
args.json = true;
|
|
72
|
+
break;
|
|
73
|
+
case '--stdin':
|
|
74
|
+
args.stdin = true;
|
|
75
|
+
break;
|
|
76
|
+
case '--stream':
|
|
77
|
+
args.stream = true;
|
|
78
|
+
break;
|
|
79
|
+
case '--no-stream':
|
|
80
|
+
args.stream = false;
|
|
81
|
+
break;
|
|
82
|
+
case '--color':
|
|
83
|
+
args.color = true;
|
|
84
|
+
break;
|
|
85
|
+
case '--no-color':
|
|
86
|
+
args.color = false;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (arg.startsWith('-')) {
|
|
92
|
+
// Silently ignoring an unknown flag is how `--jsonl` spends an
|
|
93
|
+
// afternoon looking like a broken --json.
|
|
94
|
+
return { ...args, error: `Unknown option: ${arg}\nRun with --help to see what this accepts.` };
|
|
95
|
+
}
|
|
96
|
+
if (args.ref === undefined)
|
|
97
|
+
args.ref = arg;
|
|
98
|
+
else
|
|
99
|
+
return { ...args, error: `Unexpected argument: ${arg}. Pass a message with --message instead.` };
|
|
100
|
+
}
|
|
101
|
+
return args;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The agent to open: the argument, else $ALMYTY_AGENT.
|
|
105
|
+
*
|
|
106
|
+
* The environment variable is how a compiled terminal app knows which
|
|
107
|
+
* agent it is, since the build compiles this client unchanged and the
|
|
108
|
+
* recipient should not have to type an agent reference.
|
|
109
|
+
*/
|
|
110
|
+
export function resolveRef(args, env = process.env) {
|
|
111
|
+
return args.ref ?? (env.ALMYTY_AGENT || undefined);
|
|
112
|
+
}
|
|
113
|
+
/** Split `<org>/<agent>` into its parts; a bare slug leaves org unset. */
|
|
114
|
+
export function splitRef(ref) {
|
|
115
|
+
const slash = ref.indexOf('/');
|
|
116
|
+
if (slash === -1)
|
|
117
|
+
return { agentSlug: ref };
|
|
118
|
+
return { orgSlug: ref.slice(0, slash), agentSlug: ref.slice(slash + 1) };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Whether to run without a terminal UI.
|
|
122
|
+
*
|
|
123
|
+
* A CLI that only works on a tty is half a CLI: piping in a question or
|
|
124
|
+
* out to jq has to work, and ink cannot draw to a pipe.
|
|
125
|
+
*/
|
|
126
|
+
export function isNonInteractive(args, io = {}) {
|
|
127
|
+
if (args.message !== undefined || args.json || args.stdin)
|
|
128
|
+
return true;
|
|
129
|
+
return io.stdinTty === false || io.stdoutTty === false;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Whether to emit colour.
|
|
133
|
+
*
|
|
134
|
+
* Honours the NO_COLOR convention and FORCE_COLOR, and never colours a
|
|
135
|
+
* pipe unless asked to.
|
|
136
|
+
*/
|
|
137
|
+
export function useColor(args, env = process.env, stdoutTty = true) {
|
|
138
|
+
if (args.color !== undefined)
|
|
139
|
+
return args.color;
|
|
140
|
+
if (env.NO_COLOR !== undefined && env.NO_COLOR !== '')
|
|
141
|
+
return false;
|
|
142
|
+
if (env.FORCE_COLOR !== undefined && env.FORCE_COLOR !== '0')
|
|
143
|
+
return true;
|
|
144
|
+
if (env.TERM === 'dumb')
|
|
145
|
+
return false;
|
|
146
|
+
return stdoutTty;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* `--help`, built from the same command table the REPL resolves
|
|
150
|
+
* against, so a new slash command cannot go undocumented.
|
|
151
|
+
*/
|
|
152
|
+
export function helpText(version) {
|
|
153
|
+
const commands = SLASH_COMMANDS.map((cmd) => ` /${cmd.padEnd(11)}${COMMAND_DESCS[cmd] ?? ''}`).join('\n');
|
|
154
|
+
return `almyty chat v${version} — an interactive REPL for your almyty agents.
|
|
155
|
+
|
|
156
|
+
Usage:
|
|
157
|
+
almyty chat [<org>/<agent-slug>] [options]
|
|
158
|
+
|
|
159
|
+
With no agent reference it lists the agents you can reach and asks.
|
|
160
|
+
A bare slug uses the organization on your credentials.
|
|
161
|
+
|
|
162
|
+
Options:
|
|
163
|
+
-m, --message <text> Ask one question, print the answer, exit.
|
|
164
|
+
--stdin Read the question from stdin (for pipes).
|
|
165
|
+
--resume <id> Continue a previous conversation.
|
|
166
|
+
--json One JSON object per answer. Implies non-interactive.
|
|
167
|
+
--no-stream Wait for the whole answer instead of streaming it.
|
|
168
|
+
--no-color Never colour the output. NO_COLOR is honoured too.
|
|
169
|
+
--max-steps <n> Autonomous runs: cap the number of steps.
|
|
170
|
+
--max-cost-cents <n> Autonomous runs: cap the spend, in cents.
|
|
171
|
+
-h, --help Show this.
|
|
172
|
+
-v, --version Print the version.
|
|
173
|
+
|
|
174
|
+
Slash commands, inside the REPL:
|
|
175
|
+
${commands}
|
|
176
|
+
|
|
177
|
+
Commands take unique prefixes and aliases: /q for /quit, /sw for /agents.
|
|
178
|
+
Tab completes, up and down walk your input history.
|
|
179
|
+
|
|
180
|
+
Keys:
|
|
181
|
+
Ctrl-C Cancel the running answer (server-side too). Again to exit.
|
|
182
|
+
Ctrl-D Exit.
|
|
183
|
+
Enter Send. End a line with \\ to keep typing on the next one.
|
|
184
|
+
|
|
185
|
+
Non-interactive:
|
|
186
|
+
almyty chat acme/support-bot -m "what is our refund window?"
|
|
187
|
+
echo "summarise today's errors" | almyty chat acme/ops --stdin
|
|
188
|
+
almyty chat acme/ops -m "check the deploy" --json | jq .output
|
|
189
|
+
|
|
190
|
+
The answer goes to stdout and the attribution to stderr, so a pipe
|
|
191
|
+
stays clean.
|
|
192
|
+
|
|
193
|
+
Exit codes:
|
|
194
|
+
${EXIT_CODE_HELP}
|
|
195
|
+
|
|
196
|
+
Environment:
|
|
197
|
+
ALMYTY_TOKEN Token override, instead of ~/.almyty/credentials.json.
|
|
198
|
+
ALMYTY_URL API URL override.
|
|
199
|
+
ALMYTY_AGENT Default agent reference, used when none is given.
|
|
200
|
+
ALMYTY_APP_URL Dashboard URL used in error messages.
|
|
201
|
+
ALMYTY_CHAT_HISTORY Input history file, instead of ~/.almyty/chat-history.
|
|
202
|
+
NO_COLOR Set to anything to disable colour.
|
|
203
|
+
|
|
204
|
+
Login:
|
|
205
|
+
npx @almyty/auth login
|
|
206
|
+
`;
|
|
207
|
+
}
|
package/dist/commands.d.ts
CHANGED
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
import type { RunnerSummary } from '@almyty/client';
|
|
2
|
-
export declare const SLASH_COMMANDS: readonly ["agents", "tools", "runners", "code", "code-stop", "esc", "help", "clear", "quit"];
|
|
2
|
+
export declare const SLASH_COMMANDS: readonly ["agents", "model", "tools", "cost", "trace", "resume", "new", "runners", "code", "code-stop", "esc", "help", "clear", "quit"];
|
|
3
3
|
export declare const ALIASES: Record<string, string>;
|
|
4
4
|
export declare const COMMAND_DESCS: Record<string, string>;
|
|
5
5
|
export declare function resolveSlash(input: string): string | null;
|
|
6
6
|
export declare function getSuggestion(partial: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Whether a submitted line asks to keep typing.
|
|
9
|
+
*
|
|
10
|
+
* A single-line prompt still has to accept a paragraph, so a trailing
|
|
11
|
+
* backslash continues onto the next line the way a shell does. Returns
|
|
12
|
+
* the text without the continuation marker, or null when the line is
|
|
13
|
+
* complete.
|
|
14
|
+
*/
|
|
15
|
+
export declare function continuationOf(value: string): string | null;
|
|
16
|
+
/**
|
|
17
|
+
* A pasted block, as one message.
|
|
18
|
+
*
|
|
19
|
+
* A multi-line paste used to submit on its first newline and hand the
|
|
20
|
+
* rest to the prompt a line at a time — so a pasted stack trace became
|
|
21
|
+
* a dozen messages, and any line of it starting with `/` ran as a
|
|
22
|
+
* command. Newlines inside a submitted value are content.
|
|
23
|
+
*/
|
|
24
|
+
export declare function joinSubmission(lines: string[]): string;
|
|
25
|
+
/** Whether a submission should be read as a slash command. */
|
|
26
|
+
export declare function isSlashCommand(value: string): boolean;
|
|
7
27
|
export type InputRoute = 'command' | 'coding' | 'chat';
|
|
8
28
|
/**
|
|
9
29
|
* Where a submitted line goes: slash commands are always commands; when a
|
package/dist/commands.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
// ── Slash command resolution ────────────────────────────────────
|
|
2
2
|
export const SLASH_COMMANDS = [
|
|
3
|
-
'agents', '
|
|
3
|
+
'agents', 'model', 'tools', 'cost', 'trace', 'resume', 'new',
|
|
4
|
+
'runners', 'code', 'code-stop', 'esc', 'help', 'clear', 'quit',
|
|
4
5
|
];
|
|
5
6
|
export const ALIASES = {
|
|
6
7
|
agent: 'agents', ag: 'agents', switch: 'agents', sw: 'agents',
|
|
7
8
|
tool: 'tools', t: 'tools',
|
|
8
|
-
|
|
9
|
+
usage: 'cost', spend: 'cost',
|
|
10
|
+
steps: 'trace', last: 'trace',
|
|
11
|
+
reset: 'new',
|
|
12
|
+
// `r` predates /resume and kept meaning runners; /res resolves resume.
|
|
13
|
+
runner: 'runners', r: 'runners',
|
|
9
14
|
stop: 'code-stop',
|
|
10
15
|
detach: 'esc',
|
|
11
16
|
h: 'help', '?': 'help',
|
|
@@ -14,13 +19,18 @@ export const ALIASES = {
|
|
|
14
19
|
};
|
|
15
20
|
export const COMMAND_DESCS = {
|
|
16
21
|
agents: 'browse and switch agents',
|
|
22
|
+
model: 'show the model and routing policy in use',
|
|
17
23
|
tools: 'show available tools',
|
|
24
|
+
cost: 'show tokens and spend for this session',
|
|
25
|
+
trace: "show the last run's steps",
|
|
26
|
+
resume: 'print the command that resumes this conversation',
|
|
27
|
+
new: 'start a fresh conversation with the same agent',
|
|
18
28
|
runners: 'list your runners + coding CLIs',
|
|
19
29
|
code: 'run a coding task on a runner',
|
|
20
30
|
'code-stop': 'stop the active coding session',
|
|
21
31
|
esc: 'leave coding mode (session keeps running)',
|
|
22
32
|
help: 'show commands',
|
|
23
|
-
clear: 'clear
|
|
33
|
+
clear: 'clear the transcript on screen',
|
|
24
34
|
quit: 'exit',
|
|
25
35
|
};
|
|
26
36
|
export function resolveSlash(input) {
|
|
@@ -43,13 +53,49 @@ export function getSuggestion(partial) {
|
|
|
43
53
|
const match = SLASH_COMMANDS.find(c => c.startsWith(p) && c !== p);
|
|
44
54
|
return match ? `/${match}` : '';
|
|
45
55
|
}
|
|
56
|
+
// ── Multi-line input ────────────────────────────────────────────
|
|
57
|
+
/**
|
|
58
|
+
* Whether a submitted line asks to keep typing.
|
|
59
|
+
*
|
|
60
|
+
* A single-line prompt still has to accept a paragraph, so a trailing
|
|
61
|
+
* backslash continues onto the next line the way a shell does. Returns
|
|
62
|
+
* the text without the continuation marker, or null when the line is
|
|
63
|
+
* complete.
|
|
64
|
+
*/
|
|
65
|
+
export function continuationOf(value) {
|
|
66
|
+
if (!/\\$/.test(value))
|
|
67
|
+
return null;
|
|
68
|
+
// An escaped backslash at the end is a literal one, not a hinge.
|
|
69
|
+
const trailing = value.length - value.replace(/\\+$/, '').length;
|
|
70
|
+
if (trailing % 2 === 0)
|
|
71
|
+
return null;
|
|
72
|
+
return value.slice(0, -1);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A pasted block, as one message.
|
|
76
|
+
*
|
|
77
|
+
* A multi-line paste used to submit on its first newline and hand the
|
|
78
|
+
* rest to the prompt a line at a time — so a pasted stack trace became
|
|
79
|
+
* a dozen messages, and any line of it starting with `/` ran as a
|
|
80
|
+
* command. Newlines inside a submitted value are content.
|
|
81
|
+
*/
|
|
82
|
+
export function joinSubmission(lines) {
|
|
83
|
+
return lines.join('\n').replace(/\s+$/, '');
|
|
84
|
+
}
|
|
85
|
+
/** Whether a submission should be read as a slash command. */
|
|
86
|
+
export function isSlashCommand(value) {
|
|
87
|
+
const trimmed = value.trim();
|
|
88
|
+
// Only a single-line submission can be a command: a pasted block that
|
|
89
|
+
// happens to begin with a slash is text.
|
|
90
|
+
return trimmed.startsWith('/') && !trimmed.includes('\n');
|
|
91
|
+
}
|
|
46
92
|
/**
|
|
47
93
|
* Where a submitted line goes: slash commands are always commands; when a
|
|
48
94
|
* coding session is active, everything else routes to the session's stdin;
|
|
49
95
|
* otherwise it's a normal chat message.
|
|
50
96
|
*/
|
|
51
97
|
export function classifyInput(value, codingActive) {
|
|
52
|
-
if (value
|
|
98
|
+
if (isSlashCommand(value))
|
|
53
99
|
return 'command';
|
|
54
100
|
return codingActive ? 'coding' : 'chat';
|
|
55
101
|
}
|
package/dist/components.d.ts
CHANGED
|
@@ -13,13 +13,19 @@ export declare function InlineFormat({ text }: {
|
|
|
13
13
|
text: string;
|
|
14
14
|
}): React.JSX.Element;
|
|
15
15
|
export declare function boldify(text: string): React.ReactElement;
|
|
16
|
+
/**
|
|
17
|
+
* Re-exported so callers do not have to know the arithmetic moved.
|
|
18
|
+
* Takes a message rather than text, which is how it has always read.
|
|
19
|
+
*/
|
|
16
20
|
export declare function estimateLines(msg: Message, cols: number): number;
|
|
17
|
-
export declare function MessageWindow({ messages, loading, loadingLabel, maxRows, scrollOffset }: {
|
|
21
|
+
export declare function MessageWindow({ messages, loading, loadingLabel, maxRows, scrollOffset, streaming }: {
|
|
18
22
|
messages: Message[];
|
|
19
23
|
loading: boolean;
|
|
20
24
|
loadingLabel: string;
|
|
21
25
|
maxRows: number;
|
|
22
26
|
scrollOffset?: number;
|
|
27
|
+
/** Assistant text arriving right now, drawn below the transcript. */
|
|
28
|
+
streaming?: string;
|
|
23
29
|
}): React.JSX.Element;
|
|
24
30
|
export declare function Header({ agent, conversationId }: {
|
|
25
31
|
agent: AgentInfo;
|
package/dist/components.js
CHANGED
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
4
|
import Spinner from 'ink-spinner';
|
|
5
|
+
import { columns, estimateLines as estimateMessageLines, selectWindow } from './viewport.js';
|
|
5
6
|
// ── Simple markdown rendering ───────────────────────────────────
|
|
6
7
|
export function MarkdownText({ children, prefix, prefixColor }) {
|
|
7
8
|
const lines = children.split('\n');
|
|
@@ -90,35 +91,26 @@ export function boldify(text) {
|
|
|
90
91
|
}
|
|
91
92
|
return _jsx(_Fragment, { children: parts });
|
|
92
93
|
}
|
|
93
|
-
// ── Message window (
|
|
94
|
+
// ── Message window (bounded to the terminal) ────────────────────
|
|
95
|
+
/**
|
|
96
|
+
* Re-exported so callers do not have to know the arithmetic moved.
|
|
97
|
+
* Takes a message rather than text, which is how it has always read.
|
|
98
|
+
*/
|
|
94
99
|
export function estimateLines(msg, cols) {
|
|
95
|
-
|
|
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;
|
|
100
|
+
return estimateMessageLines(msg.text, cols);
|
|
102
101
|
}
|
|
103
|
-
export function MessageWindow({ messages, loading, loadingLabel, maxRows, scrollOffset = 0 }) {
|
|
104
|
-
const cols = process.stdout.columns
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (usedRows + est > available && i < endIdx - 1)
|
|
114
|
-
break;
|
|
115
|
-
usedRows += est;
|
|
116
|
-
startIdx = i;
|
|
117
|
-
}
|
|
102
|
+
export function MessageWindow({ messages, loading, loadingLabel, maxRows, scrollOffset = 0, streaming }) {
|
|
103
|
+
const cols = columns(process.stdout.columns);
|
|
104
|
+
const streamRows = streaming ? estimateMessageLines(streaming, cols) : 0;
|
|
105
|
+
const { startIdx, endIdx, hiddenBefore, hiddenAfter } = selectWindow(messages, {
|
|
106
|
+
rows: maxRows,
|
|
107
|
+
cols,
|
|
108
|
+
// The spinner and the text still arriving both need room.
|
|
109
|
+
reservedRows: (loading && scrollOffset === 0 ? 2 : 0) + streamRows,
|
|
110
|
+
scrollOffset,
|
|
111
|
+
});
|
|
118
112
|
const visible = messages.slice(startIdx, endIdx);
|
|
119
|
-
|
|
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"] }) }))] }));
|
|
113
|
+
return (_jsxs(Box, { flexDirection: "column", children: [hiddenBefore > 0 && (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["\u2191 ", hiddenBefore, " earlier \u00B7 scroll to see more"] }) })), visible.map((msg, i) => (_jsx(MessageView, { msg: msg }, startIdx + i))), streaming && scrollOffset === 0 && _jsx(MessageView, { msg: { role: 'agent', text: streaming } }), loading && scrollOffset === 0 && _jsx(LoadingIndicator, { label: loadingLabel }), hiddenAfter > 0 && (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["\u2193 ", hiddenAfter, " newer \u00B7 scroll to see more"] }) }))] }));
|
|
122
114
|
}
|
|
123
115
|
// ── Components ──────────────────────────────────────────────────
|
|
124
116
|
export function Header({ agent, conversationId }) {
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One sentence a user can act on, for every way a chat session fails.
|
|
3
|
+
*
|
|
4
|
+
* A REPL that answers "API error 400: {"success":false,...}" has told the
|
|
5
|
+
* user nothing. Every failure a session can reach has a cause and a next
|
|
6
|
+
* step, and both are known here rather than left for the reader to infer
|
|
7
|
+
* from a status code.
|
|
8
|
+
*
|
|
9
|
+
* Kept free of ink and of the network client so it can be tested as the
|
|
10
|
+
* pure mapping it is.
|
|
11
|
+
*/
|
|
12
|
+
/** What the session knows about itself when something fails. */
|
|
13
|
+
export interface ErrorContext {
|
|
14
|
+
/** `<org>/<agent-slug>`, as the user typed it. */
|
|
15
|
+
agentRef?: string;
|
|
16
|
+
/** API base URL, for "cannot reach" messages. */
|
|
17
|
+
apiUrl?: string;
|
|
18
|
+
/** Dashboard URL, for "fix it here" messages. */
|
|
19
|
+
appUrl?: string;
|
|
20
|
+
/** What the session was doing: shapes the timeout wording. */
|
|
21
|
+
what?: 'run' | 'info' | 'history' | 'stream' | 'cancel';
|
|
22
|
+
}
|
|
23
|
+
export declare const DEFAULT_APP_URL = "https://app.almyty.com";
|
|
24
|
+
interface Failure {
|
|
25
|
+
status?: number;
|
|
26
|
+
code?: string;
|
|
27
|
+
serverMessage?: string;
|
|
28
|
+
message: string;
|
|
29
|
+
network: boolean;
|
|
30
|
+
aborted: boolean;
|
|
31
|
+
pollTimeout: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** Pull everything useful off a thrown value, whatever shape it has. */
|
|
34
|
+
export declare function inspectError(err: unknown): Failure;
|
|
35
|
+
/** The model name out of a MODEL_NOT_FOUND message, when it names one. */
|
|
36
|
+
export declare function extractModelName(message: string): string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Turn a thrown value into one sentence, plus the command or URL that
|
|
39
|
+
* fixes it.
|
|
40
|
+
*/
|
|
41
|
+
export declare function explainError(err: unknown, ctx?: ErrorContext): string;
|
|
42
|
+
/**
|
|
43
|
+
* Why `<org>/<agent>` could not be opened at startup.
|
|
44
|
+
*
|
|
45
|
+
* Startup is the one place a bad reference and a bad login look the
|
|
46
|
+
* same, and where "Agent not found" was printed for both.
|
|
47
|
+
*/
|
|
48
|
+
export declare function explainStartupFailure(err: unknown, ctx?: ErrorContext): string;
|
|
49
|
+
export {};
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One sentence a user can act on, for every way a chat session fails.
|
|
3
|
+
*
|
|
4
|
+
* A REPL that answers "API error 400: {"success":false,...}" has told the
|
|
5
|
+
* user nothing. Every failure a session can reach has a cause and a next
|
|
6
|
+
* step, and both are known here rather than left for the reader to infer
|
|
7
|
+
* from a status code.
|
|
8
|
+
*
|
|
9
|
+
* Kept free of ink and of the network client so it can be tested as the
|
|
10
|
+
* pure mapping it is.
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_APP_URL = 'https://app.almyty.com';
|
|
13
|
+
/** Pull everything useful off a thrown value, whatever shape it has. */
|
|
14
|
+
export function inspectError(err) {
|
|
15
|
+
const e = (err ?? {});
|
|
16
|
+
const message = typeof e.message === 'string' && e.message ? e.message : String(err ?? 'Unknown error');
|
|
17
|
+
let status = typeof e.status === 'number' ? e.status : undefined;
|
|
18
|
+
if (status === undefined) {
|
|
19
|
+
// Errors that crossed a package boundary as plain strings.
|
|
20
|
+
const m = message.match(/^(?:API error|SSE) (\d{3})/);
|
|
21
|
+
if (m)
|
|
22
|
+
status = Number(m[1]);
|
|
23
|
+
}
|
|
24
|
+
let code;
|
|
25
|
+
let serverMessage;
|
|
26
|
+
const body = typeof e.body === 'string' ? e.body : message;
|
|
27
|
+
const jsonStart = body.indexOf('{');
|
|
28
|
+
if (jsonStart !== -1) {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(body.slice(jsonStart));
|
|
31
|
+
if (parsed && typeof parsed === 'object') {
|
|
32
|
+
if (typeof parsed.error === 'string')
|
|
33
|
+
code = parsed.error;
|
|
34
|
+
if (typeof parsed.code === 'string')
|
|
35
|
+
code = parsed.code;
|
|
36
|
+
if (typeof parsed.message === 'string')
|
|
37
|
+
serverMessage = parsed.message;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
/* not JSON; the raw message is all there is */
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (!code && typeof e.code === 'string' && /^[A-Z_]+$/.test(e.code))
|
|
45
|
+
code = e.code;
|
|
46
|
+
const errno = typeof e.code === 'string' ? e.code : e.cause?.code;
|
|
47
|
+
const network = e.networkError === true ||
|
|
48
|
+
['ECONNREFUSED', 'ENOTFOUND', 'EAI_AGAIN', 'ECONNRESET', 'ETIMEDOUT', 'EPIPE', 'UND_ERR_SOCKET'].includes(errno) ||
|
|
49
|
+
/fetch failed|network|socket hang up|ECONNRESET/i.test(message);
|
|
50
|
+
return {
|
|
51
|
+
status,
|
|
52
|
+
code,
|
|
53
|
+
serverMessage,
|
|
54
|
+
message,
|
|
55
|
+
network,
|
|
56
|
+
aborted: e.name === 'AbortError' || e.code === 'ABORT_ERR',
|
|
57
|
+
pollTimeout: e.pollTimeout === true || /did not finish within/.test(message),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** The model name out of a MODEL_NOT_FOUND message, when it names one. */
|
|
61
|
+
export function extractModelName(message) {
|
|
62
|
+
const quoted = message.match(/"([^"]{2,80})"\s+is not available/) || message.match(/model\s+"([^"]{2,80})"/i);
|
|
63
|
+
return quoted ? quoted[1] : null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Turn a thrown value into one sentence, plus the command or URL that
|
|
67
|
+
* fixes it.
|
|
68
|
+
*/
|
|
69
|
+
export function explainError(err, ctx = {}) {
|
|
70
|
+
const f = inspectError(err);
|
|
71
|
+
const ref = ctx.agentRef ?? 'this agent';
|
|
72
|
+
const app = ctx.appUrl ?? DEFAULT_APP_URL;
|
|
73
|
+
if (f.aborted)
|
|
74
|
+
return 'Cancelled.';
|
|
75
|
+
if (f.network) {
|
|
76
|
+
const host = ctx.apiUrl ?? 'the almyty API';
|
|
77
|
+
return `Cannot reach ${host}. Check your connection, or ALMYTY_URL if you are pointing at your own deployment.`;
|
|
78
|
+
}
|
|
79
|
+
// Codes first: the server names the cause, so use its word for it.
|
|
80
|
+
switch (f.code) {
|
|
81
|
+
case 'AGENT_NOT_ACTIVE':
|
|
82
|
+
return `${ref} is not active, so it will not answer. Open ${app}/agents, activate it, and try again.`;
|
|
83
|
+
case 'AGENT_AUTH_REQUIRED':
|
|
84
|
+
case 'AGENT_AUTH_INVALID':
|
|
85
|
+
return 'Not authenticated. Run: npx @almyty/auth login';
|
|
86
|
+
case 'AGENT_AUTH_EXPIRED':
|
|
87
|
+
return 'Your credentials have expired. Run: npx @almyty/auth login';
|
|
88
|
+
case 'AGENT_AUTH_FORBIDDEN':
|
|
89
|
+
return `Your account has no access to the organization that owns ${ref}. Check the org in the reference, or log in as an account that does: npx @almyty/auth login`;
|
|
90
|
+
case 'MODEL_NOT_FOUND': {
|
|
91
|
+
const model = extractModelName(f.serverMessage ?? f.message);
|
|
92
|
+
return `${ref} is configured to use ${model ? `the model ${model}, which` : 'a model that'} its provider no longer serves. Pick a model that is available at ${app}/agents — scheduled runs stay paused until you do.`;
|
|
93
|
+
}
|
|
94
|
+
case 'BUDGET_EXCEEDED':
|
|
95
|
+
case 'SPEND_LIMIT_EXCEEDED':
|
|
96
|
+
return `This organization has reached its spend cap, so no new model calls will run. Raise it or wait for the period to reset at ${app}/settings.`;
|
|
97
|
+
}
|
|
98
|
+
const body = f.serverMessage ?? f.message;
|
|
99
|
+
if (/MODEL_NOT_FOUND|is not available/i.test(body)) {
|
|
100
|
+
const model = extractModelName(body);
|
|
101
|
+
return `${ref} is configured to use ${model ? `the model ${model}, which` : 'a model that'} its provider no longer serves. Pick a model that is available at ${app}/agents — scheduled runs stay paused until you do.`;
|
|
102
|
+
}
|
|
103
|
+
if (/no LLM provider configured|modelConfig\.providerId/i.test(body)) {
|
|
104
|
+
return `${ref} has no model configured. Open it at ${app}/agents and choose a model or a routing policy.`;
|
|
105
|
+
}
|
|
106
|
+
if (/not autonomous/i.test(body)) {
|
|
107
|
+
return `${ref} is a workflow agent, so it has no multi-turn run to continue. It answers one message at a time and that is what this session will do.`;
|
|
108
|
+
}
|
|
109
|
+
if (/budget|spend (cap|limit)|quota/i.test(body)) {
|
|
110
|
+
return `This organization has reached its spend cap, so no new model calls will run. Raise it or wait for the period to reset at ${app}/settings.`;
|
|
111
|
+
}
|
|
112
|
+
if (f.pollTimeout) {
|
|
113
|
+
return 'The run is taking longer than this session will wait. It is still going server-side — reconnect with --resume once it finishes, or stop it from the dashboard.';
|
|
114
|
+
}
|
|
115
|
+
switch (f.status) {
|
|
116
|
+
case 400:
|
|
117
|
+
return `${ref} rejected the request: ${body}`;
|
|
118
|
+
case 401:
|
|
119
|
+
return 'Not authenticated, or the stored token has expired. Run: npx @almyty/auth login';
|
|
120
|
+
case 403:
|
|
121
|
+
return `Your account has no access to ${ref}. Check the organization in the reference, or log in as an account that does: npx @almyty/auth login`;
|
|
122
|
+
case 404:
|
|
123
|
+
return `No agent at ${ref}. See what you can reach with: npx @almyty/agents list`;
|
|
124
|
+
case 409:
|
|
125
|
+
return `${ref} is busy with a conflicting request: ${body}`;
|
|
126
|
+
case 429:
|
|
127
|
+
return 'Rate limited. Wait a few seconds and send it again.';
|
|
128
|
+
case 500:
|
|
129
|
+
case 502:
|
|
130
|
+
case 503:
|
|
131
|
+
case 504:
|
|
132
|
+
return `almyty's API answered ${f.status}. This is server-side — retry in a moment, and check status if it keeps happening.`;
|
|
133
|
+
}
|
|
134
|
+
return body;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Why `<org>/<agent>` could not be opened at startup.
|
|
138
|
+
*
|
|
139
|
+
* Startup is the one place a bad reference and a bad login look the
|
|
140
|
+
* same, and where "Agent not found" was printed for both.
|
|
141
|
+
*/
|
|
142
|
+
export function explainStartupFailure(err, ctx = {}) {
|
|
143
|
+
return explainError(err, { ...ctx, what: 'info' });
|
|
144
|
+
}
|