@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/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 type { ContentType } from "./types.ts";
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
- const index = await MiruIndex.fromSource(path, content);
88
- await index.saveToDefaultCache(path);
89
- const results = await index.search({ query, topK });
90
- const out =
91
- results.length > 0 ? formatResults(query, results) : { error: "No results found." as const };
92
- console.log(JSON.stringify(out));
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
- const index = await MiruIndex.fromSource(path, content);
104
- const chunk = resolveChunk(index.chunks, filePath, line);
105
- if (!chunk) {
106
- console.error(`No chunk found at ${filePath}:${line}.`);
107
- process.exit(1);
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
- process.stdout.write(`Wrote sub-agent: ${dest}\n`);
185
+ success(`Wrote sub-agent: ${dest}`);
123
186
  } catch (err) {
124
187
  const message = err instanceof Error ? err.message : String(err);
125
- process.stderr.write(`${message}\n`);
126
- process.stderr.write("Use --force to overwrite an existing file.\n");
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
- console.log(`Cleared cached index for: ${path}`);
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
- process.stderr.write("Missing value for --agent.\n");
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
- process.stderr.write(`${formatUnknownAgent(value)}\n`);
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
- process.stderr.write("miru init requires --agent.\n");
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
- console.error("Usage: miru setup --clear cannot be combined with --key.");
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 { content, rest: contentRest } = parseContentArgv(rest);
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
- await runFindRelated(path, filePath, line, topK, content);
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
- process.stderr.write(`Unknown command: ${command}\n`);
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
- console.error(err instanceof Error ? err.message : err);
376
+ fail(err instanceof Error ? err.message : String(err));
300
377
  process.exit(1);
301
378
  });