@takara-ai/miru-code 0.1.3 → 0.1.4

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/setup.ts ADDED
@@ -0,0 +1,168 @@
1
+ import { stdin as input, stdout as output } from "node:process";
2
+ import {
3
+ clearStoredCredentials,
4
+ hasEmbeddingApiKeyInEnv,
5
+ loadStoredCredentials,
6
+ readStoredCredentials,
7
+ resolveCredentialsPath,
8
+ saveStoredCredentials,
9
+ } from "./credentials.ts";
10
+ import { validateEmbeddingApiKey } from "./embeddings/validate.ts";
11
+ import { resolveEmbeddingApiKey } from "./env.ts";
12
+ import { Spinner } from "./spinner.ts";
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
+ async function promptApiKey(): Promise<string> {
64
+ let key = "";
65
+ while (!key) {
66
+ key = await promptHidden("Takara API key (input hidden): ");
67
+ if (!key) {
68
+ output.write("API key cannot be empty.\n");
69
+ }
70
+ }
71
+ return key;
72
+ }
73
+
74
+ export interface RunSetupOptions {
75
+ apiKey?: string;
76
+ force?: boolean;
77
+ skipValidation?: boolean;
78
+ }
79
+
80
+ export async function runSetup(options: RunSetupOptions = {}): Promise<string> {
81
+ if (!options.force && hasEmbeddingApiKeyInEnv()) {
82
+ const path = resolveCredentialsPath();
83
+ const stored = await readStoredCredentials();
84
+ if (stored) {
85
+ process.stderr.write(
86
+ `API key already configured (env var and ${path}). Use --force to replace the stored key.\n`,
87
+ );
88
+ return path;
89
+ }
90
+ process.stderr.write(
91
+ "API key already set via environment variable. Stored credentials were not changed.\n",
92
+ );
93
+ return resolveCredentialsPath();
94
+ }
95
+
96
+ if (!options.force) {
97
+ const stored = await readStoredCredentials();
98
+ if (stored && !options.apiKey) {
99
+ process.stderr.write(
100
+ `API key already stored at ${resolveCredentialsPath()}. Use --force to replace it.\n`,
101
+ );
102
+ process.env.TAKARA_API_KEY = stored.takara_api_key;
103
+ return resolveCredentialsPath();
104
+ }
105
+ }
106
+
107
+ process.stderr.write("\nMiru (見る) — first-time setup\n\n");
108
+ process.stderr.write(
109
+ "Miru needs a Takara API key for code embeddings.\n" +
110
+ "Get a bearer token from Takara, then enter it below.\n\n",
111
+ );
112
+
113
+ const apiKey = options.apiKey ?? (await promptApiKey());
114
+
115
+ if (!options.skipValidation) {
116
+ const spinner = new Spinner("Validating API key");
117
+ spinner.start();
118
+ const result = await validateEmbeddingApiKey({ apiKey });
119
+ if (!result.valid) {
120
+ spinner.fail(result.message);
121
+ throw new Error(result.message);
122
+ }
123
+ spinner.succeed("API key validated");
124
+ }
125
+
126
+ const path = await saveStoredCredentials(apiKey);
127
+ process.env.TAKARA_API_KEY = apiKey;
128
+ process.stderr.write(`\nSaved credentials to ${path} (readable only by your user account).\n`);
129
+ process.stderr.write("You can also set TAKARA_API_KEY in MCP config for IDE integrations.\n\n");
130
+ return path;
131
+ }
132
+
133
+ export async function runClearCredentials(): Promise<void> {
134
+ const { cleared, path } = await clearStoredCredentials();
135
+ if (cleared) {
136
+ process.stderr.write(`Removed stored API key from ${path}.\n`);
137
+ return;
138
+ }
139
+ process.stderr.write(`No stored API key at ${path}.\n`);
140
+ }
141
+
142
+ export async function ensureCredentials(options?: { interactive?: boolean }): Promise<void> {
143
+ try {
144
+ resolveEmbeddingApiKey();
145
+ return;
146
+ } catch {
147
+ await loadStoredCredentials();
148
+ }
149
+
150
+ try {
151
+ resolveEmbeddingApiKey();
152
+ return;
153
+ } catch {
154
+ // still missing
155
+ }
156
+
157
+ const interactive = options?.interactive ?? process.stdin.isTTY;
158
+ if (interactive) {
159
+ await runSetup();
160
+ resolveEmbeddingApiKey();
161
+ return;
162
+ }
163
+
164
+ throw new Error(
165
+ "Embedding API key required. Run `miru setup` in a terminal, or set TAKARA_API_KEY " +
166
+ "in your MCP server env (Cursor mcp.json) or .env.local.",
167
+ );
168
+ }
package/src/spinner.ts ADDED
@@ -0,0 +1,57 @@
1
+ const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
2
+
3
+ export class Spinner {
4
+ private timer: ReturnType<typeof setInterval> | null = null;
5
+ private frame = 0;
6
+
7
+ constructor(private readonly message: string) {}
8
+
9
+ start(): void {
10
+ if (!process.stderr.isTTY) {
11
+ process.stderr.write(`${this.message}...\n`);
12
+ return;
13
+ }
14
+ this.timer = setInterval(() => {
15
+ const glyph = FRAMES[this.frame++ % FRAMES.length];
16
+ process.stderr.write(`\r${glyph} ${this.message}`);
17
+ }, 80);
18
+ }
19
+
20
+ stop(finalMessage?: string): void {
21
+ if (this.timer) {
22
+ clearInterval(this.timer);
23
+ this.timer = null;
24
+ }
25
+ if (process.stderr.isTTY) {
26
+ process.stderr.write("\r\x1b[K");
27
+ }
28
+ if (finalMessage) {
29
+ process.stderr.write(`${finalMessage}\n`);
30
+ }
31
+ }
32
+
33
+ succeed(message?: string): void {
34
+ this.stop(message ?? `${this.message} — ok`);
35
+ }
36
+
37
+ fail(message?: string): void {
38
+ this.stop(message ?? `${this.message} — failed`);
39
+ }
40
+ }
41
+
42
+ export async function withSpinner<T>(
43
+ message: string,
44
+ fn: () => Promise<T>,
45
+ options?: { successMessage?: string; failMessage?: string },
46
+ ): Promise<T> {
47
+ const spinner = new Spinner(message);
48
+ spinner.start();
49
+ try {
50
+ const result = await fn();
51
+ spinner.succeed(options?.successMessage);
52
+ return result;
53
+ } catch (err) {
54
+ spinner.fail(options?.failMessage);
55
+ throw err;
56
+ }
57
+ }