mirai-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.
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
package/bin/config.ts ADDED
@@ -0,0 +1,99 @@
1
+ /* ─── Config loader ───
2
+ * Reads ~/.mirai/config.yaml, merges with built-in PROVIDERS.
3
+ */
4
+
5
+ import { readFileSync, existsSync, mkdirSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { PROVIDERS, readBaseUrl, readApiKey } from "@mirai/llm";
9
+
10
+ /* ─── Types ─── */
11
+
12
+ export interface ProviderEntry {
13
+ /** Base URL for API calls */
14
+ baseURL?: string;
15
+ /** API key (optional — if unset, env {ID}_API_KEY is used, or no auth) */
16
+ apiKey?: string;
17
+ /** Default model for this provider (optional) */
18
+ default?: string;
19
+ /** Allowed models (optional — for display / autocomplete) */
20
+ models?: string[];
21
+ }
22
+
23
+ export interface MiraiConfig {
24
+ /** Default model in "providerID/modelID" format */
25
+ default?: string;
26
+ /** Custom providers (key = provider ID) */
27
+ providers: Record<string, ProviderEntry>;
28
+ }
29
+
30
+ /* ─── Load ─── */
31
+
32
+ const CONFIG_PATH = join(homedir(), ".mirai", "config.yaml");
33
+
34
+ export async function loadConfig(): Promise<MiraiConfig> {
35
+ // Start with empty config
36
+ const config: MiraiConfig = { providers: {} };
37
+
38
+ if (!existsSync(CONFIG_PATH)) {
39
+ return config;
40
+ }
41
+
42
+ try {
43
+ // Dynamic import js-yaml (ESM)
44
+ const { load: yamlLoad } = await import("js-yaml");
45
+ const raw = readFileSync(CONFIG_PATH, "utf-8");
46
+ const parsed = yamlLoad(raw) as Record<string, unknown>;
47
+
48
+ if (parsed?.default && typeof parsed.default === "string") {
49
+ config.default = parsed.default;
50
+ }
51
+
52
+ if (parsed?.providers && typeof parsed.providers === "object") {
53
+ for (const [id, entry] of Object.entries(parsed.providers)) {
54
+ const e = entry as Record<string, unknown>;
55
+ config.providers[id] = {
56
+ baseURL: typeof e.baseURL === "string" ? e.baseURL : undefined,
57
+ apiKey: typeof e.apiKey === "string" ? e.apiKey : undefined,
58
+ default: typeof e.default === "string" ? e.default : undefined,
59
+ models: Array.isArray(e.models) ? (e.models as string[]) : undefined,
60
+ };
61
+ }
62
+ }
63
+
64
+ return config;
65
+ } catch (err) {
66
+ console.error(`[mirai] Failed to load config from ${CONFIG_PATH}:`, (err as Error).message);
67
+ return config;
68
+ }
69
+ }
70
+
71
+ /* ─── Merge custom over built-in ─── */
72
+
73
+ export function getProviderConfig(id: string, config: MiraiConfig) {
74
+ // Custom provider from user config — overrides built-in
75
+ if (config.providers[id]) {
76
+ const entry = config.providers[id];
77
+ return {
78
+ name: id,
79
+ baseURL: entry.baseURL ?? "http://127.0.0.1:11434/v1",
80
+ apiKey: entry.apiKey ?? process.env[`${id.toUpperCase()}_API_KEY`],
81
+ supportsReasoning: false,
82
+ noAuth: !(entry.apiKey || process.env[`${id.toUpperCase()}_API_KEY`]),
83
+ };
84
+ }
85
+
86
+ // Built-in provider
87
+ const builtin = PROVIDERS[id];
88
+ if (builtin) {
89
+ return {
90
+ name: builtin.name,
91
+ baseURL: readBaseUrl(builtin),
92
+ apiKey: readApiKey(builtin) ?? undefined,
93
+ supportsReasoning: builtin.supportsReasoning ?? false,
94
+ noAuth: builtin.noAuth ?? false,
95
+ };
96
+ }
97
+
98
+ return null;
99
+ }
package/bin/mirai.js ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath } from "url";
3
+ import { dirname, join } from "path";
4
+ import { existsSync } from "fs";
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const dist = join(__dirname, "..", "dist", "mirai.mjs");
8
+ const src = join(__dirname, "mirai.ts");
9
+
10
+ if (existsSync(dist)) {
11
+ await import(dist);
12
+ } else {
13
+ const { spawn } = await import("child_process");
14
+ spawn("pnpm", ["exec", "tsx", src, ...process.argv.slice(2)], {
15
+ stdio: "inherit",
16
+ }).on("exit", (code) => process.exit(code ?? 0));
17
+ }
package/bin/mirai.ts ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env tsx
2
+ import { main } from "./router.js";
3
+
4
+ main();
@@ -0,0 +1,149 @@
1
+ /* ─── Provider detection (hybrid) ───
2
+ *
3
+ * Priority:
4
+ * 1. Model has "providerID/" prefix → route directly
5
+ * 2. OLLAMA_HOST env set → Ollama
6
+ * 3. OPENAI_BASE_URL env set → OpenAI custom
7
+ * 4. Model prefix detect (gpt-*, grok*, claude*, qwen-*)
8
+ * 5. API key env sniff
9
+ * 6. Config default provider
10
+ * 7. Fallback → Ollama local
11
+ */
12
+
13
+ import { PROVIDERS, readBaseUrl, readApiKey } from "@mirai/llm";
14
+ import type { MiraiConfig } from "./config.js";
15
+ import { getProviderConfig } from "./config.js";
16
+
17
+ export interface ProviderInfo {
18
+ id: string;
19
+ name: string;
20
+ baseURL: string;
21
+ apiKey?: string;
22
+ supportsReasoning: boolean;
23
+ noAuth: boolean;
24
+ model: string;
25
+ }
26
+
27
+ /* ─── Model prefix → built-in provider map ─── */
28
+
29
+ const PREFIX_MAP: [string, string][] = [
30
+ ["gpt-", "openai"],
31
+ ["openai/", "openai"],
32
+ ["grok", "xai"],
33
+ ["claude", "anthropic"],
34
+ ["anthropic/", "anthropic"],
35
+ ["qwen-", "dashscope"],
36
+ ["qwen/", "dashscope"],
37
+ ["kimi", "dashscope"],
38
+ ["deepseek", "deepseek"],
39
+ ["llama-", "groq"],
40
+ ["mixtral", "groq"],
41
+ ["gemma", "groq"],
42
+ ];
43
+
44
+ /* ─── Main detection logic ─── */
45
+
46
+ export function resolveProvider(model: string, config: MiraiConfig): ProviderInfo {
47
+ // 1. Explicit "providerID/modelID" prefix
48
+ const slashIdx = model.indexOf("/");
49
+ if (slashIdx > 0) {
50
+ const providerId = model.slice(0, slashIdx);
51
+ const modelId = model.slice(slashIdx + 1);
52
+ const resolved = getProviderConfig(providerId, config);
53
+ if (resolved) {
54
+ return {
55
+ id: providerId,
56
+ ...resolved,
57
+ model: modelId,
58
+ };
59
+ }
60
+ // Provider not found — continue to detect / fallback
61
+ }
62
+
63
+ // 2. OLLAMA_HOST env set → Ollama
64
+ if (process.env.OLLAMA_HOST) {
65
+ const ollama = PROVIDERS.ollama;
66
+ return {
67
+ id: "ollama",
68
+ name: ollama.name,
69
+ baseURL: readBaseUrl(ollama),
70
+ apiKey: undefined,
71
+ supportsReasoning: false,
72
+ noAuth: true,
73
+ model,
74
+ };
75
+ }
76
+
77
+ // 3. OPENAI_BASE_URL env set (custom OpenAI-compatible)
78
+ if (process.env.OPENAI_BASE_URL) {
79
+ return {
80
+ id: "openai",
81
+ name: "OpenAI (custom)",
82
+ baseURL: process.env.OPENAI_BASE_URL,
83
+ apiKey: process.env.OPENAI_API_KEY ?? undefined,
84
+ supportsReasoning: false,
85
+ noAuth: !process.env.OPENAI_API_KEY,
86
+ model,
87
+ };
88
+ }
89
+
90
+ // 4. Model prefix detection
91
+ for (const [prefix, providerId] of PREFIX_MAP) {
92
+ if (model.startsWith(prefix)) {
93
+ const builtin = PROVIDERS[providerId];
94
+ if (builtin) {
95
+ return {
96
+ id: providerId,
97
+ name: builtin.name,
98
+ baseURL: readBaseUrl(builtin),
99
+ apiKey: readApiKey(builtin) ?? undefined,
100
+ supportsReasoning: builtin.supportsReasoning ?? false,
101
+ noAuth: builtin.noAuth ?? false,
102
+ model,
103
+ };
104
+ }
105
+ }
106
+ }
107
+
108
+ // 5. API key env sniff
109
+ const keyProviders: [string, string][] = [
110
+ ["OPENAI_API_KEY", "openai"],
111
+ ["XAI_API_KEY", "xai"],
112
+ ["DASHSCOPE_API_KEY", "dashscope"],
113
+ ["GROQ_API_KEY", "groq"],
114
+ ["DEEPSEEK_API_KEY", "deepseek"],
115
+ ];
116
+ for (const [envKey, providerId] of keyProviders) {
117
+ if (process.env[envKey]) {
118
+ const builtin = PROVIDERS[providerId];
119
+ if (builtin) {
120
+ return {
121
+ id: providerId,
122
+ name: builtin.name,
123
+ baseURL: readBaseUrl(builtin),
124
+ apiKey: process.env[envKey]!,
125
+ supportsReasoning: builtin.supportsReasoning ?? false,
126
+ noAuth: false,
127
+ model,
128
+ };
129
+ }
130
+ }
131
+ }
132
+
133
+ // 6. Config default — if not already resolved (avoid recursion)
134
+ if (config.default && config.default.includes("/")) {
135
+ return resolveProvider(config.default, config);
136
+ }
137
+
138
+ // 7. Fallback → Ollama local
139
+ const ollama = PROVIDERS.ollama;
140
+ return {
141
+ id: "ollama",
142
+ name: ollama.name,
143
+ baseURL: readBaseUrl(ollama),
144
+ apiKey: undefined,
145
+ supportsReasoning: false,
146
+ noAuth: true,
147
+ model,
148
+ };
149
+ }
package/bin/router.ts ADDED
@@ -0,0 +1,134 @@
1
+ /* ─── Mirai CLI router ───
2
+ * Entry from bin/mirai.ts
3
+ * Parses args, loads config, detects provider, boots TUI or web
4
+ */
5
+
6
+ import { loadConfig, type MiraiConfig } from "./config.js";
7
+ import { resolveProvider, type ProviderInfo } from "./provider.js";
8
+ import { DEFAULT_MODEL } from "@mirai/core/constants";
9
+
10
+ const HELP = `
11
+ Usage:
12
+ mirai Start TUI (default)
13
+ mirai --model <model> Start TUI with specific model
14
+ mirai --web Start web interface
15
+ mirai --web --model <m> Web + model
16
+ mirai --help Show this help
17
+ mirai --version Show version
18
+
19
+ Examples:
20
+ mirai --model openai/gpt-4.1-mini
21
+ mirai --model groq/llama-4
22
+ mirai --web
23
+ `;
24
+
25
+ export async function main(): Promise<void> {
26
+ const args = parseArgs(process.argv.slice(2));
27
+
28
+ if (args.help) {
29
+ console.log(HELP);
30
+ process.exit(0);
31
+ }
32
+
33
+ if (args.version) {
34
+ console.log("mirai v1.0.0");
35
+ process.exit(0);
36
+ }
37
+
38
+ // 1. Load config
39
+ const config = await loadConfig();
40
+
41
+ // 2. Resolve model (priority: CLI > env > config > hardcode)
42
+ const model =
43
+ args.model ?? process.env.DEFAULT_MODEL ?? config.default ?? DEFAULT_MODEL;
44
+
45
+ // 3. Resolve provider for this model
46
+ const provider = resolveProvider(model, config);
47
+
48
+ // 4. Boot
49
+ if (args.web) {
50
+ await bootWeb(model, provider, config);
51
+ } else {
52
+ await bootTui(model, provider, config);
53
+ }
54
+ }
55
+
56
+ /* ─── Args parsing ─── */
57
+
58
+ interface CliArgs {
59
+ model?: string;
60
+ web: boolean;
61
+ help: boolean;
62
+ version: boolean;
63
+ }
64
+
65
+ function parseArgs(argv: string[]): CliArgs {
66
+ const args: CliArgs = { web: false, help: false, version: false };
67
+
68
+ for (let i = 0; i < argv.length; i++) {
69
+ switch (argv[i]) {
70
+ case "--model":
71
+ case "-m":
72
+ args.model = argv[++i];
73
+ break;
74
+ case "--web":
75
+ case "-w":
76
+ args.web = true;
77
+ break;
78
+ case "--help":
79
+ case "-h":
80
+ args.help = true;
81
+ break;
82
+ case "--version":
83
+ case "-v":
84
+ args.version = true;
85
+ break;
86
+ }
87
+ }
88
+
89
+ return args;
90
+ }
91
+
92
+ /* ─── Boot TUI ─── */
93
+
94
+ async function bootTui(
95
+ model: string,
96
+ _provider: ProviderInfo,
97
+ _config: MiraiConfig,
98
+ ): Promise<void> {
99
+ const { render } = await import("ink");
100
+ const { createElement } = await import("react");
101
+ const { default: App } = await import("../src/app/index.js");
102
+ const { mouseInput } = await import("../src/services/mouse-input.js");
103
+
104
+ mouseInput.onExit(() => process.exit(0));
105
+ mouseInput.enable();
106
+
107
+ const stdin = Object.assign(mouseInput.stdin, {
108
+ isTTY: true as const,
109
+ setRawMode: (mode: boolean) => process.stdin.setRawMode?.(mode),
110
+ ref: () => process.stdin.ref?.(),
111
+ unref: () => process.stdin.unref?.(),
112
+ }) as unknown as NodeJS.ReadStream;
113
+
114
+ const { waitUntilExit } = render(createElement(App, { model }), {
115
+ stdin,
116
+ interactive: true,
117
+ alternateScreen: true,
118
+ maxFps: 140,
119
+ });
120
+
121
+ await waitUntilExit();
122
+ mouseInput.disable();
123
+ }
124
+
125
+ /* ─── Boot Web ─── */
126
+
127
+ async function bootWeb(
128
+ _model: string,
129
+ _provider: ProviderInfo,
130
+ _config: MiraiConfig,
131
+ ): Promise<void> {
132
+ console.log("[mirai] Web mode coming soon — use without --web for TUI");
133
+ process.exit(0);
134
+ }