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.
- package/bin/config.ts +99 -0
- package/bin/mirai.js +17 -0
- package/bin/mirai.ts +4 -0
- package/bin/provider.ts +149 -0
- package/bin/router.ts +134 -0
- package/dist/mirai.mjs +28316 -0
- package/package.json +29 -0
- package/src/app/index.tsx +274 -0
- package/src/components/chat.tsx +254 -0
- package/src/components/dialog/help-dialog.tsx +101 -0
- package/src/components/dialog/index.ts +3 -0
- package/src/components/dialog/provider.tsx +96 -0
- package/src/components/header/index.tsx +78 -0
- package/src/components/input/command-palette.tsx +129 -0
- package/src/components/input/commands.ts +46 -0
- package/src/components/input/index.tsx +284 -0
- package/src/components/matrix-rain/index.tsx +122 -0
- package/src/components/permission-modal.tsx +66 -0
- package/src/components/scroll-bar/index.tsx +56 -0
- package/src/components/status-bar/index.tsx +43 -0
- package/src/components/tool-result.tsx +11 -0
- package/src/hooks/use-chat.ts +208 -0
- package/src/hooks/use-mouse.tsx +121 -0
- package/src/hooks/use-permission.ts +35 -0
- package/src/hooks/use-runtime.ts +99 -0
- package/src/hooks/use-scroll-bar-drag.ts +115 -0
- package/src/hooks/use-scroll.ts +70 -0
- package/src/index.ts +39 -0
- package/src/renderers/builtins/BashResult.tsx +65 -0
- package/src/renderers/builtins/EditFileResult.tsx +69 -0
- package/src/renderers/builtins/GenericToolResult.tsx +39 -0
- package/src/renderers/builtins/GlobSearchResult.tsx +40 -0
- package/src/renderers/builtins/GrepSearchResult.tsx +49 -0
- package/src/renderers/builtins/ReadFileResult.tsx +54 -0
- package/src/renderers/builtins/WriteFileResult.tsx +24 -0
- package/src/renderers/constants.ts +7 -0
- package/src/renderers/register-builtins.ts +27 -0
- package/src/renderers/registry.ts +37 -0
- package/src/renderers/status.ts +22 -0
- package/src/renderers/utils.ts +70 -0
- package/src/services/hit-test.ts +49 -0
- package/src/services/mouse-input.ts +237 -0
- package/src/services/scroll-registry.ts +64 -0
- package/src/services/tui-permission-provider.ts +35 -0
- package/src/theme.ts +38 -0
- 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
package/bin/provider.ts
ADDED
|
@@ -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
|
+
}
|