@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,91 @@
1
+ import type { Command } from 'commander';
2
+ import { ApiClient } from '../core/client.js';
3
+ import { resolveCredentials } from '../core/credentials.js';
4
+ import { CliError, ExitCode, handleErrors } from '../core/errors.js';
5
+ import { emit, setJsonMode } from '../core/output.js';
6
+ import { waitForRun } from '../core/waiters.js';
7
+ import { commaSet, httpTimeoutSec, readGlobals } from './command-opts.js';
8
+
9
+ export function registerAgent(program: Command): void {
10
+ const agent = program.command('agent').description('Agent operations');
11
+
12
+ agent
13
+ .command('invoke <thread_id> <prompt>')
14
+ .description('Send a message, start the agent, optional wait/stream')
15
+ .option('--wait', 'Block until run completes', false)
16
+ .option('--stream', 'Stream run events', false)
17
+ .option('--cancel-on-interrupt', 'Cancel run on Ctrl-C (reserved)', false)
18
+ .option('--filter <types>', 'Comma-separated SSE event types')
19
+ .option('--model <name>', 'Model override')
20
+ .option('--timeout <sec>', 'Wait/stream timeout (0 = no limit)', '120')
21
+ .action(
22
+ handleErrors(async function agentInvokeAction(this: Command, threadId: string, prompt: string) {
23
+ const g = readGlobals(this);
24
+ const opts = this.opts() as {
25
+ wait?: boolean;
26
+ stream?: boolean;
27
+ filter?: string;
28
+ model?: string;
29
+ timeout?: string;
30
+ };
31
+ setJsonMode(Boolean(g.json));
32
+
33
+ if (opts.wait && opts.stream) {
34
+ throw new CliError('--wait and --stream are mutually exclusive', ExitCode.USAGE_ERROR);
35
+ }
36
+
37
+ const creds = resolveCredentials({
38
+ token: g.token,
39
+ endpoint: g.endpoint,
40
+ profile: g.profile ?? 'e2e',
41
+ real: g.real,
42
+ });
43
+ const client = new ApiClient(creds, {
44
+ debug: g.debug,
45
+ timeoutSec: httpTimeoutSec(g),
46
+ });
47
+
48
+ const addResult = await client.post('/api/message/add', {
49
+ thread_id: threadId,
50
+ content: prompt,
51
+ });
52
+
53
+ const startData: Record<string, string> = {};
54
+ if (opts.model) {
55
+ startData.model_name = opts.model;
56
+ }
57
+ const startResult = (await client.postForm(`/api/thread/${threadId}/agent/start`, startData)) as {
58
+ agent_run_id?: string;
59
+ };
60
+ const agentRunId = startResult.agent_run_id ?? '';
61
+
62
+ if (!opts.wait && !opts.stream) {
63
+ emit({
64
+ message: addResult,
65
+ agent_run_id: agentRunId,
66
+ status: 'started',
67
+ });
68
+ return;
69
+ }
70
+
71
+ const rawT = opts.timeout ?? '120';
72
+ const tNum = Number.parseFloat(rawT);
73
+ const effectiveTimeout = Number.isFinite(tNum) && tNum === 0 ? null : Number.isFinite(tNum) ? tNum : 120;
74
+
75
+ const result = await waitForRun({
76
+ endpoint: creds.endpoint,
77
+ token: creds.token,
78
+ agent_run_id: agentRunId,
79
+ timeout: effectiveTimeout ?? undefined,
80
+ stream_output: Boolean(opts.stream),
81
+ json_mode: Boolean(g.json),
82
+ debug: g.debug,
83
+ event_filter: commaSet(opts.filter),
84
+ });
85
+
86
+ if (opts.wait && !opts.stream) {
87
+ emit(result);
88
+ }
89
+ })
90
+ );
91
+ }
@@ -0,0 +1,245 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import type { Command } from 'commander';
3
+ import consola from 'consola';
4
+ import { ApiClient } from '../core/client.js';
5
+ import { CliError, ExitCode, handleErrors } from '../core/errors.js';
6
+ import { buildIndex, type OperationInfo } from '../core/openapi.js';
7
+ import { emit, setJsonMode } from '../core/output.js';
8
+ import { readGlobals } from './command-opts.js';
9
+
10
+ function parseExtraArgs(args: string[]): Record<string, string> {
11
+ const params: Record<string, string> = {};
12
+ let i = 0;
13
+ while (i < args.length) {
14
+ const arg = args[i];
15
+ if (arg === undefined) {
16
+ break;
17
+ }
18
+ if (arg.startsWith('--')) {
19
+ const key = arg.replace(/^-+/, '');
20
+ const next = args[i + 1];
21
+ if (next !== undefined && !next.startsWith('--')) {
22
+ params[key] = next;
23
+ i += 2;
24
+ } else {
25
+ params[key] = 'true';
26
+ i += 1;
27
+ }
28
+ } else {
29
+ i += 1;
30
+ }
31
+ }
32
+ return params;
33
+ }
34
+
35
+ function resolveBody(body: string | undefined, bodyStdin: boolean): unknown | undefined {
36
+ if (bodyStdin) {
37
+ const raw = readFileSync(0, 'utf8');
38
+ try {
39
+ return JSON.parse(raw) as unknown;
40
+ } catch (e) {
41
+ throw new CliError(`Invalid JSON from stdin: ${e}`, ExitCode.USAGE_ERROR);
42
+ }
43
+ }
44
+ if (body === undefined) {
45
+ return undefined;
46
+ }
47
+ if (body.startsWith('@')) {
48
+ const filePath = body.slice(1);
49
+ try {
50
+ const text = readFileSync(filePath, 'utf8');
51
+ return JSON.parse(text) as unknown;
52
+ } catch (e) {
53
+ if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
54
+ throw new CliError(`File not found: ${filePath}`, ExitCode.USAGE_ERROR);
55
+ }
56
+ throw new CliError(`Invalid JSON in ${filePath}: ${e}`, ExitCode.USAGE_ERROR);
57
+ }
58
+ }
59
+ try {
60
+ return JSON.parse(body) as unknown;
61
+ } catch (e) {
62
+ throw new CliError(`Invalid JSON body: ${e}`, ExitCode.USAGE_ERROR);
63
+ }
64
+ }
65
+
66
+ async function executeOperation(
67
+ client: ApiClient,
68
+ op: OperationInfo,
69
+ body: unknown | undefined,
70
+ extraParams: Record<string, string>
71
+ ): Promise<unknown> {
72
+ let path = op.path;
73
+ const method = op.method;
74
+ const pathParams = [...path.matchAll(/\{(\w+)\}/g)].map((m) => m[1] ?? '');
75
+ const queryParams = { ...extraParams };
76
+ for (const param of pathParams) {
77
+ if (param in queryParams) {
78
+ const v = queryParams[param];
79
+ delete queryParams[param];
80
+ path = path.replace(`{${param}}`, v ?? '');
81
+ } else {
82
+ throw new CliError(`Missing required path parameter: --${param}`, ExitCode.USAGE_ERROR);
83
+ }
84
+ }
85
+
86
+ if (method === 'get') {
87
+ const q = Object.keys(queryParams).length > 0 ? { ...queryParams } : undefined;
88
+ return client.get(path, q as Record<string, unknown> | undefined);
89
+ }
90
+ if (method === 'post') {
91
+ return client.post(path, body);
92
+ }
93
+ if (method === 'patch') {
94
+ return client.patch(path, body);
95
+ }
96
+ if (method === 'put') {
97
+ return client.put(path, body);
98
+ }
99
+ if (method === 'delete') {
100
+ return client.delete(path);
101
+ }
102
+ throw new CliError(`Unsupported HTTP method: ${method}`, ExitCode.USAGE_ERROR);
103
+ }
104
+
105
+ function tagHuman(data: unknown): void {
106
+ const d = data as { tag?: string; operations?: number };
107
+ consola.log(` \x1b[1m${d.tag ?? ''}\x1b[0m (${d.operations ?? 0} operations)`);
108
+ }
109
+
110
+ function opHuman(data: unknown): void {
111
+ const d = data as { method?: string; operation?: string; summary?: string };
112
+ const method = d.method ?? '';
113
+ const color =
114
+ method === 'GET'
115
+ ? '\x1b[32m'
116
+ : method === 'POST'
117
+ ? '\x1b[34m'
118
+ : method === 'PATCH'
119
+ ? '\x1b[33m'
120
+ : method === 'DELETE'
121
+ ? '\x1b[31m'
122
+ : '\x1b[37m';
123
+ const op = (d.operation ?? '').padEnd(30);
124
+ consola.log(` ${color}${method.padEnd(6)}\x1b[0m ${op} ${d.summary ?? ''}`);
125
+ }
126
+
127
+ export function registerApi(program: Command): void {
128
+ const api = program
129
+ .command('api')
130
+ .description('Auto-generated API commands from OpenAPI spec');
131
+
132
+ api
133
+ .command('list-tags')
134
+ .description('List all available API tags')
135
+ .option('--endpoint <url>', 'API base URL')
136
+ .action(
137
+ handleErrors(async function listTagsAction(this: Command) {
138
+ const g = readGlobals(this);
139
+ const o = this.opts() as { endpoint?: string };
140
+ setJsonMode(Boolean(g.json));
141
+ const index = await buildIndex({ endpoint: o.endpoint ?? g.endpoint });
142
+ for (const tag of index.tags) {
143
+ const ops = index.operationsForTag(tag);
144
+ emit({ tag, operations: ops.length }, tagHuman);
145
+ }
146
+ })
147
+ );
148
+
149
+ api
150
+ .command('list-ops <tag>')
151
+ .description('List operations for a tag')
152
+ .option('--endpoint <url>', 'API base URL')
153
+ .action(
154
+ handleErrors(async function listOpsAction(this: Command, tag: string) {
155
+ const g = readGlobals(this);
156
+ const o = this.opts() as { endpoint?: string };
157
+ setJsonMode(Boolean(g.json));
158
+ const index = await buildIndex({ endpoint: o.endpoint ?? g.endpoint });
159
+ const ops = index.operationsForTag(tag);
160
+ if (ops.length === 0) {
161
+ throw new CliError(
162
+ `Unknown tag '${tag}'. Available: ${index.tags.join(', ')}`,
163
+ ExitCode.USAGE_ERROR
164
+ );
165
+ }
166
+ for (const op of ops) {
167
+ emit(
168
+ { operation: op.operation_id, method: op.method.toUpperCase(), path: op.path, summary: op.summary },
169
+ opHuman
170
+ );
171
+ }
172
+ })
173
+ );
174
+
175
+ api
176
+ .command('api-call <tag> <operation>')
177
+ .description(
178
+ 'Execute an API operation (examples: mitsein api api-call threads list-threads --body \'{"limit":20}\')'
179
+ )
180
+ .allowUnknownOption(true)
181
+ .allowExcessArguments(true)
182
+ .option('--body <json>', 'JSON body: inline or @file.json')
183
+ .option('--body-stdin', 'Read JSON body from stdin', false)
184
+ .option('--endpoint <url>', 'API base URL')
185
+ .option('--token <token>', 'Bearer token')
186
+ .option('--timeout <sec>', 'HTTP timeout (0 = none)', '30')
187
+ .option('--debug', 'Print HTTP details', false)
188
+ .action(
189
+ handleErrors(async function apiCallAction(this: Command, tag: string, operation: string) {
190
+ const g = readGlobals(this);
191
+ const opts = this.opts() as {
192
+ body?: string;
193
+ bodyStdin?: boolean;
194
+ endpoint?: string;
195
+ token?: string;
196
+ timeout?: string;
197
+ debug?: boolean;
198
+ };
199
+ setJsonMode(Boolean(g.json));
200
+
201
+ const index = await buildIndex({ endpoint: opts.endpoint ?? g.endpoint });
202
+ const op = index.getOperation(operation);
203
+ if (op === undefined) {
204
+ const ops = index.operationsForTag(tag);
205
+ if (ops.length === 0) {
206
+ const available =
207
+ index.tags.length > 0
208
+ ? index.tags.join(', ')
209
+ : '(none — run `mitsein dev openapi --refresh`)';
210
+ throw new CliError(`Unknown tag '${tag}'. Available tags: ${available}`, ExitCode.USAGE_ERROR);
211
+ }
212
+ const available = ops.map((o) => o.operation_id).join(', ');
213
+ throw new CliError(
214
+ `Unknown operation '${operation}' in tag '${tag}'. Available: ${available}`,
215
+ ExitCode.USAGE_ERROR
216
+ );
217
+ }
218
+ if (op.tag !== tag) {
219
+ throw new CliError(
220
+ `Operation '${operation}' belongs to tag '${op.tag}', not '${tag}'.`,
221
+ ExitCode.USAGE_ERROR
222
+ );
223
+ }
224
+
225
+ const requestBody = resolveBody(opts.body, Boolean(opts.bodyStdin));
226
+ const extra = parseExtraArgs((this.args as string[]).slice(2));
227
+
228
+ const rawTimeout = opts.timeout ?? g.timeout ?? '30';
229
+ const tn = Number.parseFloat(rawTimeout);
230
+ const effectiveTimeout = Number.isFinite(tn) && tn === 0 ? undefined : Number.isFinite(tn) ? tn : 30;
231
+
232
+ const client = ApiClient.fromOptions({
233
+ token: opts.token ?? g.token,
234
+ endpoint: opts.endpoint ?? g.endpoint,
235
+ profile: g.profile ?? 'e2e',
236
+ real: g.real,
237
+ timeoutSec: effectiveTimeout,
238
+ debug: Boolean(opts.debug ?? g.debug),
239
+ });
240
+
241
+ const result = await executeOperation(client, op, requestBody, extra);
242
+ emit(result);
243
+ })
244
+ );
245
+ }
@@ -0,0 +1,126 @@
1
+ import { createServer } from 'node:http';
2
+ import { parse as parseQuery, type ParsedUrlQuery } from 'node:querystring';
3
+ import consola from 'consola';
4
+ import { CliError, ExitCode } from '../core/errors.js';
5
+ import { emit } from '../core/output.js';
6
+ import { LOCALHOST_PORT, openBrowserUrl, saveOAuthCredentials } from './auth-internal.js';
7
+
8
+ function firstQuery(q: ParsedUrlQuery, key: string): string | undefined {
9
+ const v = q[key];
10
+ if (Array.isArray(v)) {
11
+ return v[0];
12
+ }
13
+ return v;
14
+ }
15
+
16
+ export async function loginBrowserFlow(
17
+ endpoint: string,
18
+ provider: string,
19
+ profile: string,
20
+ jsonOutput: boolean
21
+ ): Promise<void> {
22
+ const received: Record<string, string | boolean> = {};
23
+
24
+ await new Promise<void>((resolve, reject) => {
25
+ let settled = false;
26
+ let timer: ReturnType<typeof setTimeout> | undefined;
27
+
28
+ const finish = (): void => {
29
+ if (settled) {
30
+ return;
31
+ }
32
+ settled = true;
33
+ if (timer !== undefined) {
34
+ clearTimeout(timer);
35
+ }
36
+ server.close(() => resolve());
37
+ };
38
+
39
+ const server = createServer((req, res) => {
40
+ const u = req.url ?? '';
41
+ let pathname = '';
42
+ try {
43
+ pathname = new URL(u, 'http://127.0.0.1').pathname;
44
+ } catch {
45
+ pathname = '';
46
+ }
47
+ if (pathname !== '/callback') {
48
+ res.writeHead(404);
49
+ res.end();
50
+ finish();
51
+ return;
52
+ }
53
+ const q = u.includes('?') ? parseQuery(u.split('?', 2)[1] ?? '') : {};
54
+ if (q.error) {
55
+ res.writeHead(200, { 'Content-Type': 'text/html' });
56
+ const msg = firstQuery(q, 'error_description') ?? firstQuery(q, 'error') ?? 'Unknown error';
57
+ res.end(`<html><body><h2>Login failed</h2><p>${String(msg)}</p></body></html>`);
58
+ received.error = String(msg);
59
+ finish();
60
+ return;
61
+ }
62
+ if (q.account) {
63
+ received.account = firstQuery(q, 'account') ?? '';
64
+ }
65
+ if (q.email) {
66
+ received.email = firstQuery(q, 'email') ?? '';
67
+ }
68
+ if (q.token) {
69
+ received.token = firstQuery(q, 'token') ?? '';
70
+ }
71
+ if (q.session_id) {
72
+ received.session_id = firstQuery(q, 'session_id') ?? '';
73
+ }
74
+ res.writeHead(200, { 'Content-Type': 'text/html' });
75
+ res.end(
76
+ '<html><body><h2>Login successful!</h2><p>You can close this window.</p></body></html>'
77
+ );
78
+ received.success = true;
79
+ finish();
80
+ });
81
+
82
+ server.once('error', (err) => {
83
+ if (!settled) {
84
+ settled = true;
85
+ if (timer !== undefined) {
86
+ clearTimeout(timer);
87
+ }
88
+ reject(err);
89
+ }
90
+ });
91
+
92
+ timer = setTimeout(() => finish(), 300_000);
93
+
94
+ server.listen(LOCALHOST_PORT, '127.0.0.1', () => {
95
+ const loginUrl = `${endpoint}/api/auth/cognito/login?provider=${encodeURIComponent(provider)}`;
96
+ if (!jsonOutput) {
97
+ consola.log('\nOpening browser for login...');
98
+ consola.log(`If browser does not open, visit: ${loginUrl}\n`);
99
+ }
100
+ openBrowserUrl(loginUrl);
101
+ if (!jsonOutput) {
102
+ consola.log('Waiting for browser login...');
103
+ }
104
+ });
105
+ });
106
+
107
+ if (received.error !== undefined) {
108
+ throw new CliError(`Login failed: ${String(received.error)}`, ExitCode.BUSINESS_ERROR);
109
+ }
110
+ if (received.success === true) {
111
+ const result = {
112
+ access_token: String(received.token ?? ''),
113
+ user_email: String(received.email ?? ''),
114
+ user_id: String(received.account ?? ''),
115
+ };
116
+ saveOAuthCredentials(profile, endpoint, result);
117
+ emit(
118
+ { status: 'logged_in', email: result.user_email, profile },
119
+ () => {
120
+ consola.success(`Logged in as ${result.user_email}`);
121
+ }
122
+ );
123
+ return;
124
+ }
125
+ throw new CliError('Login timed out. Try again.', ExitCode.TIMEOUT);
126
+ }
@@ -0,0 +1,97 @@
1
+ import consola from 'consola';
2
+ import { CliError, ExitCode } from '../core/errors.js';
3
+ import { emit } from '../core/output.js';
4
+ import { openBrowserUrl, saveOAuthCredentials } from './auth-internal.js';
5
+
6
+ async function sleep(ms: number): Promise<void> {
7
+ await new Promise((r) => setTimeout(r, ms));
8
+ }
9
+
10
+ export async function loginDeviceCodeFlow(
11
+ endpoint: string,
12
+ profile: string,
13
+ jsonOutput: boolean
14
+ ): Promise<void> {
15
+ const deviceRes = await fetch(`${endpoint}/api/auth/cli/device`, {
16
+ method: 'POST',
17
+ signal: AbortSignal.timeout(10_000),
18
+ });
19
+ if (!deviceRes.ok) {
20
+ throw new CliError(`Failed to create device code: HTTP ${deviceRes.status}`, ExitCode.HTTP_ERROR);
21
+ }
22
+ const data = (await deviceRes.json()) as {
23
+ device_code: string;
24
+ user_code: string;
25
+ verification_url: string;
26
+ expires_in: number;
27
+ interval?: number;
28
+ };
29
+ const {
30
+ device_code: deviceCodeVal,
31
+ user_code: userCode,
32
+ verification_url: verificationUrl,
33
+ expires_in: expiresIn,
34
+ } = data;
35
+ const interval = data.interval ?? 5;
36
+ const fullUrl = `${verificationUrl}?user_code=${encodeURIComponent(userCode)}`;
37
+
38
+ if (!jsonOutput) {
39
+ consola.log('\nMitsein CLI Login\n');
40
+ consola.log('Open this URL in your browser:');
41
+ consola.log(fullUrl);
42
+ consola.log(`Or go to ${verificationUrl} and enter code:`);
43
+ consola.log(userCode);
44
+ consola.log(`Waiting for authorization (expires in ${Math.floor(expiresIn / 60)} min)...`);
45
+ }
46
+
47
+ openBrowserUrl(fullUrl);
48
+
49
+ const deadline = Date.now() + expiresIn * 1000;
50
+ while (Date.now() < deadline) {
51
+ await sleep(interval * 1000);
52
+ try {
53
+ const pollResp = await fetch(`${endpoint}/api/auth/cli/token`, {
54
+ method: 'POST',
55
+ headers: { 'Content-Type': 'application/json' },
56
+ body: JSON.stringify({ device_code: deviceCodeVal, grant_type: 'device_code' }),
57
+ signal: AbortSignal.timeout(10_000),
58
+ });
59
+ const result = (await pollResp.json()) as {
60
+ status?: string;
61
+ user_email?: string;
62
+ access_token?: string;
63
+ user_id?: string;
64
+ expires_in?: number;
65
+ };
66
+ const status = result.status;
67
+
68
+ if (status === 'authorized') {
69
+ saveOAuthCredentials(profile, endpoint, result);
70
+ emit(
71
+ { status: 'logged_in', email: result.user_email ?? '', profile },
72
+ () => {
73
+ consola.success(`\nLogged in as ${result.user_email ?? ''}`);
74
+ }
75
+ );
76
+ return;
77
+ }
78
+ if (status === 'expired') {
79
+ throw new CliError(
80
+ 'Device code expired. Run `mitsein auth login` again.',
81
+ ExitCode.BUSINESS_ERROR
82
+ );
83
+ }
84
+ if (!jsonOutput) {
85
+ process.stderr.write('.');
86
+ }
87
+ } catch (e) {
88
+ if (e instanceof CliError) {
89
+ throw e;
90
+ }
91
+ if (!jsonOutput) {
92
+ consola.log(`\nPoll error: ${e}, retrying...`);
93
+ }
94
+ }
95
+ }
96
+ throw new CliError('Login timed out. Run `mitsein auth login` again.', ExitCode.TIMEOUT);
97
+ }
@@ -0,0 +1,49 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { chmodSync, mkdirSync, writeFileSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ import { ensureConfigDir, getOauthPath, DEFAULT_ENDPOINT } from '../core/config.js';
5
+
6
+ export const LOCALHOST_PORT = 19284;
7
+
8
+ export function saveOAuthCredentials(
9
+ profile: string,
10
+ endpoint: string,
11
+ result: {
12
+ access_token?: string;
13
+ user_email?: string;
14
+ user_id?: string;
15
+ expires_in?: number;
16
+ }
17
+ ): void {
18
+ ensureConfigDir();
19
+ const oauthPath = getOauthPath(profile);
20
+ mkdirSync(dirname(oauthPath), { recursive: true, mode: 0o700 });
21
+ const data = {
22
+ access_token: result.access_token ?? '',
23
+ user_email: result.user_email ?? '',
24
+ user_id: result.user_id ?? '',
25
+ endpoint,
26
+ created_at: Date.now() / 1000,
27
+ expires_in: result.expires_in ?? 86400,
28
+ };
29
+ writeFileSync(oauthPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
30
+ chmodSync(oauthPath, 0o600);
31
+ }
32
+
33
+ export function openBrowserUrl(url: string): void {
34
+ try {
35
+ if (process.platform === 'darwin') {
36
+ execFileSync('open', [url], { stdio: 'ignore' });
37
+ } else if (process.platform === 'win32') {
38
+ execFileSync('cmd', ['/c', 'start', '', url], { stdio: 'ignore' });
39
+ } else {
40
+ execFileSync('xdg-open', [url], { stdio: 'ignore' });
41
+ }
42
+ } catch {
43
+ /* ignore */
44
+ }
45
+ }
46
+
47
+ export function normalizeEndpoint(endpoint: string | undefined): string {
48
+ return (endpoint ?? process.env.MITSEIN_API_URL ?? DEFAULT_ENDPOINT).replace(/\/$/, '');
49
+ }