@takara-ai/miru-code 0.2.0 → 0.3.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 CHANGED
@@ -60,7 +60,7 @@ Add credentials to `.env.local` (loaded automatically when you run from this rep
60
60
 
61
61
  | Variable | Default |
62
62
  |----------|---------|
63
- | `TAKARA_API_KEY` / `OPENAI_API_KEY` / `MIRU_OPENAI_API_KEY` | (required) Bearer token for Takara |
63
+ | `TAKARA_API_KEY` | (required) Takara bearer token for embeddings |
64
64
  | `MIRU_OPENAI_BASE_URL` | `https://infer.dev.takara.ai/v1` |
65
65
  | `MIRU_OPENAI_EMBEDDING_MODEL` | `ds1-potion-code-16m` |
66
66
  | `MIRU_EMBEDDING_DIMENSIONS` | `256` (auto for potion) |
@@ -68,12 +68,8 @@ Add credentials to `.env.local` (loaded automatically when you run from this rep
68
68
  | `MIRU_EMBEDDING_BATCH_SIZE` | `32` (max for this model) |
69
69
  | `MIRU_CONCURRENCY` | logical CPUs **minus 2** (min 1) |
70
70
 
71
- `SEMBLE_*` env names are still accepted as aliases for migration.
72
-
73
71
  Parallelism: file reads/chunking, embedding batches (up to concurrency in flight), query embed overlapping BM25, and **BM25 scoring in Bun workers** (shards by doc range when index has 256+ chunks). BM25 uses an inverted postings list for fast single-threaded search on small indexes. Override with `MIRU_CONCURRENCY`.
74
72
 
75
- `OPENAI_*` env aliases are also accepted where noted in the CLI help.
76
-
77
73
  ## IDE integration
78
74
 
79
75
  Miru supports six agent platforms. For each, you can wire the **MCP server** (recommended) and/or install a **CLI sub-agent** with `miru init`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takara-ai/miru-code",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Miru (見る) — hybrid code search for agents. Bun, TypeScript, Takara embeddings.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/cli.ts CHANGED
@@ -4,6 +4,13 @@ import { type AgentId, writeAgentFile } from "./agents.ts";
4
4
  import { clearCache } from "./cache.ts";
5
5
  import { loadStoredCredentials } from "./credentials.ts";
6
6
  import { loadEnvFiles } from "./env-files.ts";
7
+ import {
8
+ AGENT_IDS,
9
+ formatUnknownAgent,
10
+ printCommandHelp,
11
+ printFullHelp,
12
+ printMainHelp,
13
+ } from "./help.ts";
7
14
  import { serveMcp } from "./mcp/serve.ts";
8
15
  import { MiruIndex } from "./miru-index.ts";
9
16
  import { ensureCredentials, runClearCredentials, runSetup } from "./setup.ts";
@@ -13,9 +20,18 @@ import { formatResults, resolveChunk, resolveContent } from "./utils.ts";
13
20
  loadEnvFiles();
14
21
  await loadStoredCredentials();
15
22
 
16
- const CLI_COMMANDS = new Set(["search", "find-related", "init", "setup", "clear", "-h", "--help"]);
23
+ const CLI_COMMANDS = new Set([
24
+ "search",
25
+ "find-related",
26
+ "init",
27
+ "setup",
28
+ "clear",
29
+ "help",
30
+ "-h",
31
+ "--help",
32
+ ]);
17
33
 
18
- const AGENTS = new Set<AgentId>(["claude", "copilot", "cursor", "gemini", "kiro", "opencode"]);
34
+ const AGENTS = new Set<AgentId>(AGENT_IDS);
19
35
 
20
36
  function parseContentArgv(argv: string[]): { content: ContentType[]; rest: string[] } {
21
37
  const rest: string[] = [];
@@ -101,8 +117,15 @@ async function runFindRelated(
101
117
  }
102
118
 
103
119
  async function runInit(agent: AgentId, force: boolean): Promise<void> {
104
- const dest = await writeAgentFile(agent, { force });
105
- console.log(`Created ${dest}`);
120
+ try {
121
+ const dest = await writeAgentFile(agent, { force });
122
+ process.stdout.write(`Wrote sub-agent: ${dest}\n`);
123
+ } catch (err) {
124
+ 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");
127
+ process.exit(1);
128
+ }
106
129
  }
107
130
 
108
131
  async function runClear(path: string): Promise<void> {
@@ -110,53 +133,55 @@ async function runClear(path: string): Promise<void> {
110
133
  console.log(`Cleared cached index for: ${path}`);
111
134
  }
112
135
 
113
- function printHelp(): void {
114
- console.log(`Miru (見る) — hybrid code search (Bun + TypeScript + Takara)
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.
127
-
128
- Examples:
129
- miru search "auth middleware" ./src
130
- miru find-related src/auth.ts 42 .
131
-
132
- Environment:
133
- TAKARA_API_KEY / OPENAI_API_KEY / MIRU_OPENAI_API_KEY Takara bearer token
134
- MIRU_OPENAI_BASE_URL Default: infer.dev.takara.ai
135
- `);
136
- }
137
-
138
136
  async function runCli(argv: string[]): Promise<void> {
139
137
  const [command, ...rest] = argv;
140
138
 
141
- if (command === "-h" || command === "--help" || command === undefined) {
142
- printHelp();
139
+ if (command === undefined) {
140
+ printMainHelp();
141
+ return;
142
+ }
143
+
144
+ if (command === "-h" || command === "--help") {
145
+ printFullHelp();
146
+ return;
147
+ }
148
+
149
+ if (command === "help") {
150
+ const topic = rest[0];
151
+ if (!topic) {
152
+ printMainHelp();
153
+ return;
154
+ }
155
+ printCommandHelp(topic);
143
156
  return;
144
157
  }
145
158
 
146
159
  if (command === "init") {
147
- let agent: AgentId = "claude";
160
+ let agent: AgentId | undefined;
148
161
  let force = false;
149
162
  for (let i = 0; i < rest.length; i++) {
150
163
  const arg = rest[i];
151
164
  if (arg === "--force") {
152
165
  force = true;
153
- } else if ((arg === "--agent" || arg === "-a") && rest[i + 1]) {
154
- const value = rest[++i] as AgentId;
155
- if (AGENTS.has(value)) {
156
- agent = value;
166
+ } else if (arg === "--agent" || arg === "-a") {
167
+ const value = rest[++i];
168
+ if (!value) {
169
+ process.stderr.write("Missing value for --agent.\n");
170
+ printCommandHelp("init");
171
+ process.exit(1);
172
+ }
173
+ if (!AGENTS.has(value as AgentId)) {
174
+ process.stderr.write(`${formatUnknownAgent(value)}\n`);
175
+ process.exit(1);
157
176
  }
177
+ agent = value as AgentId;
158
178
  }
159
179
  }
180
+ if (!agent) {
181
+ process.stderr.write("miru init requires --agent.\n");
182
+ printCommandHelp("init");
183
+ process.exit(1);
184
+ }
160
185
  await runInit(agent, force);
161
186
  return;
162
187
  }
@@ -199,7 +224,7 @@ async function runCli(argv: string[]): Promise<void> {
199
224
  if (command === "search") {
200
225
  const query = sizedRest[0];
201
226
  if (!query) {
202
- console.error("Usage: miru search <query> [path] [-k N] [--content ...]");
227
+ printCommandHelp("search");
203
228
  process.exit(1);
204
229
  }
205
230
  const path = resolve(sizedRest[1] ?? process.cwd());
@@ -211,7 +236,7 @@ async function runCli(argv: string[]): Promise<void> {
211
236
  const filePath = sizedRest[0];
212
237
  const lineRaw = sizedRest[1];
213
238
  if (!filePath || !lineRaw) {
214
- console.error("Usage: miru find-related <file_path> <line> [path] [-k N] [--content ...]");
239
+ printCommandHelp("find-related");
215
240
  process.exit(1);
216
241
  }
217
242
  const line = Number(lineRaw);
@@ -220,7 +245,8 @@ async function runCli(argv: string[]): Promise<void> {
220
245
  return;
221
246
  }
222
247
 
223
- printHelp();
248
+ process.stderr.write(`Unknown command: ${command}\n`);
249
+ printMainHelp();
224
250
  process.exit(1);
225
251
  }
226
252
 
@@ -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 { EMBEDDING_API_KEY_NAMES, envFirstString } from "./env.ts";
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 (hasEmbeddingApiKeyInEnv()) {
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 EMBEDDING_API_KEY_NAMES = [
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 { EMBEDDING_API_KEY_NAMES };
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 = envFirstString([...EMBEDDING_API_KEY_NAMES], "");
41
+ const key = process.env[TAKARA_API_KEY_ENV]?.trim() ?? "";
45
42
  if (!key) {
46
43
  throw new Error(
47
- "Embedding API key required. Run `miru setup`, set TAKARA_API_KEY, OPENAI_API_KEY, " +
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;
package/src/help.ts ADDED
@@ -0,0 +1,175 @@
1
+ import type { AgentId } from "./agents.ts";
2
+
3
+ export const AGENT_IDS: readonly AgentId[] = [
4
+ "claude",
5
+ "copilot",
6
+ "cursor",
7
+ "gemini",
8
+ "kiro",
9
+ "opencode",
10
+ ] as const;
11
+
12
+ const AGENT_LIST = AGENT_IDS.join(", ");
13
+
14
+ export function printMainHelp(): void {
15
+ const lines = [
16
+ "",
17
+ "Miru (見る) — hybrid code search for agents",
18
+ "",
19
+ "Usage:",
20
+ " miru Start MCP server (stdio)",
21
+ " miru <command> [options]",
22
+ "",
23
+ "Commands:",
24
+ " search Hybrid search over a codebase",
25
+ " find-related Find chunks related to a file:line",
26
+ " setup Save your Takara API key locally",
27
+ " init Write a miru-code sub-agent for your IDE",
28
+ " clear Remove cached index for a path",
29
+ "",
30
+ "Quick start:",
31
+ " miru setup",
32
+ " miru init --agent cursor",
33
+ ' miru search "auth middleware" ./src',
34
+ "",
35
+ "Run miru help <command> for details.",
36
+ "Run miru -h for environment variables.",
37
+ "",
38
+ ];
39
+ process.stdout.write(`${lines.join("\n")}\n`);
40
+ }
41
+
42
+ export function printEnvHelp(): void {
43
+ const lines = [
44
+ "",
45
+ "Environment:",
46
+ " TAKARA_API_KEY",
47
+ " Takara bearer token for embeddings",
48
+ " MIRU_OPENAI_BASE_URL",
49
+ " Default: infer.dev.takara.ai",
50
+ "",
51
+ ];
52
+ process.stdout.write(`${lines.join("\n")}\n`);
53
+ }
54
+
55
+ export function printFullHelp(): void {
56
+ printMainHelp();
57
+ printEnvHelp();
58
+ }
59
+
60
+ export function printCommandHelp(command: string): void {
61
+ switch (command) {
62
+ case "search":
63
+ process.stdout.write(
64
+ [
65
+ "",
66
+ "miru search — hybrid search",
67
+ "",
68
+ "Usage:",
69
+ " miru search <query> [path] [-k N] [--content TYPE ...]",
70
+ "",
71
+ "Options:",
72
+ " -k, --top-k N Number of results (default: 5)",
73
+ " --content TYPE code | docs | config | all (default: code)",
74
+ "",
75
+ "Example:",
76
+ ' miru search "where is auth" ./src -k 10 --content code docs',
77
+ "",
78
+ ].join("\n"),
79
+ );
80
+ return;
81
+ case "find-related":
82
+ process.stdout.write(
83
+ [
84
+ "",
85
+ "miru find-related — related chunks",
86
+ "",
87
+ "Usage:",
88
+ " miru find-related <file> <line> [path] [-k N] [--content TYPE ...]",
89
+ "",
90
+ "Example:",
91
+ " miru find-related src/auth.ts 42 . -k 8",
92
+ "",
93
+ ].join("\n"),
94
+ );
95
+ return;
96
+ case "setup":
97
+ process.stdout.write(
98
+ [
99
+ "",
100
+ "miru setup — store API key",
101
+ "",
102
+ "Usage:",
103
+ " miru setup",
104
+ " miru setup --key TOKEN",
105
+ " miru setup --force",
106
+ " miru setup --clear",
107
+ "",
108
+ "Options:",
109
+ " --key, -k TOKEN Provide key on the command line (non-interactive)",
110
+ " --force Replace an existing stored key",
111
+ " --clear Remove stored credentials",
112
+ "",
113
+ ].join("\n"),
114
+ );
115
+ return;
116
+ case "init":
117
+ process.stdout.write(
118
+ [
119
+ "",
120
+ "miru init — IDE sub-agent file",
121
+ "",
122
+ "Usage:",
123
+ " miru init --agent AGENT [--force]",
124
+ "",
125
+ "Agents:",
126
+ ` ${AGENT_LIST}`,
127
+ "",
128
+ "Examples:",
129
+ " miru init --agent cursor",
130
+ " miru init --agent claude --force",
131
+ "",
132
+ ].join("\n"),
133
+ );
134
+ return;
135
+ case "clear":
136
+ process.stdout.write(
137
+ [
138
+ "",
139
+ "miru clear — drop cached index",
140
+ "",
141
+ "Usage:",
142
+ " miru clear [path]",
143
+ "",
144
+ "Example:",
145
+ " miru clear .",
146
+ "",
147
+ ].join("\n"),
148
+ );
149
+ return;
150
+ case "mcp":
151
+ process.stdout.write(
152
+ [
153
+ "",
154
+ "miru — MCP server (default with no subcommand)",
155
+ "",
156
+ "Usage:",
157
+ " miru [--ref BRANCH] [--content TYPE ...]",
158
+ "",
159
+ "Indexes a repo on the first search/find_related tool call (repo argument).",
160
+ "With no path, uses the process working directory.",
161
+ "",
162
+ ].join("\n"),
163
+ );
164
+ return;
165
+ default:
166
+ process.stderr.write(`Unknown command: ${command}\n\n`);
167
+ printMainHelp();
168
+ process.exit(1);
169
+ }
170
+ process.stdout.write("\n");
171
+ }
172
+
173
+ export function formatUnknownAgent(agent: string): string {
174
+ return `Unknown agent "${agent}". Choose one of: ${AGENT_LIST}`;
175
+ }
package/src/prompt.ts ADDED
@@ -0,0 +1,111 @@
1
+ import { stdin as input, stdout as output } from "node:process";
2
+
3
+ export interface HiddenPromptState {
4
+ value: string;
5
+ escapeBuffer: string;
6
+ }
7
+
8
+ export function createHiddenPromptState(): HiddenPromptState {
9
+ return { value: "", escapeBuffer: "" };
10
+ }
11
+
12
+ /** Apply one UTF-16 code unit from raw stdin; returns terminal echo bytes (may be empty). */
13
+ export function applyHiddenPromptChar(
14
+ state: HiddenPromptState,
15
+ char: string,
16
+ ): { state: HiddenPromptState; echo: string; submit?: boolean; cancel?: boolean } {
17
+ if (char === "\x1b") {
18
+ return { state: { ...state, escapeBuffer: "\x1b" }, echo: "" };
19
+ }
20
+
21
+ if (state.escapeBuffer.length > 0) {
22
+ const escapeBuffer = state.escapeBuffer + char;
23
+ let done = false;
24
+ if (escapeBuffer.startsWith("\x1b[")) {
25
+ done = escapeBuffer.length > 2 && char >= "@" && char <= "~";
26
+ } else if (escapeBuffer.startsWith("\x1b")) {
27
+ done = escapeBuffer.length >= 2;
28
+ }
29
+ return {
30
+ state: { ...state, escapeBuffer: done ? "" : escapeBuffer },
31
+ echo: "",
32
+ };
33
+ }
34
+
35
+ if (char === "\n" || char === "\r" || char === "\u0004") {
36
+ return { state, echo: "", submit: true };
37
+ }
38
+
39
+ if (char === "\u0003") {
40
+ return { state, echo: "", cancel: true };
41
+ }
42
+
43
+ if (char === "\u007f" || char === "\b") {
44
+ if (state.value.length === 0) {
45
+ return { state, echo: "" };
46
+ }
47
+ return {
48
+ state: { ...state, value: state.value.slice(0, -1) },
49
+ echo: "\b \b",
50
+ };
51
+ }
52
+
53
+ if (char < " " || char === "\x7f") {
54
+ return { state, echo: "" };
55
+ }
56
+
57
+ return {
58
+ state: { ...state, value: state.value + char },
59
+ echo: "*",
60
+ };
61
+ }
62
+
63
+ export async function promptHidden(message: string): Promise<string> {
64
+ if (!input.isTTY) {
65
+ throw new Error(
66
+ "Cannot prompt for API key: stdin is not a TTY. Run `miru setup --key YOUR_KEY` or set TAKARA_API_KEY.",
67
+ );
68
+ }
69
+
70
+ return new Promise((resolve, reject) => {
71
+ output.write(message);
72
+ input.setRawMode?.(true);
73
+ input.resume();
74
+ input.setEncoding("utf8");
75
+
76
+ let state = createHiddenPromptState();
77
+
78
+ const onData = (chunk: string) => {
79
+ for (const char of chunk) {
80
+ const result = applyHiddenPromptChar(state, char);
81
+ state = result.state;
82
+
83
+ if (result.cancel) {
84
+ cleanup();
85
+ output.write("\n");
86
+ reject(new Error("Setup cancelled."));
87
+ process.exit(130);
88
+ }
89
+
90
+ if (result.submit) {
91
+ cleanup();
92
+ output.write("\n");
93
+ resolve(state.value.trim());
94
+ return;
95
+ }
96
+
97
+ if (result.echo) {
98
+ output.write(result.echo);
99
+ }
100
+ }
101
+ };
102
+
103
+ const cleanup = () => {
104
+ input.setRawMode?.(false);
105
+ input.pause();
106
+ input.removeListener("data", onData);
107
+ };
108
+
109
+ input.on("data", onData);
110
+ });
111
+ }
package/src/setup.ts CHANGED
@@ -1,65 +1,16 @@
1
- import { stdin as input, stdout as output } from "node:process";
1
+ import { stdout as output } from "node:process";
2
2
  import {
3
3
  clearStoredCredentials,
4
- hasEmbeddingApiKeyInEnv,
5
4
  loadStoredCredentials,
6
5
  readStoredCredentials,
7
6
  resolveCredentialsPath,
8
7
  saveStoredCredentials,
9
8
  } from "./credentials.ts";
10
9
  import { validateEmbeddingApiKey } from "./embeddings/validate.ts";
11
- import { resolveEmbeddingApiKey } from "./env.ts";
10
+ import { hasTakaraApiKeyInEnv, resolveEmbeddingApiKey } from "./env.ts";
11
+ import { promptHidden } from "./prompt.ts";
12
12
  import { Spinner } from "./spinner.ts";
13
13
 
14
- async function promptHidden(message: string): Promise<string> {
15
- if (!input.isTTY) {
16
- throw new Error(
17
- "Cannot prompt for API key: stdin is not a TTY. Run `miru setup --key YOUR_KEY` or set TAKARA_API_KEY.",
18
- );
19
- }
20
-
21
- return new Promise((resolve, reject) => {
22
- output.write(message);
23
- input.setRawMode?.(true);
24
- input.resume();
25
- input.setEncoding("utf8");
26
-
27
- let value = "";
28
- const onData = (chunk: string) => {
29
- const char = chunk[0];
30
- if (char === "\n" || char === "\r" || char === "\u0004") {
31
- cleanup();
32
- output.write("\n");
33
- resolve(value.trim());
34
- return;
35
- }
36
- if (char === "\u0003") {
37
- cleanup();
38
- output.write("\n");
39
- reject(new Error("Setup cancelled."));
40
- process.exit(130);
41
- }
42
- if (char === "\u007f" || char === "\b") {
43
- if (value.length > 0) {
44
- value = value.slice(0, -1);
45
- output.write("\b \b");
46
- }
47
- return;
48
- }
49
- value += chunk;
50
- output.write("*");
51
- };
52
-
53
- const cleanup = () => {
54
- input.setRawMode?.(false);
55
- input.pause();
56
- input.removeListener("data", onData);
57
- };
58
-
59
- input.on("data", onData);
60
- });
61
- }
62
-
63
14
  async function promptApiKey(): Promise<string> {
64
15
  let key = "";
65
16
  while (!key) {
@@ -78,7 +29,7 @@ export interface RunSetupOptions {
78
29
  }
79
30
 
80
31
  export async function runSetup(options: RunSetupOptions = {}): Promise<string> {
81
- if (!options.force && hasEmbeddingApiKeyInEnv()) {
32
+ if (!options.force && hasTakaraApiKeyInEnv()) {
82
33
  const path = resolveCredentialsPath();
83
34
  const stored = await readStoredCredentials();
84
35
  if (stored) {