@mitsein-ai/cli 0.1.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.
@@ -0,0 +1,182 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { ensureConfigDir, getOpenapiCachePath } from './config.js';
3
+ import { CliError, ExitCode } from './errors.js';
4
+
5
+ export type OpenApiSpec = Record<string, unknown>;
6
+
7
+ export interface OperationInfo {
8
+ operation_id: string;
9
+ tag: string;
10
+ method: string;
11
+ path: string;
12
+ summary: string;
13
+ parameters: Record<string, unknown>[];
14
+ request_body: Record<string, unknown> | null;
15
+ }
16
+
17
+ /** camelCase / PascalCase / snake_case / spaces → kebab-case (Python `_kebab`). */
18
+ export function kebab(s: string): string {
19
+ let x = s.replace(/([a-z0-9])([A-Z])/g, '$1-$2');
20
+ x = x.replace(/[_\s/]+/g, '-');
21
+ x = x.replace(/-+/g, '-');
22
+ x = x.replace(/^-+|-+$/g, '');
23
+ return x.toLowerCase();
24
+ }
25
+
26
+ export class OpenApiIndex {
27
+ private readonly _spec: OpenApiSpec;
28
+ private readonly _operations = new Map<string, OperationInfo[]>();
29
+ private readonly _byId = new Map<string, OperationInfo>();
30
+
31
+ constructor(spec: OpenApiSpec) {
32
+ this._spec = spec;
33
+ this.parse();
34
+ }
35
+
36
+ private parse(): void {
37
+ const paths = this._spec.paths;
38
+ if (paths === null || typeof paths !== 'object' || Array.isArray(paths)) {
39
+ return;
40
+ }
41
+ for (const [path, pathItem] of Object.entries(paths as Record<string, unknown>)) {
42
+ if (pathItem === null || typeof pathItem !== 'object' || Array.isArray(pathItem)) {
43
+ continue;
44
+ }
45
+ const pi = pathItem as Record<string, unknown>;
46
+ if ('$ref' in pi && Object.keys(pi).length === 1) {
47
+ continue;
48
+ }
49
+ for (const method of ['get', 'post', 'put', 'patch', 'delete'] as const) {
50
+ const op = pi[method];
51
+ if (op === null || typeof op !== 'object' || Array.isArray(op)) {
52
+ continue;
53
+ }
54
+ const o = op as Record<string, unknown>;
55
+ const opIdRaw = typeof o.operationId === 'string' ? o.operationId : '';
56
+ const tags = Array.isArray(o.tags) ? o.tags : ['untagged'];
57
+ const firstTag = typeof tags[0] === 'string' ? tags[0] : 'untagged';
58
+ const tag = kebab(firstTag);
59
+ const summary = typeof o.summary === 'string' ? o.summary : '';
60
+ const parameters = Array.isArray(o.parameters) ? (o.parameters as Record<string, unknown>[]) : [];
61
+ const requestBody =
62
+ o.requestBody !== null && typeof o.requestBody === 'object' && !Array.isArray(o.requestBody)
63
+ ? (o.requestBody as Record<string, unknown>)
64
+ : null;
65
+
66
+ const operation_id = opIdRaw
67
+ ? kebab(opIdRaw)
68
+ : `${method}-${kebab(path)}`;
69
+
70
+ const info: OperationInfo = {
71
+ operation_id,
72
+ tag,
73
+ method,
74
+ path,
75
+ summary,
76
+ parameters,
77
+ request_body: requestBody,
78
+ };
79
+
80
+ const list = this._operations.get(tag) ?? [];
81
+ list.push(info);
82
+ this._operations.set(tag, list);
83
+ this._byId.set(operation_id, info);
84
+ }
85
+ }
86
+ }
87
+
88
+ get tags(): string[] {
89
+ return [...this._operations.keys()].sort();
90
+ }
91
+
92
+ operationsForTag(tag: string): OperationInfo[] {
93
+ return this._operations.get(tag) ?? [];
94
+ }
95
+
96
+ getOperation(operationId: string): OperationInfo | undefined {
97
+ return this._byId.get(operationId);
98
+ }
99
+
100
+ get allOperations(): OperationInfo[] {
101
+ return [...this._byId.values()];
102
+ }
103
+ }
104
+
105
+ function loadFromFile(path: string): OpenApiSpec {
106
+ const text = readFileSync(path, 'utf8');
107
+ try {
108
+ return JSON.parse(text) as OpenApiSpec;
109
+ } catch {
110
+ const trimmed = text.trim();
111
+ if (trimmed.startsWith('openapi:') || trimmed.startsWith('swagger:')) {
112
+ throw new CliError(
113
+ `File ${path} appears to be YAML; install a YAML parser or use JSON.`,
114
+ ExitCode.USAGE_ERROR
115
+ );
116
+ }
117
+ throw new CliError(`Failed to parse ${path} as JSON`, ExitCode.USAGE_ERROR);
118
+ }
119
+ }
120
+
121
+ async function fetchAndCache(endpoint: string, cachePath: string): Promise<OpenApiSpec> {
122
+ const url = `${endpoint.replace(/\/$/, '')}/openapi.json`;
123
+ let res: Response;
124
+ try {
125
+ res = await fetch(url, { signal: AbortSignal.timeout(10_000) });
126
+ } catch (e) {
127
+ throw new CliError(`Failed to fetch OpenAPI spec from ${url}: ${e}`, ExitCode.HTTP_ERROR);
128
+ }
129
+ if (!res.ok) {
130
+ throw new CliError(
131
+ `Failed to fetch OpenAPI spec from ${url}: HTTP ${res.status}`,
132
+ ExitCode.HTTP_ERROR
133
+ );
134
+ }
135
+ const spec = (await res.json()) as OpenApiSpec;
136
+ try {
137
+ ensureConfigDir();
138
+ writeFileSync(cachePath, `${JSON.stringify(spec, null, 2)}\n`, 'utf8');
139
+ } catch {
140
+ /* non-fatal */
141
+ }
142
+ return spec;
143
+ }
144
+
145
+ export interface LoadOpenApiOptions {
146
+ endpoint?: string | null;
147
+ refresh?: boolean;
148
+ }
149
+
150
+ export async function loadOpenApiSpec(options: LoadOpenApiOptions = {}): Promise<OpenApiSpec> {
151
+ const envPath = process.env.MITSEIN_OPENAPI_PATH;
152
+ if (envPath) {
153
+ return loadFromFile(envPath);
154
+ }
155
+
156
+ const cachePath = getOpenapiCachePath();
157
+ if (!options.refresh && existsSync(cachePath)) {
158
+ try {
159
+ return loadFromFile(cachePath);
160
+ } catch {
161
+ /* fall through */
162
+ }
163
+ }
164
+
165
+ if (options.endpoint) {
166
+ return fetchAndCache(options.endpoint, cachePath);
167
+ }
168
+
169
+ if (existsSync(cachePath)) {
170
+ return loadFromFile(cachePath);
171
+ }
172
+
173
+ throw new CliError(
174
+ 'No OpenAPI spec available. Run `mitsein dev openapi --refresh` or set MITSEIN_OPENAPI_PATH.',
175
+ ExitCode.USAGE_ERROR
176
+ );
177
+ }
178
+
179
+ export async function buildIndex(options: LoadOpenApiOptions = {}): Promise<OpenApiIndex> {
180
+ const spec = await loadOpenApiSpec(options);
181
+ return new OpenApiIndex(spec);
182
+ }
@@ -0,0 +1,59 @@
1
+ let jsonMode = false;
2
+
3
+ export function setJsonMode(enabled: boolean): void {
4
+ jsonMode = enabled;
5
+ }
6
+
7
+ export function isJsonMode(): boolean {
8
+ return jsonMode;
9
+ }
10
+
11
+ export function isTTY(): boolean {
12
+ return process.stdout.isTTY === true;
13
+ }
14
+
15
+ export type HumanFormatter = (data: unknown) => void;
16
+
17
+ /**
18
+ * Emit output: JSON if `--json`, otherwise human-readable.
19
+ * Mirrors Python `emit()` (rich console → stdout lines here).
20
+ */
21
+ export function emit(data: unknown, humanFormatter?: HumanFormatter): void {
22
+ if (jsonMode) {
23
+ process.stdout.write(`${JSON.stringify(data, replacer)}\n`);
24
+ return;
25
+ }
26
+ if (humanFormatter) {
27
+ humanFormatter(data);
28
+ return;
29
+ }
30
+ if (data !== null && typeof data === 'object' && !Array.isArray(data)) {
31
+ printDictHuman(data as Record<string, unknown>);
32
+ return;
33
+ }
34
+ if (Array.isArray(data)) {
35
+ for (const item of data) {
36
+ if (item !== null && typeof item === 'object' && !Array.isArray(item)) {
37
+ printDictHuman(item as Record<string, unknown>);
38
+ } else {
39
+ process.stdout.write(`${String(item)}\n`);
40
+ }
41
+ process.stdout.write('\n');
42
+ }
43
+ return;
44
+ }
45
+ process.stdout.write(`${data === undefined ? '' : String(data)}\n`);
46
+ }
47
+
48
+ function replacer(_key: string, value: unknown): unknown {
49
+ if (typeof value === 'bigint') {
50
+ return value.toString();
51
+ }
52
+ return value;
53
+ }
54
+
55
+ function printDictHuman(d: Record<string, unknown>): void {
56
+ for (const [key, value] of Object.entries(d)) {
57
+ process.stdout.write(`${key}: ${String(value)}\n`);
58
+ }
59
+ }
@@ -0,0 +1,135 @@
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
+ }
@@ -0,0 +1,95 @@
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
+ }
@@ -0,0 +1,169 @@
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
+ }
package/src/index.ts ADDED
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env bun
2
+ import { Command } from 'commander';
3
+ import consola from 'consola';
4
+ import { registerAgent } from './commands/agent.js';
5
+ import { registerApi } from './commands/api-auto.js';
6
+ import { registerAuth } from './commands/auth.js';
7
+ import { registerDev } from './commands/dev.js';
8
+ import { registerMessages } from './commands/messages.js';
9
+ import { registerProject } from './commands/project.js';
10
+ import { registerRun } from './commands/run.js';
11
+ import { registerThread } from './commands/thread.js';
12
+ import { runVersion } from './commands/version.js';
13
+ import { CliError } from './core/errors.js';
14
+ import { setJsonMode } from './core/output.js';
15
+
16
+ interface GlobalOpts {
17
+ json?: boolean;
18
+ }
19
+
20
+ const program = new Command();
21
+
22
+ program
23
+ .name('mitsein')
24
+ .description('Mitsein CLI — internal dev tool for agent verification and workflow automation.')
25
+ .option('--endpoint <url>', 'API endpoint URL')
26
+ .option('--token <token>', 'Bearer token')
27
+ .option('--profile <name>', 'Profile name', 'e2e')
28
+ .option('--real', 'Use real account for dev token', false)
29
+ .option('--json', 'Output structured JSON', false)
30
+ .option('--debug', 'Print HTTP request/response details', false)
31
+ .option('--timeout <sec>', 'HTTP timeout in seconds (0 = no timeout)', '30')
32
+ .hook('preAction', (_thisCommand, actionCommand) => {
33
+ const cmd = actionCommand ?? _thisCommand;
34
+ const g = cmd.optsWithGlobals() as GlobalOpts;
35
+ setJsonMode(Boolean(g.json));
36
+ });
37
+
38
+ program
39
+ .command('version')
40
+ .description('Print CLI version')
41
+ .action(() => {
42
+ runVersion();
43
+ });
44
+
45
+ registerDev(program);
46
+ registerApi(program);
47
+ registerThread(program);
48
+ registerRun(program);
49
+ registerMessages(program);
50
+ registerAgent(program);
51
+ registerProject(program);
52
+ registerAuth(program);
53
+
54
+ try {
55
+ await program.parseAsync(process.argv, { from: 'node' });
56
+ } catch (e) {
57
+ if (e instanceof CliError) {
58
+ consola.error(`Error: ${e.message}`);
59
+ if (e.detail != null && e.detail !== '') {
60
+ consola.error(String(e.detail));
61
+ }
62
+ process.exit(e.code);
63
+ }
64
+ consola.error(e);
65
+ process.exit(1);
66
+ }