@mitsein-ai/cli 0.2.1 → 0.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 +75 -41
- package/dist/index.js +138 -140
- package/package.json +1 -1
- package/src/commands/agent.ts +90 -108
- package/src/commands/api-auto.ts +1 -1
- package/src/commands/dev.ts +1 -1
- package/src/commands/files.ts +1 -1
- package/src/commands/project.ts +6 -6
- package/src/commands/thread.ts +66 -143
- package/src/core/chat-output.ts +93 -0
- package/src/core/chat-run.ts +156 -0
- package/src/core/chat-stream.ts +188 -0
- package/src/core/client.ts +9 -2
- package/src/core/config.ts +1 -1
- package/src/core/credentials.ts +1 -1
- package/src/core/output.ts +14 -1
- package/src/index.ts +1 -3
- package/src/commands/messages.ts +0 -61
- package/src/core/sse.ts +0 -135
- package/src/core/waiters-output.ts +0 -95
- package/src/core/waiters.ts +0 -169
package/src/core/sse.ts
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import { CliError, ExitCode } from './errors.js';
|
|
2
|
-
|
|
3
|
-
export interface SseEvent {
|
|
4
|
-
event_type: string;
|
|
5
|
-
data: Record<string, unknown> | string;
|
|
6
|
-
raw: string;
|
|
7
|
-
timestamp: number;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function normalizeBase(endpoint: string): string {
|
|
11
|
-
return endpoint.replace(/\/$/, '');
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** Parse one SSE block (`\n\n` delimited). Returns `null` for heartbeats / empty. */
|
|
15
|
-
export function parseSseEvent(text: string, debug = false): SseEvent | null {
|
|
16
|
-
const lines = text.trim().split('\n');
|
|
17
|
-
const dataLines: string[] = [];
|
|
18
|
-
|
|
19
|
-
for (const line of lines) {
|
|
20
|
-
if (line.startsWith(':')) {
|
|
21
|
-
if (debug) {
|
|
22
|
-
process.stderr.write(`[debug] SSE comment: ${line}\n`);
|
|
23
|
-
}
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
if (line.startsWith('data: ')) {
|
|
27
|
-
dataLines.push(line.slice(6));
|
|
28
|
-
} else if (line.startsWith('data:')) {
|
|
29
|
-
dataLines.push(line.slice(5));
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (dataLines.length === 0) {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const rawData = dataLines.join('\n');
|
|
38
|
-
const timestamp = Date.now() / 1000;
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
const parsed = JSON.parse(rawData) as unknown;
|
|
42
|
-
const event_type =
|
|
43
|
-
typeof parsed === 'object' &&
|
|
44
|
-
parsed !== null &&
|
|
45
|
-
'type' in parsed &&
|
|
46
|
-
typeof (parsed as { type: unknown }).type === 'string'
|
|
47
|
-
? (parsed as { type: string }).type
|
|
48
|
-
: 'message';
|
|
49
|
-
return {
|
|
50
|
-
event_type,
|
|
51
|
-
data: parsed as Record<string, unknown>,
|
|
52
|
-
raw: rawData,
|
|
53
|
-
timestamp,
|
|
54
|
-
};
|
|
55
|
-
} catch {
|
|
56
|
-
return {
|
|
57
|
-
event_type: 'unknown',
|
|
58
|
-
data: rawData,
|
|
59
|
-
raw: rawData,
|
|
60
|
-
timestamp,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface StreamSseOptions {
|
|
66
|
-
endpoint: string;
|
|
67
|
-
path: string;
|
|
68
|
-
token: string;
|
|
69
|
-
timeoutSec?: number | null;
|
|
70
|
-
debug?: boolean;
|
|
71
|
-
signal?: AbortSignal | undefined;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** SSE client using `fetch` + `ReadableStream`. */
|
|
75
|
-
export async function* streamSse(options: StreamSseOptions): AsyncGenerator<SseEvent> {
|
|
76
|
-
const url = `${normalizeBase(options.endpoint)}${options.path.startsWith('/') ? options.path : `/${options.path}`}`;
|
|
77
|
-
if (options.debug) {
|
|
78
|
-
process.stderr.write(`[debug] SSE connecting to ${url}\n`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const start = Date.now();
|
|
82
|
-
const timeoutMs =
|
|
83
|
-
options.timeoutSec === undefined || options.timeoutSec === null || options.timeoutSec === 0
|
|
84
|
-
? null
|
|
85
|
-
: options.timeoutSec * 1000;
|
|
86
|
-
|
|
87
|
-
const res = await fetch(url, {
|
|
88
|
-
headers: {
|
|
89
|
-
Authorization: `Bearer ${options.token}`,
|
|
90
|
-
Accept: 'text/event-stream',
|
|
91
|
-
},
|
|
92
|
-
signal: options.signal,
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
if (!res.ok) {
|
|
96
|
-
throw new CliError(`SSE connection failed: HTTP ${res.status}`, ExitCode.HTTP_ERROR);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (options.debug) {
|
|
100
|
-
process.stderr.write(`[debug] SSE connected (${res.status})\n`);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const body = res.body;
|
|
104
|
-
if (!body) {
|
|
105
|
-
throw new CliError('SSE response has no body', ExitCode.HTTP_ERROR);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const reader = body.getReader();
|
|
109
|
-
const decoder = new TextDecoder();
|
|
110
|
-
let buffer = '';
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
for (;;) {
|
|
114
|
-
const { done, value } = await reader.read();
|
|
115
|
-
if (done) {
|
|
116
|
-
break;
|
|
117
|
-
}
|
|
118
|
-
buffer += decoder.decode(value, { stream: true });
|
|
119
|
-
let sep: number;
|
|
120
|
-
while ((sep = buffer.indexOf('\n\n')) !== -1) {
|
|
121
|
-
const block = buffer.slice(0, sep);
|
|
122
|
-
buffer = buffer.slice(sep + 2);
|
|
123
|
-
const ev = parseSseEvent(block, options.debug ?? false);
|
|
124
|
-
if (ev !== null) {
|
|
125
|
-
yield ev;
|
|
126
|
-
if (timeoutMs !== null && Date.now() - start > timeoutMs) {
|
|
127
|
-
throw new CliError('Stream timeout', ExitCode.TIMEOUT);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
} finally {
|
|
133
|
-
reader.releaseLock();
|
|
134
|
-
}
|
|
135
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { isTTY } from './output.js';
|
|
2
|
-
import type { SseEvent } from './sse.js';
|
|
3
|
-
|
|
4
|
-
function formatLocalTime(tsSec: number): string {
|
|
5
|
-
const d = new Date(tsSec * 1000);
|
|
6
|
-
const h = String(d.getHours()).padStart(2, '0');
|
|
7
|
-
const m = String(d.getMinutes()).padStart(2, '0');
|
|
8
|
-
const s = String(d.getSeconds()).padStart(2, '0');
|
|
9
|
-
return `${h}:${m}:${s}`;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function truncateStr(s: string, max: number): string {
|
|
13
|
-
return s.length > max ? `${s.slice(0, max)}...` : s;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** Human-readable line for one SSE event (`waiters._print_event_human`). */
|
|
17
|
-
export function printEventHuman(event: SseEvent): void {
|
|
18
|
-
const data = event.data;
|
|
19
|
-
|
|
20
|
-
if (typeof data !== 'object' || data === null) {
|
|
21
|
-
process.stdout.write(`\x1b[2m${String(data)}\x1b[0m\n`);
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const etype = event.event_type;
|
|
26
|
-
const content =
|
|
27
|
-
typeof data.content === 'string' ? data.content : '';
|
|
28
|
-
const ts = formatLocalTime(event.timestamp);
|
|
29
|
-
|
|
30
|
-
if (etype === 'assistant' || etype === 'text' || etype === 'message') {
|
|
31
|
-
process.stdout.write(
|
|
32
|
-
`\x1b[2m[${ts}]\x1b[0m \x1b[1massistant:\x1b[0m ${content}\n`
|
|
33
|
-
);
|
|
34
|
-
} else if (etype === 'tool_call') {
|
|
35
|
-
const name =
|
|
36
|
-
(typeof data.name === 'string' ? data.name : null) ??
|
|
37
|
-
(typeof data.tool_name === 'string' ? data.tool_name : null) ??
|
|
38
|
-
'?';
|
|
39
|
-
let args = data.arguments ?? data.args ?? '';
|
|
40
|
-
if (typeof args === 'string') {
|
|
41
|
-
args = truncateStr(args, 80);
|
|
42
|
-
} else {
|
|
43
|
-
args = JSON.stringify(args);
|
|
44
|
-
}
|
|
45
|
-
process.stdout.write(
|
|
46
|
-
`\x1b[2m[${ts}]\x1b[0m \x1b[33mtool_call:\x1b[0m ${name}(${String(args)})\n`
|
|
47
|
-
);
|
|
48
|
-
} else if (etype === 'tool_result') {
|
|
49
|
-
let output = data.output ?? data.result ?? data.content ?? '';
|
|
50
|
-
if (typeof output === 'string') {
|
|
51
|
-
output = truncateStr(output, 100);
|
|
52
|
-
} else {
|
|
53
|
-
output = JSON.stringify(output);
|
|
54
|
-
}
|
|
55
|
-
process.stdout.write(
|
|
56
|
-
`\x1b[2m[${ts}]\x1b[0m \x1b[32mtool_result:\x1b[0m ${String(output)}\n`
|
|
57
|
-
);
|
|
58
|
-
} else if (etype === 'status') {
|
|
59
|
-
const st = typeof data.status === 'string' ? data.status : '';
|
|
60
|
-
process.stdout.write(`\x1b[2m[${ts}]\x1b[0m \x1b[34mstatus:\x1b[0m ${st}\n`);
|
|
61
|
-
} else if (etype === 'error') {
|
|
62
|
-
process.stdout.write(
|
|
63
|
-
`\x1b[2m[${ts}]\x1b[0m \x1b[1m\x1b[31merror:\x1b[0m ${content}\n`
|
|
64
|
-
);
|
|
65
|
-
} else {
|
|
66
|
-
const snippet = JSON.stringify(data).slice(0, 120);
|
|
67
|
-
process.stdout.write(`\x1b[2m[${ts}]\x1b[0m \x1b[2m${etype}:\x1b[0m ${snippet}\n`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Completion summary after streaming (`waiters._print_summary`). */
|
|
72
|
-
export function printSummary(result: Record<string, unknown>): void {
|
|
73
|
-
const status = String(result.status ?? '');
|
|
74
|
-
const durationMs = Number(result.duration_ms ?? 0) / 1000;
|
|
75
|
-
const eventsCount = result.events_count;
|
|
76
|
-
|
|
77
|
-
if (status === 'succeeded') {
|
|
78
|
-
process.stdout.write(
|
|
79
|
-
`\n\x1b[1m\x1b[32m✓\x1b[0m run completed in ${durationMs.toFixed(1)}s (${eventsCount} events)\n`
|
|
80
|
-
);
|
|
81
|
-
} else if (status === 'failed') {
|
|
82
|
-
const err = String(result.error ?? '');
|
|
83
|
-
process.stdout.write(
|
|
84
|
-
`\n\x1b[1m\x1b[31m✗\x1b[0m run failed in ${durationMs.toFixed(1)}s: ${err}\n`
|
|
85
|
-
);
|
|
86
|
-
} else if (status === 'disconnected') {
|
|
87
|
-
process.stdout.write(
|
|
88
|
-
`\n\x1b[2mdisconnected after ${durationMs.toFixed(1)}s (${eventsCount} events)\x1b[0m\n`
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function shouldPrintStreamSummary(streamOutput: boolean, jsonMode: boolean): boolean {
|
|
94
|
-
return streamOutput && !jsonMode && isTTY();
|
|
95
|
-
}
|
package/src/core/waiters.ts
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import { CliError, ExitCode } from './errors.js';
|
|
2
|
-
import { isJsonMode } from './output.js';
|
|
3
|
-
import {
|
|
4
|
-
printEventHuman,
|
|
5
|
-
printSummary,
|
|
6
|
-
shouldPrintStreamSummary,
|
|
7
|
-
} from './waiters-output.js';
|
|
8
|
-
import { streamSse, type SseEvent } from './sse.js';
|
|
9
|
-
|
|
10
|
-
export interface WaitForRunOptions {
|
|
11
|
-
endpoint: string;
|
|
12
|
-
token: string;
|
|
13
|
-
agent_run_id: string;
|
|
14
|
-
timeout?: number | null;
|
|
15
|
-
stream_output?: boolean;
|
|
16
|
-
json_mode?: boolean;
|
|
17
|
-
debug?: boolean;
|
|
18
|
-
event_filter?: Set<string> | null;
|
|
19
|
-
on_event?: ((e: SseEvent) => void) | null;
|
|
20
|
-
/** Test hook: custom event stream instead of `streamSse`. */
|
|
21
|
-
eventStream?: AsyncIterable<SseEvent> | null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function outputEvent(
|
|
25
|
-
event: SseEvent,
|
|
26
|
-
jsonMode: boolean,
|
|
27
|
-
eventFilter: Set<string> | null | undefined
|
|
28
|
-
): void {
|
|
29
|
-
if (eventFilter && !eventFilter.has(event.event_type)) {
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
if (jsonMode) {
|
|
33
|
-
if (typeof event.data === 'object' && event.data !== null) {
|
|
34
|
-
const line = { ts: event.timestamp, ...event.data };
|
|
35
|
-
process.stdout.write(`${JSON.stringify(line)}\n`);
|
|
36
|
-
} else {
|
|
37
|
-
process.stdout.write(
|
|
38
|
-
`${JSON.stringify({ ts: event.timestamp, type: event.event_type, raw: event.data })}\n`
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
} else {
|
|
42
|
-
printEventHuman(event);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Wait for an agent run via SSE (`waiters.wait_for_run`).
|
|
48
|
-
* Exit semantics via `CliError`: failed run → BUSINESS_ERROR; timeout from stream → TIMEOUT.
|
|
49
|
-
*/
|
|
50
|
-
export async function waitForRun(options: WaitForRunOptions): Promise<Record<string, unknown>> {
|
|
51
|
-
const start = Date.now();
|
|
52
|
-
const events: { type: string; data: unknown }[] = [];
|
|
53
|
-
let finalStatus = 'unknown';
|
|
54
|
-
let errorMessage: string | null = null;
|
|
55
|
-
const assistantTextParts: string[] = [];
|
|
56
|
-
const toolCalls: Record<string, unknown>[] = [];
|
|
57
|
-
|
|
58
|
-
const streamOutput = options.stream_output ?? false;
|
|
59
|
-
const jsonMode = options.json_mode ?? isJsonMode();
|
|
60
|
-
const eventFilter = options.event_filter ?? null;
|
|
61
|
-
const debug = options.debug ?? false;
|
|
62
|
-
|
|
63
|
-
const useBuiltinStream = options.eventStream == null;
|
|
64
|
-
const ac = useBuiltinStream ? new AbortController() : null;
|
|
65
|
-
const onSigint = (): void => {
|
|
66
|
-
ac?.abort();
|
|
67
|
-
};
|
|
68
|
-
if (useBuiltinStream) {
|
|
69
|
-
process.once('SIGINT', onSigint);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const path = `/api/agent-run/${options.agent_run_id}/stream`;
|
|
73
|
-
const iterable: AsyncIterable<SseEvent> =
|
|
74
|
-
options.eventStream ??
|
|
75
|
-
streamSse({
|
|
76
|
-
endpoint: options.endpoint,
|
|
77
|
-
path,
|
|
78
|
-
token: options.token,
|
|
79
|
-
timeoutSec: options.timeout ?? undefined,
|
|
80
|
-
debug,
|
|
81
|
-
signal: ac?.signal,
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
try {
|
|
86
|
-
for await (const event of iterable) {
|
|
87
|
-
events.push({ type: event.event_type, data: event.data });
|
|
88
|
-
options.on_event?.(event);
|
|
89
|
-
if (streamOutput) {
|
|
90
|
-
outputEvent(event, jsonMode, eventFilter);
|
|
91
|
-
}
|
|
92
|
-
if (typeof event.data === 'object' && event.data !== null) {
|
|
93
|
-
const d = event.data as Record<string, unknown>;
|
|
94
|
-
const etype = event.event_type;
|
|
95
|
-
if (etype === 'status') {
|
|
96
|
-
const st = typeof d.status === 'string' ? d.status : '';
|
|
97
|
-
if (st === 'completed') {
|
|
98
|
-
finalStatus = 'succeeded';
|
|
99
|
-
break;
|
|
100
|
-
}
|
|
101
|
-
if (st === 'failed' || st === 'error') {
|
|
102
|
-
finalStatus = 'failed';
|
|
103
|
-
errorMessage =
|
|
104
|
-
(typeof d.content === 'string' ? d.content : null) ??
|
|
105
|
-
(typeof d.error === 'string' ? d.error : null) ??
|
|
106
|
-
null;
|
|
107
|
-
break;
|
|
108
|
-
}
|
|
109
|
-
} else if (etype === 'error') {
|
|
110
|
-
finalStatus = 'failed';
|
|
111
|
-
errorMessage =
|
|
112
|
-
(typeof d.content === 'string' ? d.content : null) ?? String(event.data);
|
|
113
|
-
break;
|
|
114
|
-
} else if (etype === 'assistant' || etype === 'text' || etype === 'message') {
|
|
115
|
-
const c = typeof d.content === 'string' ? d.content : '';
|
|
116
|
-
if (c) {
|
|
117
|
-
assistantTextParts.push(c);
|
|
118
|
-
}
|
|
119
|
-
} else if (etype === 'tool_call' || etype === 'tool_result') {
|
|
120
|
-
toolCalls.push(d);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
} catch (e) {
|
|
125
|
-
if (e !== null && typeof e === 'object' && (e as Error).name === 'AbortError') {
|
|
126
|
-
if (!jsonMode) {
|
|
127
|
-
process.stderr.write(
|
|
128
|
-
'\n\x1b[2mDisconnected from stream (run continues in background)\x1b[0m\n'
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
finalStatus = 'disconnected';
|
|
132
|
-
} else {
|
|
133
|
-
throw e;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
} finally {
|
|
137
|
-
if (useBuiltinStream) {
|
|
138
|
-
process.off('SIGINT', onSigint);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const duration_ms = Date.now() - start;
|
|
143
|
-
const result: Record<string, unknown> = {
|
|
144
|
-
status: finalStatus,
|
|
145
|
-
agent_run_id: options.agent_run_id,
|
|
146
|
-
duration_ms,
|
|
147
|
-
events_count: events.length,
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
if (assistantTextParts.length > 0) {
|
|
151
|
-
result.assistant_message = assistantTextParts.join('');
|
|
152
|
-
}
|
|
153
|
-
if (toolCalls.length > 0) {
|
|
154
|
-
result.tool_calls = toolCalls;
|
|
155
|
-
}
|
|
156
|
-
if (errorMessage) {
|
|
157
|
-
result.error = errorMessage;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (shouldPrintStreamSummary(streamOutput, jsonMode)) {
|
|
161
|
-
printSummary(result);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (finalStatus === 'failed') {
|
|
165
|
-
throw new CliError(`Run failed: ${errorMessage ?? 'unknown error'}`, ExitCode.BUSINESS_ERROR, result);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return result;
|
|
169
|
-
}
|