mirai-cli 1.0.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.
Files changed (46) hide show
  1. package/bin/config.ts +99 -0
  2. package/bin/mirai.js +17 -0
  3. package/bin/mirai.ts +4 -0
  4. package/bin/provider.ts +149 -0
  5. package/bin/router.ts +134 -0
  6. package/dist/mirai.mjs +28316 -0
  7. package/package.json +29 -0
  8. package/src/app/index.tsx +274 -0
  9. package/src/components/chat.tsx +254 -0
  10. package/src/components/dialog/help-dialog.tsx +101 -0
  11. package/src/components/dialog/index.ts +3 -0
  12. package/src/components/dialog/provider.tsx +96 -0
  13. package/src/components/header/index.tsx +78 -0
  14. package/src/components/input/command-palette.tsx +129 -0
  15. package/src/components/input/commands.ts +46 -0
  16. package/src/components/input/index.tsx +284 -0
  17. package/src/components/matrix-rain/index.tsx +122 -0
  18. package/src/components/permission-modal.tsx +66 -0
  19. package/src/components/scroll-bar/index.tsx +56 -0
  20. package/src/components/status-bar/index.tsx +43 -0
  21. package/src/components/tool-result.tsx +11 -0
  22. package/src/hooks/use-chat.ts +208 -0
  23. package/src/hooks/use-mouse.tsx +121 -0
  24. package/src/hooks/use-permission.ts +35 -0
  25. package/src/hooks/use-runtime.ts +99 -0
  26. package/src/hooks/use-scroll-bar-drag.ts +115 -0
  27. package/src/hooks/use-scroll.ts +70 -0
  28. package/src/index.ts +39 -0
  29. package/src/renderers/builtins/BashResult.tsx +65 -0
  30. package/src/renderers/builtins/EditFileResult.tsx +69 -0
  31. package/src/renderers/builtins/GenericToolResult.tsx +39 -0
  32. package/src/renderers/builtins/GlobSearchResult.tsx +40 -0
  33. package/src/renderers/builtins/GrepSearchResult.tsx +49 -0
  34. package/src/renderers/builtins/ReadFileResult.tsx +54 -0
  35. package/src/renderers/builtins/WriteFileResult.tsx +24 -0
  36. package/src/renderers/constants.ts +7 -0
  37. package/src/renderers/register-builtins.ts +27 -0
  38. package/src/renderers/registry.ts +37 -0
  39. package/src/renderers/status.ts +22 -0
  40. package/src/renderers/utils.ts +70 -0
  41. package/src/services/hit-test.ts +49 -0
  42. package/src/services/mouse-input.ts +237 -0
  43. package/src/services/scroll-registry.ts +64 -0
  44. package/src/services/tui-permission-provider.ts +35 -0
  45. package/src/theme.ts +38 -0
  46. package/tsconfig.json +27 -0
