@takara-ai/miru-code 0.2.0 → 0.4.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/README.md +202 -167
- 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 +167 -64
- package/src/credentials.ts +2 -6
- package/src/env.ts +6 -10
- package/src/help.ts +160 -0
- 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/prompt.ts +111 -0
- package/src/setup.ts +20 -72
- 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,20 +2,62 @@
|
|
|
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";
|
|
16
|
+
import {
|
|
17
|
+
AGENT_IDS,
|
|
18
|
+
formatUnknownAgent,
|
|
19
|
+
printCommandHelp,
|
|
20
|
+
printFullHelp,
|
|
21
|
+
printMainHelp,
|
|
22
|
+
} from "./help.ts";
|
|
23
|
+
import { runInstaller } from "./installer/installer.ts";
|
|
7
24
|
import { serveMcp } from "./mcp/serve.ts";
|
|
8
25
|
import { MiruIndex } from "./miru-index.ts";
|
|
9
26
|
import { ensureCredentials, runClearCredentials, runSetup } from "./setup.ts";
|
|
10
|
-
import
|
|
27
|
+
import { withSpinner } from "./spinner.ts";
|
|
28
|
+
import type { ContentType, SearchResult } from "./types.ts";
|
|
11
29
|
import { formatResults, resolveChunk, resolveContent } from "./utils.ts";
|
|
12
30
|
|
|
13
31
|
loadEnvFiles();
|
|
14
32
|
await loadStoredCredentials();
|
|
15
33
|
|
|
16
|
-
const CLI_COMMANDS = new Set([
|
|
34
|
+
const CLI_COMMANDS = new Set([
|
|
35
|
+
"search",
|
|
36
|
+
"find-related",
|
|
37
|
+
"init",
|
|
38
|
+
"install",
|
|
39
|
+
"uninstall",
|
|
40
|
+
"setup",
|
|
41
|
+
"clear",
|
|
42
|
+
"help",
|
|
43
|
+
"-h",
|
|
44
|
+
"--help",
|
|
45
|
+
]);
|
|
17
46
|
|
|
18
|
-
const AGENTS = new Set<AgentId>(
|
|
47
|
+
const AGENTS = new Set<AgentId>(AGENT_IDS);
|
|
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
|
+
}
|
|
19
61
|
|
|
20
62
|
function parseContentArgv(argv: string[]): { content: ContentType[]; rest: string[] } {
|
|
21
63
|
const rest: string[] = [];
|
|
@@ -61,19 +103,46 @@ function parseTopK(argv: string[]): { topK: number; rest: string[] } {
|
|
|
61
103
|
return { topK: Number.isFinite(topK) && topK >= 1 ? Math.floor(topK) : 5, rest };
|
|
62
104
|
}
|
|
63
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
|
+
|
|
64
129
|
async function runSearch(
|
|
65
130
|
path: string,
|
|
66
131
|
query: string,
|
|
67
132
|
topK: number,
|
|
68
133
|
content: ContentType[],
|
|
134
|
+
jsonFlag: boolean,
|
|
69
135
|
): Promise<void> {
|
|
70
136
|
await ensureCredentials();
|
|
71
|
-
|
|
72
|
-
await
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
results
|
|
76
|
-
|
|
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);
|
|
77
146
|
}
|
|
78
147
|
|
|
79
148
|
async function runFindRelated(
|
|
@@ -82,81 +151,105 @@ async function runFindRelated(
|
|
|
82
151
|
line: number,
|
|
83
152
|
topK: number,
|
|
84
153
|
content: ContentType[],
|
|
154
|
+
jsonFlag: boolean,
|
|
85
155
|
): Promise<void> {
|
|
86
156
|
await ensureCredentials();
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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";
|
|
92
179
|
}
|
|
93
|
-
const results = await index.findRelated(chunk, topK);
|
|
94
|
-
const label = `Chunks related to ${filePath}:${line}`;
|
|
95
|
-
const out =
|
|
96
|
-
results.length > 0
|
|
97
|
-
? formatResults(label, results)
|
|
98
|
-
: { error: `No related chunks found for ${filePath}:${line}.` };
|
|
99
|
-
console.log(JSON.stringify(out));
|
|
100
|
-
await index.saveToDefaultCache(path);
|
|
101
180
|
}
|
|
102
181
|
|
|
103
182
|
async function runInit(agent: AgentId, force: boolean): Promise<void> {
|
|
104
|
-
|
|
105
|
-
|
|
183
|
+
try {
|
|
184
|
+
const dest = await writeAgentFile(agent, { force });
|
|
185
|
+
success(`Wrote sub-agent: ${dest}`);
|
|
186
|
+
} catch (err) {
|
|
187
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
188
|
+
fail(message);
|
|
189
|
+
hint("Use --force to overwrite an existing file.");
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
106
192
|
}
|
|
107
193
|
|
|
108
194
|
async function runClear(path: string): Promise<void> {
|
|
109
195
|
await clearCache(path);
|
|
110
|
-
|
|
196
|
+
success(`Cleared cached index for ${path}`);
|
|
111
197
|
}
|
|
112
198
|
|
|
113
|
-
function
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
CLI:
|
|
117
|
-
miru search <query> [path] [-k N] [--content code|docs|config|all]
|
|
118
|
-
miru find-related <file_path> <line> [path] [-k N] [--content ...]
|
|
119
|
-
miru setup [--key KEY] [--force] [--clear]
|
|
120
|
-
miru init [--agent cursor] [--force]
|
|
121
|
-
miru clear [path]
|
|
122
|
-
|
|
123
|
-
MCP server (default when no CLI subcommand):
|
|
124
|
-
miru [--ref BRANCH] [--content code ...]
|
|
125
|
-
|
|
126
|
-
Indexes a repo on the first search/find_related call via the tool repo argument.
|
|
199
|
+
async function runCli(argv: string[]): Promise<void> {
|
|
200
|
+
const [command, ...rest] = argv;
|
|
127
201
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
202
|
+
if (command === undefined) {
|
|
203
|
+
printMainHelp();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
131
206
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
207
|
+
if (command === "-h" || command === "--help") {
|
|
208
|
+
printFullHelp();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
137
211
|
|
|
138
|
-
|
|
139
|
-
|
|
212
|
+
if (command === "help") {
|
|
213
|
+
const topic = rest[0];
|
|
214
|
+
if (!topic) {
|
|
215
|
+
printMainHelp();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
printCommandHelp(topic);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
140
221
|
|
|
141
|
-
if (command === "
|
|
142
|
-
|
|
222
|
+
if (command === "install" || command === "uninstall") {
|
|
223
|
+
await runInstaller(command);
|
|
143
224
|
return;
|
|
144
225
|
}
|
|
145
226
|
|
|
146
227
|
if (command === "init") {
|
|
147
|
-
let agent: AgentId
|
|
228
|
+
let agent: AgentId | undefined;
|
|
148
229
|
let force = false;
|
|
149
230
|
for (let i = 0; i < rest.length; i++) {
|
|
150
231
|
const arg = rest[i];
|
|
151
232
|
if (arg === "--force") {
|
|
152
233
|
force = true;
|
|
153
|
-
} else if (
|
|
154
|
-
const value = rest[++i]
|
|
155
|
-
if (
|
|
156
|
-
|
|
234
|
+
} else if (arg === "--agent" || arg === "-a") {
|
|
235
|
+
const value = rest[++i];
|
|
236
|
+
if (!value) {
|
|
237
|
+
fail("Missing value for --agent.");
|
|
238
|
+
printCommandHelp("init");
|
|
239
|
+
process.exit(1);
|
|
157
240
|
}
|
|
241
|
+
if (!AGENTS.has(value as AgentId)) {
|
|
242
|
+
fail(formatUnknownAgent(value));
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
agent = value as AgentId;
|
|
158
246
|
}
|
|
159
247
|
}
|
|
248
|
+
if (!agent) {
|
|
249
|
+
fail("miru init requires --agent.");
|
|
250
|
+
printCommandHelp("init");
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
160
253
|
await runInit(agent, force);
|
|
161
254
|
return;
|
|
162
255
|
}
|
|
@@ -177,7 +270,7 @@ async function runCli(argv: string[]): Promise<void> {
|
|
|
177
270
|
}
|
|
178
271
|
if (clear) {
|
|
179
272
|
if (apiKey) {
|
|
180
|
-
|
|
273
|
+
fail("miru setup --clear cannot be combined with --key.");
|
|
181
274
|
process.exit(1);
|
|
182
275
|
}
|
|
183
276
|
await runClearCredentials();
|
|
@@ -193,17 +286,18 @@ async function runCli(argv: string[]): Promise<void> {
|
|
|
193
286
|
return;
|
|
194
287
|
}
|
|
195
288
|
|
|
196
|
-
const {
|
|
289
|
+
const { present: jsonFlag, rest: jsonRest } = parseFlagArgv(rest, "--json");
|
|
290
|
+
const { content, rest: contentRest } = parseContentArgv(jsonRest);
|
|
197
291
|
const { topK, rest: sizedRest } = parseTopK(contentRest);
|
|
198
292
|
|
|
199
293
|
if (command === "search") {
|
|
200
294
|
const query = sizedRest[0];
|
|
201
295
|
if (!query) {
|
|
202
|
-
|
|
296
|
+
printCommandHelp("search");
|
|
203
297
|
process.exit(1);
|
|
204
298
|
}
|
|
205
299
|
const path = resolve(sizedRest[1] ?? process.cwd());
|
|
206
|
-
await runSearch(path, query, topK, content);
|
|
300
|
+
await runSearch(path, query, topK, content, jsonFlag);
|
|
207
301
|
return;
|
|
208
302
|
}
|
|
209
303
|
|
|
@@ -211,16 +305,25 @@ async function runCli(argv: string[]): Promise<void> {
|
|
|
211
305
|
const filePath = sizedRest[0];
|
|
212
306
|
const lineRaw = sizedRest[1];
|
|
213
307
|
if (!filePath || !lineRaw) {
|
|
214
|
-
|
|
308
|
+
printCommandHelp("find-related");
|
|
215
309
|
process.exit(1);
|
|
216
310
|
}
|
|
217
311
|
const line = Number(lineRaw);
|
|
218
312
|
const path = resolve(sizedRest[2] ?? process.cwd());
|
|
219
|
-
|
|
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
|
+
}
|
|
220
322
|
return;
|
|
221
323
|
}
|
|
222
324
|
|
|
223
|
-
|
|
325
|
+
fail(`Unknown command: ${command}`);
|
|
326
|
+
printMainHelp();
|
|
224
327
|
process.exit(1);
|
|
225
328
|
}
|
|
226
329
|
|
|
@@ -270,6 +373,6 @@ async function main(): Promise<void> {
|
|
|
270
373
|
}
|
|
271
374
|
|
|
272
375
|
main().catch((err) => {
|
|
273
|
-
|
|
376
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
274
377
|
process.exit(1);
|
|
275
378
|
});
|
package/src/credentials.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { hasTakaraApiKeyInEnv } from "./env.ts";
|
|
4
4
|
|
|
5
5
|
const CREDENTIALS_FILENAME = "credentials.json";
|
|
6
6
|
const CREDENTIALS_VERSION = 1;
|
|
@@ -31,10 +31,6 @@ export function resolveCredentialsPath(): string {
|
|
|
31
31
|
return join(resolveCredentialsDir(), CREDENTIALS_FILENAME);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export function hasEmbeddingApiKeyInEnv(): boolean {
|
|
35
|
-
return Boolean(envFirstString([...EMBEDDING_API_KEY_NAMES], ""));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
34
|
export async function readStoredCredentials(): Promise<StoredCredentials | null> {
|
|
39
35
|
const path = resolveCredentialsPath();
|
|
40
36
|
if (!(await Bun.file(path).exists())) {
|
|
@@ -53,7 +49,7 @@ export async function readStoredCredentials(): Promise<StoredCredentials | null>
|
|
|
53
49
|
|
|
54
50
|
/** Set TAKARA_API_KEY from the user credentials file when env is unset. */
|
|
55
51
|
export async function loadStoredCredentials(): Promise<boolean> {
|
|
56
|
-
if (
|
|
52
|
+
if (hasTakaraApiKeyInEnv()) {
|
|
57
53
|
return false;
|
|
58
54
|
}
|
|
59
55
|
const stored = await readStoredCredentials();
|
package/src/env.ts
CHANGED
|
@@ -31,21 +31,17 @@ export function envOptionalInt(names: string[], min = 1): number | undefined {
|
|
|
31
31
|
return undefined;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
const
|
|
35
|
-
"TAKARA_API_KEY",
|
|
36
|
-
"OPENAI_API_KEY",
|
|
37
|
-
"MIRU_OPENAI_API_KEY",
|
|
38
|
-
"SEMBLE_OPENAI_API_KEY",
|
|
39
|
-
] as const;
|
|
34
|
+
export const TAKARA_API_KEY_ENV = "TAKARA_API_KEY";
|
|
40
35
|
|
|
41
|
-
export
|
|
36
|
+
export function hasTakaraApiKeyInEnv(): boolean {
|
|
37
|
+
return Boolean(process.env[TAKARA_API_KEY_ENV]?.trim());
|
|
38
|
+
}
|
|
42
39
|
|
|
43
40
|
export function resolveEmbeddingApiKey(): string {
|
|
44
|
-
const key =
|
|
41
|
+
const key = process.env[TAKARA_API_KEY_ENV]?.trim() ?? "";
|
|
45
42
|
if (!key) {
|
|
46
43
|
throw new Error(
|
|
47
|
-
"
|
|
48
|
-
"MIRU_OPENAI_API_KEY, or SEMBLE_OPENAI_API_KEY in your MCP server env or .env.local.",
|
|
44
|
+
"Takara API key required. Run `miru setup`, or set TAKARA_API_KEY in your MCP server env or .env.local.",
|
|
49
45
|
);
|
|
50
46
|
}
|
|
51
47
|
return key;
|