@takara-ai/miru-code 0.3.0 → 0.4.1
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/README.md +180 -259
- package/package.json +1 -1
- package/src/agents/claude.md +1 -1
- package/src/agents/copilot.md +1 -1
- package/src/agents/cursor.md +1 -1
- package/src/agents/gemini.md +1 -1
- package/src/agents/kiro.md +1 -1
- package/src/agents/opencode.md +1 -1
- package/src/cli-ui.ts +189 -0
- package/src/cli.ts +110 -33
- package/src/help.ts +115 -130
- package/src/installer/agents.ts +246 -0
- package/src/installer/config.ts +273 -0
- package/src/installer/installer.ts +256 -0
- package/src/installer/prompt.ts +77 -0
- package/src/setup.ts +17 -20
- package/src/spinner.ts +10 -4
package/src/cli-ui.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type { SearchResult } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
const ANSI = {
|
|
4
|
+
reset: "\x1b[0m",
|
|
5
|
+
bold: "\x1b[1m",
|
|
6
|
+
dim: "\x1b[2m",
|
|
7
|
+
cyan: "\x1b[36m",
|
|
8
|
+
green: "\x1b[32m",
|
|
9
|
+
red: "\x1b[31m",
|
|
10
|
+
yellow: "\x1b[33m",
|
|
11
|
+
magenta: "\x1b[35m",
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
function colorEnabled(stream: NodeJS.WriteStream = process.stdout): boolean {
|
|
15
|
+
return stream.isTTY && process.env.NO_COLOR === undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function paint(text: string, code: string, stream?: NodeJS.WriteStream): string {
|
|
19
|
+
if (!colorEnabled(stream)) {
|
|
20
|
+
return text;
|
|
21
|
+
}
|
|
22
|
+
return `${code}${text}${ANSI.reset}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function bold(text: string, stream?: NodeJS.WriteStream): string {
|
|
26
|
+
return paint(text, ANSI.bold, stream);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function dim(text: string, stream?: NodeJS.WriteStream): string {
|
|
30
|
+
return paint(text, ANSI.dim, stream);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function cyan(text: string, stream?: NodeJS.WriteStream): string {
|
|
34
|
+
return paint(text, ANSI.cyan, stream);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function green(text: string, stream?: NodeJS.WriteStream): string {
|
|
38
|
+
return paint(text, ANSI.green, stream);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function red(text: string, stream?: NodeJS.WriteStream): string {
|
|
42
|
+
return paint(text, ANSI.red, stream);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function yellow(text: string, stream?: NodeJS.WriteStream): string {
|
|
46
|
+
return paint(text, ANSI.yellow, stream);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function magenta(text: string, stream?: NodeJS.WriteStream): string {
|
|
50
|
+
return paint(text, ANSI.magenta, stream);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isInteractiveStdout(): boolean {
|
|
54
|
+
return process.stdout.isTTY && process.env.NO_COLOR === undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function prefersJsonOutput(jsonFlag: boolean): boolean {
|
|
58
|
+
return jsonFlag || !process.stdout.isTTY;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function brandTitle(): string {
|
|
62
|
+
return bold("Miru", process.stderr) + dim(" (見る)", process.stderr);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function writeStdout(line = ""): void {
|
|
66
|
+
process.stdout.write(`${line}\n`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function writeStderr(line = ""): void {
|
|
70
|
+
process.stderr.write(`${line}\n`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function divider(char = "─", width = 52, stream: NodeJS.WriteStream = process.stdout): void {
|
|
74
|
+
const line = char.repeat(width);
|
|
75
|
+
stream.write(`${dim(line, stream)}\n`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function header(title: string, subtitle?: string): void {
|
|
79
|
+
writeStdout("");
|
|
80
|
+
writeStdout(brandTitle() + (title ? dim(` · ${title}`) : ""));
|
|
81
|
+
if (subtitle) {
|
|
82
|
+
writeStdout(dim(subtitle));
|
|
83
|
+
}
|
|
84
|
+
divider();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function section(title: string): void {
|
|
88
|
+
writeStdout("");
|
|
89
|
+
writeStdout(bold(title));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function commandRow(name: string, description: string, indent = 2): void {
|
|
93
|
+
const pad = " ".repeat(indent);
|
|
94
|
+
const nameCol = name.padEnd(16);
|
|
95
|
+
writeStdout(`${pad}${cyan(nameCol)} ${dim(description)}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function writeLine(message: string, stream: NodeJS.WriteStream = process.stderr): void {
|
|
99
|
+
stream.write(`${message}\n`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function success(message: string, stream?: NodeJS.WriteStream): void {
|
|
103
|
+
writeLine(green("✓ ") + message, stream);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function info(message: string, stream?: NodeJS.WriteStream): void {
|
|
107
|
+
writeLine(dim("· ") + message, stream);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function warn(message: string, stream?: NodeJS.WriteStream): void {
|
|
111
|
+
writeLine(yellow("! ") + message, stream);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function fail(message: string, stream?: NodeJS.WriteStream): void {
|
|
115
|
+
writeLine(red("✗ ") + message, stream);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function hint(message: string, stream?: NodeJS.WriteStream): void {
|
|
119
|
+
writeLine(dim(` ${message}`), stream);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const PREVIEW_LINES = 12;
|
|
123
|
+
const PREVIEW_WIDTH = 72;
|
|
124
|
+
|
|
125
|
+
function truncateLine(line: string, width: number): string {
|
|
126
|
+
if (line.length <= width) {
|
|
127
|
+
return line;
|
|
128
|
+
}
|
|
129
|
+
return `${line.slice(0, width - 1)}…`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function previewContent(content: string): string[] {
|
|
133
|
+
return content
|
|
134
|
+
.split("\n")
|
|
135
|
+
.slice(0, PREVIEW_LINES)
|
|
136
|
+
.map((line) => truncateLine(line.replace(/\t/g, " "), PREVIEW_WIDTH));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function formatSearchResultsPretty(query: string, results: SearchResult[]): string {
|
|
140
|
+
const lines: string[] = [];
|
|
141
|
+
const count = results.length;
|
|
142
|
+
const label = count === 1 ? "1 result" : `${count} results`;
|
|
143
|
+
|
|
144
|
+
lines.push("");
|
|
145
|
+
lines.push(bold(label) + dim(` for `) + cyan(`"${query}"`));
|
|
146
|
+
lines.push("");
|
|
147
|
+
|
|
148
|
+
for (let i = 0; i < results.length; i++) {
|
|
149
|
+
const result = results[i];
|
|
150
|
+
if (!result) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const { chunk, score } = result;
|
|
154
|
+
const location = `${chunk.file_path}:${chunk.start_line}-${chunk.end_line}`;
|
|
155
|
+
const lang = chunk.language ? dim(` ${chunk.language}`) : "";
|
|
156
|
+
const rank = dim(`[${i + 1}]`);
|
|
157
|
+
const scoreText = magenta(score.toFixed(3));
|
|
158
|
+
|
|
159
|
+
lines.push(`${rank} ${scoreText} ${bold(location)}${lang}`);
|
|
160
|
+
lines.push(dim("─".repeat(Math.min(PREVIEW_WIDTH, 52))));
|
|
161
|
+
|
|
162
|
+
for (const line of previewContent(chunk.content)) {
|
|
163
|
+
lines.push(dim(" ") + line);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const totalLines = chunk.content.split("\n").length;
|
|
167
|
+
if (totalLines > PREVIEW_LINES) {
|
|
168
|
+
lines.push(dim(` … ${totalLines - PREVIEW_LINES} more lines`));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (i < results.length - 1) {
|
|
172
|
+
lines.push("");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (isInteractiveStdout()) {
|
|
177
|
+
lines.push("");
|
|
178
|
+
lines.push(dim("Tip: add --json for machine-readable output"));
|
|
179
|
+
}
|
|
180
|
+
return lines.join("\n");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function formatSearchErrorPretty(message: string): string {
|
|
184
|
+
return `\n${yellow("No results")} ${dim("—")} ${message}\n`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function formatRelatedHeader(filePath: string, line: number): string {
|
|
188
|
+
return `related to ${filePath}:${line}`;
|
|
189
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { type AgentId, writeAgentFile } from "./agents.ts";
|
|
4
4
|
import { clearCache } from "./cache.ts";
|
|
5
|
+
import {
|
|
6
|
+
fail,
|
|
7
|
+
formatRelatedHeader,
|
|
8
|
+
formatSearchErrorPretty,
|
|
9
|
+
formatSearchResultsPretty,
|
|
10
|
+
hint,
|
|
11
|
+
prefersJsonOutput,
|
|
12
|
+
success,
|
|
13
|
+
} from "./cli-ui.ts";
|
|
5
14
|
import { loadStoredCredentials } from "./credentials.ts";
|
|
6
15
|
import { loadEnvFiles } from "./env-files.ts";
|
|
7
16
|
import {
|
|
@@ -11,10 +20,12 @@ import {
|
|
|
11
20
|
printFullHelp,
|
|
12
21
|
printMainHelp,
|
|
13
22
|
} from "./help.ts";
|
|
23
|
+
import { runInstaller } from "./installer/installer.ts";
|
|
14
24
|
import { serveMcp } from "./mcp/serve.ts";
|
|
15
25
|
import { MiruIndex } from "./miru-index.ts";
|
|
16
26
|
import { ensureCredentials, runClearCredentials, runSetup } from "./setup.ts";
|
|
17
|
-
import
|
|
27
|
+
import { withSpinner } from "./spinner.ts";
|
|
28
|
+
import type { ContentType, SearchResult } from "./types.ts";
|
|
18
29
|
import { formatResults, resolveChunk, resolveContent } from "./utils.ts";
|
|
19
30
|
|
|
20
31
|
loadEnvFiles();
|
|
@@ -24,6 +35,8 @@ const CLI_COMMANDS = new Set([
|
|
|
24
35
|
"search",
|
|
25
36
|
"find-related",
|
|
26
37
|
"init",
|
|
38
|
+
"install",
|
|
39
|
+
"uninstall",
|
|
27
40
|
"setup",
|
|
28
41
|
"clear",
|
|
29
42
|
"help",
|
|
@@ -33,6 +46,19 @@ const CLI_COMMANDS = new Set([
|
|
|
33
46
|
|
|
34
47
|
const AGENTS = new Set<AgentId>(AGENT_IDS);
|
|
35
48
|
|
|
49
|
+
function parseFlagArgv(argv: string[], flag: string): { present: boolean; rest: string[] } {
|
|
50
|
+
const rest: string[] = [];
|
|
51
|
+
let present = false;
|
|
52
|
+
for (const arg of argv) {
|
|
53
|
+
if (arg === flag) {
|
|
54
|
+
present = true;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
rest.push(arg);
|
|
58
|
+
}
|
|
59
|
+
return { present, rest };
|
|
60
|
+
}
|
|
61
|
+
|
|
36
62
|
function parseContentArgv(argv: string[]): { content: ContentType[]; rest: string[] } {
|
|
37
63
|
const rest: string[] = [];
|
|
38
64
|
const content: string[] = [];
|
|
@@ -77,19 +103,46 @@ function parseTopK(argv: string[]): { topK: number; rest: string[] } {
|
|
|
77
103
|
return { topK: Number.isFinite(topK) && topK >= 1 ? Math.floor(topK) : 5, rest };
|
|
78
104
|
}
|
|
79
105
|
|
|
106
|
+
function emitSearchOutput(
|
|
107
|
+
query: string,
|
|
108
|
+
results: SearchResult[],
|
|
109
|
+
jsonFlag: boolean,
|
|
110
|
+
emptyMessage = "No results found.",
|
|
111
|
+
): void {
|
|
112
|
+
if (results.length === 0) {
|
|
113
|
+
if (prefersJsonOutput(jsonFlag)) {
|
|
114
|
+
console.log(JSON.stringify({ error: emptyMessage }));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
process.stdout.write(formatSearchErrorPretty(emptyMessage));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (prefersJsonOutput(jsonFlag)) {
|
|
122
|
+
console.log(JSON.stringify(formatResults(query, results)));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
process.stdout.write(formatSearchResultsPretty(query, results));
|
|
127
|
+
}
|
|
128
|
+
|
|
80
129
|
async function runSearch(
|
|
81
130
|
path: string,
|
|
82
131
|
query: string,
|
|
83
132
|
topK: number,
|
|
84
133
|
content: ContentType[],
|
|
134
|
+
jsonFlag: boolean,
|
|
85
135
|
): Promise<void> {
|
|
86
136
|
await ensureCredentials();
|
|
87
|
-
|
|
88
|
-
await
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
results
|
|
92
|
-
|
|
137
|
+
|
|
138
|
+
const index = await withSpinner("Indexing and searching", async () => {
|
|
139
|
+
const built = await MiruIndex.fromSource(path, content);
|
|
140
|
+
await built.saveToDefaultCache(path);
|
|
141
|
+
const results = await built.search({ query, topK });
|
|
142
|
+
return { index: built, results };
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
emitSearchOutput(query, index.results, jsonFlag);
|
|
93
146
|
}
|
|
94
147
|
|
|
95
148
|
async function runFindRelated(
|
|
@@ -98,39 +151,49 @@ async function runFindRelated(
|
|
|
98
151
|
line: number,
|
|
99
152
|
topK: number,
|
|
100
153
|
content: ContentType[],
|
|
154
|
+
jsonFlag: boolean,
|
|
101
155
|
): Promise<void> {
|
|
102
156
|
await ensureCredentials();
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
157
|
+
|
|
158
|
+
const { results, label } = await withSpinner("Finding related chunks", async () => {
|
|
159
|
+
const built = await MiruIndex.fromSource(path, content);
|
|
160
|
+
const chunk = resolveChunk(built.chunks, filePath, line);
|
|
161
|
+
if (!chunk) {
|
|
162
|
+
throw new RelatedChunkNotFoundError(filePath, line);
|
|
163
|
+
}
|
|
164
|
+
const hits = await built.findRelated(chunk, topK);
|
|
165
|
+
await built.saveToDefaultCache(path);
|
|
166
|
+
return {
|
|
167
|
+
results: hits,
|
|
168
|
+
label: formatRelatedHeader(filePath, line),
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
emitSearchOutput(label, results, jsonFlag, `No related chunks found for ${filePath}:${line}.`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
class RelatedChunkNotFoundError extends Error {
|
|
176
|
+
constructor(filePath: string, line: number) {
|
|
177
|
+
super(`No chunk found at ${filePath}:${line}.`);
|
|
178
|
+
this.name = "RelatedChunkNotFoundError";
|
|
108
179
|
}
|
|
109
|
-
const results = await index.findRelated(chunk, topK);
|
|
110
|
-
const label = `Chunks related to ${filePath}:${line}`;
|
|
111
|
-
const out =
|
|
112
|
-
results.length > 0
|
|
113
|
-
? formatResults(label, results)
|
|
114
|
-
: { error: `No related chunks found for ${filePath}:${line}.` };
|
|
115
|
-
console.log(JSON.stringify(out));
|
|
116
|
-
await index.saveToDefaultCache(path);
|
|
117
180
|
}
|
|
118
181
|
|
|
119
182
|
async function runInit(agent: AgentId, force: boolean): Promise<void> {
|
|
120
183
|
try {
|
|
121
184
|
const dest = await writeAgentFile(agent, { force });
|
|
122
|
-
|
|
185
|
+
success(`Wrote sub-agent: ${dest}`);
|
|
123
186
|
} catch (err) {
|
|
124
187
|
const message = err instanceof Error ? err.message : String(err);
|
|
125
|
-
|
|
126
|
-
|
|
188
|
+
fail(message);
|
|
189
|
+
hint("Use --force to overwrite an existing file.");
|
|
127
190
|
process.exit(1);
|
|
128
191
|
}
|
|
129
192
|
}
|
|
130
193
|
|
|
131
194
|
async function runClear(path: string): Promise<void> {
|
|
132
195
|
await clearCache(path);
|
|
133
|
-
|
|
196
|
+
success(`Cleared cached index for ${path}`);
|
|
134
197
|
}
|
|
135
198
|
|
|
136
199
|
async function runCli(argv: string[]): Promise<void> {
|
|
@@ -156,6 +219,11 @@ async function runCli(argv: string[]): Promise<void> {
|
|
|
156
219
|
return;
|
|
157
220
|
}
|
|
158
221
|
|
|
222
|
+
if (command === "install" || command === "uninstall") {
|
|
223
|
+
await runInstaller(command);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
159
227
|
if (command === "init") {
|
|
160
228
|
let agent: AgentId | undefined;
|
|
161
229
|
let force = false;
|
|
@@ -166,19 +234,19 @@ async function runCli(argv: string[]): Promise<void> {
|
|
|
166
234
|
} else if (arg === "--agent" || arg === "-a") {
|
|
167
235
|
const value = rest[++i];
|
|
168
236
|
if (!value) {
|
|
169
|
-
|
|
237
|
+
fail("Missing value for --agent.");
|
|
170
238
|
printCommandHelp("init");
|
|
171
239
|
process.exit(1);
|
|
172
240
|
}
|
|
173
241
|
if (!AGENTS.has(value as AgentId)) {
|
|
174
|
-
|
|
242
|
+
fail(formatUnknownAgent(value));
|
|
175
243
|
process.exit(1);
|
|
176
244
|
}
|
|
177
245
|
agent = value as AgentId;
|
|
178
246
|
}
|
|
179
247
|
}
|
|
180
248
|
if (!agent) {
|
|
181
|
-
|
|
249
|
+
fail("miru init requires --agent.");
|
|
182
250
|
printCommandHelp("init");
|
|
183
251
|
process.exit(1);
|
|
184
252
|
}
|
|
@@ -202,7 +270,7 @@ async function runCli(argv: string[]): Promise<void> {
|
|
|
202
270
|
}
|
|
203
271
|
if (clear) {
|
|
204
272
|
if (apiKey) {
|
|
205
|
-
|
|
273
|
+
fail("miru setup --clear cannot be combined with --key.");
|
|
206
274
|
process.exit(1);
|
|
207
275
|
}
|
|
208
276
|
await runClearCredentials();
|
|
@@ -218,7 +286,8 @@ async function runCli(argv: string[]): Promise<void> {
|
|
|
218
286
|
return;
|
|
219
287
|
}
|
|
220
288
|
|
|
221
|
-
const {
|
|
289
|
+
const { present: jsonFlag, rest: jsonRest } = parseFlagArgv(rest, "--json");
|
|
290
|
+
const { content, rest: contentRest } = parseContentArgv(jsonRest);
|
|
222
291
|
const { topK, rest: sizedRest } = parseTopK(contentRest);
|
|
223
292
|
|
|
224
293
|
if (command === "search") {
|
|
@@ -228,7 +297,7 @@ async function runCli(argv: string[]): Promise<void> {
|
|
|
228
297
|
process.exit(1);
|
|
229
298
|
}
|
|
230
299
|
const path = resolve(sizedRest[1] ?? process.cwd());
|
|
231
|
-
await runSearch(path, query, topK, content);
|
|
300
|
+
await runSearch(path, query, topK, content, jsonFlag);
|
|
232
301
|
return;
|
|
233
302
|
}
|
|
234
303
|
|
|
@@ -241,11 +310,19 @@ async function runCli(argv: string[]): Promise<void> {
|
|
|
241
310
|
}
|
|
242
311
|
const line = Number(lineRaw);
|
|
243
312
|
const path = resolve(sizedRest[2] ?? process.cwd());
|
|
244
|
-
|
|
313
|
+
try {
|
|
314
|
+
await runFindRelated(path, filePath, line, topK, content, jsonFlag);
|
|
315
|
+
} catch (err) {
|
|
316
|
+
if (err instanceof RelatedChunkNotFoundError) {
|
|
317
|
+
fail(err.message);
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
throw err;
|
|
321
|
+
}
|
|
245
322
|
return;
|
|
246
323
|
}
|
|
247
324
|
|
|
248
|
-
|
|
325
|
+
fail(`Unknown command: ${command}`);
|
|
249
326
|
printMainHelp();
|
|
250
327
|
process.exit(1);
|
|
251
328
|
}
|
|
@@ -296,6 +373,6 @@ async function main(): Promise<void> {
|
|
|
296
373
|
}
|
|
297
374
|
|
|
298
375
|
main().catch((err) => {
|
|
299
|
-
|
|
376
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
300
377
|
process.exit(1);
|
|
301
378
|
});
|