@@ -0,0 +1,27 @@
1
+ import { registerRenderer } from './registry.js';
2
+
3
+ export async function registerBuiltinRenderers(): Promise<void> {
4
+ const [
5
+ { ReadFileResult },
6
+ { WriteFileResult },
7
+ { EditFileResult },
8
+ { GlobSearchResult },
9
+ { GrepSearchResult },
10
+ { BashResult },
11
+ ] = await Promise.all([
12
+ import('./builtins/ReadFileResult.js'),
13
+ import('./builtins/WriteFileResult.js'),
14
+ import('./builtins/EditFileResult.js'),
15
+ import('./builtins/GlobSearchResult.js'),
16
+ import('./builtins/GrepSearchResult.js'),
17
+ import('./builtins/BashResult.js'),
18
+ ]);
19
+
20
+ registerRenderer('read_file', ReadFileResult);
21
+ registerRenderer('write_file', WriteFileResult);
22
+ registerRenderer('edit_file', EditFileResult);
23
+ registerRenderer('glob_search', GlobSearchResult);
24
+ registerRenderer('grep_search', GrepSearchResult);
25
+ registerRenderer('bash', BashResult);
26
+ registerRenderer('shell', BashResult);
27
+ }
@@ -0,0 +1,37 @@
1
+ import type { ComponentType } from 'react';
2
+ import type { ToolResultBlock } from '@mirai/core/types';
3
+ import { GenericToolResult } from './builtins/GenericToolResult.js';
4
+
5
+ export type ToolRenderer = ComponentType<{ block: ToolResultBlock }>;
6
+
7
+ const rendererRegistry = new Map<string, ToolRenderer>();
8
+
9
+ export function registerRenderer(
10
+ toolName: string,
11
+ renderer: ToolRenderer
12
+ ): void {
13
+ if (rendererRegistry.has(toolName)) {
14
+ console.warn(
15
+ `[renderer] Overwriting existing renderer for "${toolName}"`
16
+ );
17
+ }
18
+ rendererRegistry.set(toolName, renderer);
19
+ }
20
+
21
+ export function getRenderer(
22
+ toolName: string
23
+ ): ToolRenderer | undefined {
24
+ return rendererRegistry.get(toolName);
25
+ }
26
+
27
+ export function resolveRenderer(
28
+ toolName: string
29
+ ): ToolRenderer {
30
+ return rendererRegistry.get(toolName) ?? GenericToolResult;
31
+ }
32
+
33
+ export function hasRenderer(
34
+ toolName: string
35
+ ): boolean {
36
+ return rendererRegistry.has(toolName);
37
+ }
@@ -0,0 +1,22 @@
1
+ import { theme } from "../theme.js";
2
+
3
+ export const STATUS_CONFIG = {
4
+ success: {
5
+ icon: '✓',
6
+ color: theme.success.primary,
7
+ },
8
+ error: {
9
+ icon: '✗',
10
+ color: theme.error.primary,
11
+ },
12
+ timeout: {
13
+ icon: '⏱',
14
+ color: theme.warning.primary,
15
+ },
16
+ cancelled: {
17
+ icon: '⊘',
18
+ color: theme.info.primary,
19
+ },
20
+ } as const;
21
+
22
+ export type ToolStatus = keyof typeof STATUS_CONFIG;
@@ -0,0 +1,70 @@
1
+ export interface TruncatedOutput {
2
+ content: string;
3
+ truncated: boolean;
4
+ }
5
+
6
+ export function truncateOutputSimple(
7
+ content: string,
8
+ maxLines: number,
9
+ maxChars: number
10
+ ): TruncatedOutput {
11
+ const original = content.trimEnd();
12
+ if (original.length === 0) {
13
+ return { content: '', truncated: false };
14
+ }
15
+
16
+ const lines = original.split('\n');
17
+ const previewLines: string[] = [];
18
+ let usedChars = 0;
19
+ let truncated = false;
20
+
21
+ for (let i = 0; i < lines.length; i++) {
22
+ if (i >= maxLines) {
23
+ truncated = true;
24
+ break;
25
+ }
26
+
27
+ const line = lines[i];
28
+ const newlineCost = i > 0 ? 1 : 0;
29
+ const available = maxChars - usedChars - newlineCost;
30
+
31
+ if (available <= 0) {
32
+ truncated = true;
33
+ break;
34
+ }
35
+
36
+ if (line.length > available) {
37
+ previewLines.push(line.slice(0, available) + '\u2026');
38
+ truncated = true;
39
+ break;
40
+ }
41
+
42
+ previewLines.push(line);
43
+ usedChars += newlineCost + line.length;
44
+ }
45
+
46
+ if (lines.length > maxLines || usedChars >= maxChars) {
47
+ truncated = true;
48
+ }
49
+
50
+ return {
51
+ content: previewLines.join('\n'),
52
+ truncated,
53
+ };
54
+ }
55
+
56
+ export function truncateForSummary(content: string, limit: number): string {
57
+ const trimmed = content.trim();
58
+ if (trimmed.length <= limit) {
59
+ return trimmed;
60
+ }
61
+ return trimmed.slice(0, limit) + '\u2026';
62
+ }
63
+
64
+ export function safeParseJson(content: string): unknown {
65
+ try {
66
+ return JSON.parse(content);
67
+ } catch {
68
+ return content;
69
+ }
70
+ }
@@ -0,0 +1,49 @@
1
+ export interface Rect {
2
+ left: number;
3
+ top: number;
4
+ width: number;
5
+ height: number;
6
+ }
7
+
8
+ export function inRect(x: number, y: number, rect: Rect): boolean {
9
+ return (
10
+ x >= rect.left &&
11
+ x < rect.left + rect.width &&
12
+ y >= rect.top &&
13
+ y < rect.top + rect.height
14
+ );
15
+ }
16
+
17
+ export interface HitTestEntry {
18
+ id: string;
19
+ zIndex: number;
20
+ order: number;
21
+ getRect: () => Rect | null;
22
+ }
23
+
24
+ export function hitTest(
25
+ x: number,
26
+ y: number,
27
+ entries: HitTestEntry[],
28
+ skipId?: string,
29
+ ): string | null {
30
+ let best: HitTestEntry | null = null;
31
+
32
+ for (let i = 0; i < entries.length; i++) {
33
+ const entry = entries[i];
34
+ if (entry.id === skipId) continue;
35
+
36
+ const rect = entry.getRect();
37
+ if (!rect || !inRect(x, y, rect)) continue;
38
+
39
+ if (
40
+ !best ||
41
+ entry.zIndex > best.zIndex ||
42
+ (entry.zIndex === best.zIndex && entry.order > best.order)
43
+ ) {
44
+ best = entry;
45
+ }
46
+ }
47
+
48
+ return best?.id ?? null;
49
+ }
@@ -0,0 +1,237 @@
1
+ import { PassThrough } from "stream";
2
+
3
+ // ─── State machine ──────────────────────────────────────────
4
+ // Only tracks the states needed for filtering SGR mouse sequences.
5
+ // GROUND → ESC → CSI is the only path we need to intercept.
6
+ const enum SeqState {
7
+ /** Normal text bytes, passed through immediately */
8
+ GROUND,
9
+ /** After receiving \x1b (ESC), waiting for next byte */
10
+ ESC,
11
+ /** After receiving \x1b[ (CSI prefix), accumulating params until final byte */
12
+ CSI,
13
+ }
14
+
15
+ // ─── SGR mouse regex (matches complete CSI sequences only) ──
16
+ const SGR_MOUSE_RE = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/;
17
+
18
+ // CSI final bytes are in range 0x40-0x7e
19
+ const isFinal = (b: number) => b >= 0x40 && b <= 0x7e;
20
+
21
+ export interface MouseState {
22
+ x: number;
23
+ y: number;
24
+ button: "left" | "middle" | "right" | "none";
25
+ isPressed: boolean;
26
+ wheelDelta: number;
27
+ }
28
+
29
+ type Listener = (state: MouseState) => void;
30
+
31
+ class MouseInputService {
32
+ private onExitHandler: (() => void) | null = null;
33
+ private pass = new PassThrough();
34
+ private listeners = new Set<Listener>();
35
+ private _enabled = false;
36
+ private _state: MouseState = { x: 0, y: 0, button: "none", isPressed: false, wheelDelta: 0 };
37
+
38
+ // ── Tokenizer state ──
39
+ private seqState = SeqState.GROUND;
40
+ /** Buffer for the current escape sequence bytes (ESC + subsequent) */
41
+ private escSeq: number[] = [];
42
+ /** Buffer for normal text bytes accumulated between escapes */
43
+ private textBuf: number[] = [];
44
+
45
+ // ── Watchdog ──
46
+ private watchdogTimer: ReturnType<typeof setTimeout> | null = null;
47
+ private readonly WATCHDOUT_MS = 50;
48
+
49
+ readonly stdin = this.pass;
50
+
51
+ get isTTY(): boolean {
52
+ return process.stdin.isTTY === true;
53
+ }
54
+
55
+ get state(): MouseState {
56
+ return this._state;
57
+ }
58
+
59
+ on(cb: Listener): () => void {
60
+ this.listeners.add(cb);
61
+ return () => this.listeners.delete(cb);
62
+ }
63
+
64
+ onExit(cb: () => void): void {
65
+ this.onExitHandler = cb;
66
+ }
67
+
68
+ enable() {
69
+ if (this._enabled || !process.stdin.isTTY) return;
70
+ this._enabled = true;
71
+
72
+ process.stdin.setRawMode?.(true);
73
+ process.stdin.on("readable", this.onReadable);
74
+ process.stdout.write("\x1b[?1002h\x1b[?1006h");
75
+ }
76
+
77
+ disable() {
78
+ if (!this._enabled) return;
79
+ this._enabled = false;
80
+
81
+ this.stopWatchdog();
82
+ process.stdin.off("readable", this.onReadable);
83
+ process.stdin.setRawMode?.(false);
84
+ process.stdout.write("\x1b[?1002l\x1b[?1006l");
85
+ }
86
+
87
+ // ── Readable handler ──
88
+ private onReadable = () => {
89
+ let chunk: Buffer;
90
+ while ((chunk = process.stdin.read()) !== null) {
91
+ for (let i = 0; i < chunk.length; i++) {
92
+ this.feedByte(chunk[i]!);
93
+ }
94
+ }
95
+ // Flush any text still in buffer so it reaches Ink promptly
96
+ this.flushText();
97
+ };
98
+
99
+ // ── Byte-at-a-time tokenizer ──
100
+ private feedByte(byte: number) {
101
+ if (byte === 0x03) {
102
+ this.disable();
103
+ this.onExitHandler?.();
104
+ return;
105
+ }
106
+
107
+ switch (this.seqState) {
108
+ // ──── GROUND ────
109
+ case SeqState.GROUND: {
110
+ if (byte === 0x1b) {
111
+ this.flushText();
112
+ this.seqState = SeqState.ESC;
113
+ this.escSeq = [byte];
114
+ this.startWatchdog();
115
+ } else {
116
+ this.textBuf.push(byte);
117
+ }
118
+ break;
119
+ }
120
+
121
+ // ──── ESC ────
122
+ case SeqState.ESC: {
123
+ this.escSeq.push(byte);
124
+ if (byte === 0x5b /* [ */) {
125
+ this.seqState = SeqState.CSI;
126
+ // Watchdog already armed from ESC
127
+ } else {
128
+ this.flushEscSeq();
129
+ this.seqState = SeqState.GROUND;
130
+ this.stopWatchdog();
131
+ }
132
+ break;
133
+ }
134
+
135
+ // ──── CSI ────
136
+ case SeqState.CSI: {
137
+ this.escSeq.push(byte);
138
+ // Restart watchdog on each byte so the timeout is measured from
139
+ // the most recent byte, not from when CSI was entered.
140
+ this.restartWatchdog();
141
+ if (isFinal(byte)) {
142
+ const seq = Buffer.from(this.escSeq);
143
+ const s = seq.toString("utf8");
144
+ const match = SGR_MOUSE_RE.exec(s);
145
+
146
+ if (match) {
147
+ const btn = parseInt(match[1]!, 10);
148
+ const x = parseInt(match[2]!, 10) - 1;
149
+ const y = parseInt(match[3]!, 10) - 1;
150
+ const isRelease = match[4] === "m";
151
+ const wheel = btn & 0b11000000;
152
+
153
+ if (wheel) {
154
+ if (isRelease) {
155
+ // skip wheel release events (no listener dispatch)
156
+ } else {
157
+ this._state = {
158
+ x,
159
+ y,
160
+ button: "none",
161
+ isPressed: false,
162
+ wheelDelta: btn === 64 ? 1 : -1,
163
+ };
164
+ for (const cb of this.listeners) cb(this._state);
165
+ }
166
+ } else {
167
+ const btnId = btn & 0b11;
168
+ this._state = {
169
+ x,
170
+ y,
171
+ button:
172
+ btnId === 0 ? "left" : btnId === 1 ? "middle" : "right",
173
+ isPressed: !isRelease,
174
+ wheelDelta: 0,
175
+ };
176
+ for (const cb of this.listeners) cb(this._state);
177
+ }
178
+ } else {
179
+ this.pass.write(seq);
180
+ }
181
+
182
+ this.escSeq = [];
183
+ this.seqState = SeqState.GROUND;
184
+ this.stopWatchdog();
185
+ }
186
+ break;
187
+ }
188
+ }
189
+ }
190
+
191
+ // ── Buffered writes ──
192
+ private flushText() {
193
+ if (this.textBuf.length > 0) {
194
+ this.pass.write(Buffer.from(this.textBuf));
195
+ this.textBuf = [];
196
+ }
197
+ }
198
+
199
+ private flushEscSeq() {
200
+ if (this.escSeq.length > 0) {
201
+ this.pass.write(Buffer.from(this.escSeq));
202
+ this.escSeq = [];
203
+ }
204
+ }
205
+
206
+ // ── Watchdog (ESCDELAY) ──
207
+ private startWatchdog() {
208
+ if (this.watchdogTimer) clearTimeout(this.watchdogTimer);
209
+ this.watchdogTimer = setTimeout(this.onWatchdog, this.WATCHDOUT_MS);
210
+ }
211
+
212
+ private restartWatchdog() {
213
+ if (this.watchdogTimer) clearTimeout(this.watchdogTimer);
214
+ this.watchdogTimer = setTimeout(this.onWatchdog, this.WATCHDOUT_MS);
215
+ }
216
+
217
+ private stopWatchdog() {
218
+ if (this.watchdogTimer) {
219
+ clearTimeout(this.watchdogTimer);
220
+ this.watchdogTimer = null;
221
+ }
222
+ }
223
+
224
+ /** Fires when stdin has been quiet for WATCHDOG_MS while in ESC or CSI.
225
+ * Flushes the incomplete sequence as raw bytes (ESCDELAY handling)
226
+ * so a lone \x1b (Escape key) reaches Ink as a keypress. */
227
+ private onWatchdog = () => {
228
+ this.watchdogTimer = null;
229
+
230
+ if (this.seqState === SeqState.ESC || this.seqState === SeqState.CSI) {
231
+ this.flushEscSeq();
232
+ this.seqState = SeqState.GROUND;
233
+ }
234
+ };
235
+ }
236
+
237
+ export const mouseInput = new MouseInputService();
@@ -0,0 +1,64 @@
1
+ import { hitTest, type Rect, type HitTestEntry } from "./hit-test.js";
2
+
3
+ type ScrollHandler = (delta: number) => boolean;
4
+
5
+ export interface Region {
6
+ id: string;
7
+ zIndex?: number;
8
+ getRect: () => Rect | null;
9
+ onScroll: ScrollHandler;
10
+ }
11
+
12
+ interface InternalRegion extends Region {
13
+ zIndex: number;
14
+ order: number;
15
+ }
16
+
17
+ class ScrollRegistryService {
18
+ private regions = new Map<string, InternalRegion>();
19
+ private nextOrder = 0;
20
+
21
+ register(region: Region): () => void {
22
+ const entry: InternalRegion = {
23
+ ...region,
24
+ zIndex: region.zIndex ?? 0,
25
+ order: this.nextOrder++,
26
+ };
27
+ this.regions.set(region.id, entry);
28
+ return () => this.regions.delete(region.id);
29
+ }
30
+
31
+ unregister(id: string): void {
32
+ this.regions.delete(id);
33
+ }
34
+
35
+ dispatch(x: number, y: number, delta: number): boolean {
36
+ const entries: HitTestEntry[] = [];
37
+ for (const region of this.regions.values()) {
38
+ entries.push({
39
+ id: region.id,
40
+ zIndex: region.zIndex,
41
+ order: region.order,
42
+ getRect: region.getRect,
43
+ });
44
+ }
45
+
46
+ let skipId: string | undefined;
47
+ let handled = false;
48
+
49
+ for (;;) {
50
+ const id = hitTest(x, y, entries, skipId);
51
+ if (!id) break;
52
+ handled = true;
53
+
54
+ const region = this.regions.get(id)!;
55
+ const shouldBubble = region.onScroll(delta);
56
+ if (!shouldBubble) break;
57
+ skipId = id;
58
+ }
59
+
60
+ return handled;
61
+ }
62
+ }
63
+
64
+ export const scrollRegistry = new ScrollRegistryService();
@@ -0,0 +1,35 @@
1
+ import type { PermissionProvider, PermissionRequest, PermissionResponse } from "@mirai/permission";
2
+
3
+ export type PermissionListener = (request: PermissionRequest | null) => void;
4
+
5
+ export class TuiPermissionProvider implements PermissionProvider {
6
+ private pendingResolve: ((response: PermissionResponse) => void) | null = null;
7
+ private currentRequest: PermissionRequest | null = null;
8
+ private listener: PermissionListener | null = null;
9
+
10
+ onRequest(listener: PermissionListener): void {
11
+ this.listener = listener;
12
+ }
13
+
14
+ async requestPermission(req: PermissionRequest): Promise<PermissionResponse> {
15
+ this.currentRequest = req;
16
+ this.listener?.(req);
17
+
18
+ return new Promise<PermissionResponse>((resolve) => {
19
+ this.pendingResolve = resolve;
20
+ });
21
+ }
22
+
23
+ resolve(response: PermissionResponse): void {
24
+ if (this.pendingResolve) {
25
+ this.pendingResolve(response);
26
+ this.pendingResolve = null;
27
+ this.currentRequest = null;
28
+ this.listener?.(null);
29
+ }
30
+ }
31
+
32
+ getCurrentRequest(): PermissionRequest | null {
33
+ return this.currentRequest;
34
+ }
35
+ }
package/src/theme.ts ADDED
@@ -0,0 +1,38 @@
1
+ /* ═══════════════════════════════════════
2
+ Mirai Theme — Neon Matrix color tokens
3
+ ═══════════════════════════════════════ */
4
+
5
+ import { NEON_COLORS } from "@mirai/core/constants";
6
+
7
+ const [g1, g2, g3, g4] = NEON_COLORS;
8
+ // g1=#39ff14 (bright), g2=#00ff41, g3=#00d936, g4=#a3ff12 (yellow-green)
9
+
10
+ export const theme = {
11
+ /** Surfaces & backgrounds */
12
+ bg: {
13
+ surface: "#1a1b26",
14
+ alt: "#16161e",
15
+ code: "#24283b",
16
+ overlay: "#000000",
17
+ },
18
+
19
+ /** Text hierarchy */
20
+ text: {
21
+ primary: "#c0caf5",
22
+ dim: "#565f89",
23
+ muted: "#3b4261",
24
+ },
25
+
26
+ /** Semantic role colors */
27
+ role: {
28
+ user: "#7aa2f7",
29
+ assistant: "#9ece6a",
30
+ thinking: "#bb9af7",
31
+ },
32
+
33
+ /** Semantic status colors */
34
+ success: { primary: "#9ece6a", dim: "#1e3b2a" },
35
+ error: { primary: "#f7768e", dim: "#3a1620" },
36
+ warning: { primary: "#e0af68", dim: "#3d2e1a" },
37
+ info: { primary: "#2ac3de", dim: "#143a42" },
38
+ } as const;
package/tsconfig.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "preserve",
5
+ "moduleResolution": "bundler",
6
+ "jsx": "react-jsx",
7
+ "strict": false,
8
+ "outDir": "dist",
9
+ "rootDir": "../..",
10
+ "baseUrl": ".",
11
+ "skipLibCheck": true,
12
+ "esModuleInterop": true,
13
+ "forceConsistentCasingInFileNames": true,
14
+ "resolveJsonModule": true,
15
+ "typeRoots": ["../../node_modules/@types"],
16
+ },
17
+ "include": [
18
+ "src",
19
+ "bin",
20
+ "../core",
21
+ "../protocol",
22
+ "../llm",
23
+ "../runtime",
24
+ "../tools",
25
+ ],
26
+ "exclude": ["node_modules", "dist"],
27
+ }