@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 +1 -5
- package/package.json +1 -1
- package/src/cli.ts +65 -39
- package/src/credentials.ts +2 -6
- package/src/env.ts +6 -10
- package/src/help.ts +175 -0
- package/src/prompt.ts +111 -0
- package/src/setup.ts +4 -53
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`
|
|
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
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([
|
|
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>(
|
|
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
|
-
|
|
105
|
-
|
|
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 ===
|
|
142
|
-
|
|
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
|
|
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 (
|
|
154
|
-
const value = rest[++i]
|
|
155
|
-
if (
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
248
|
+
process.stderr.write(`Unknown command: ${command}\n`);
|
|
249
|
+
printMainHelp();
|
|
224
250
|
process.exit(1);
|
|
225
251
|
}
|
|
226
252
|
|
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;
|
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 {
|
|
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 &&
|
|
32
|
+
if (!options.force && hasTakaraApiKeyInEnv()) {
|
|
82
33
|
const path = resolveCredentialsPath();
|
|
83
34
|
const stored = await readStoredCredentials();
|
|
84
35
|
if (stored) {
|