@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/src/help.ts ADDED
@@ -0,0 +1,160 @@
1
+ import type { AgentId } from "./agents.ts";
2
+ import {
3
+ brandTitle,
4
+ commandRow,
5
+ divider,
6
+ fail,
7
+ header,
8
+ hint,
9
+ section,
10
+ writeStdout,
11
+ } from "./cli-ui.ts";
12
+
13
+ export const AGENT_IDS: readonly AgentId[] = [
14
+ "claude",
15
+ "copilot",
16
+ "cursor",
17
+ "gemini",
18
+ "kiro",
19
+ "opencode",
20
+ ] as const;
21
+
22
+ const AGENT_LIST = AGENT_IDS.join(", ");
23
+
24
+ export function printMainHelp(): void {
25
+ header("hybrid code search for agents");
26
+
27
+ section("Usage");
28
+ writeStdout(" miru Start MCP server (stdio)");
29
+ writeStdout(" miru <command> [options]");
30
+ divider();
31
+
32
+ section("Commands");
33
+ commandRow("search", "Hybrid search over a codebase");
34
+ commandRow("find-related", "Find chunks related to a file:line");
35
+ commandRow("setup", "Save your Takara API key locally");
36
+ commandRow("install", "Configure miru across coding agents");
37
+ commandRow("uninstall", "Remove miru agent configuration");
38
+ commandRow("init", "Write a project-local sub-agent file");
39
+ commandRow("clear", "Remove cached index for a path");
40
+ commandRow("help", "Show help for a command");
41
+ divider();
42
+
43
+ section("Quick start");
44
+ writeStdout(" miru setup && miru install");
45
+ writeStdout(' miru search "auth middleware" ./src');
46
+ writeStdout("");
47
+ hint("miru help <command> · miru -h for environment variables");
48
+ writeStdout("");
49
+ }
50
+
51
+ export function printEnvHelp(): void {
52
+ section("Environment");
53
+ writeStdout(" TAKARA_API_KEY");
54
+ writeStdout(" Takara bearer token for embeddings");
55
+ writeStdout(" MIRU_OPENAI_BASE_URL");
56
+ writeStdout(" Default: https://infer.dev.takara.ai/v1");
57
+ writeStdout(" MIRU_CONCURRENCY");
58
+ writeStdout(" Parallel workers (default: CPUs − 2)");
59
+ writeStdout("");
60
+ }
61
+
62
+ export function printFullHelp(): void {
63
+ printMainHelp();
64
+ printEnvHelp();
65
+ }
66
+
67
+ function printCommandHeader(name: string, summary: string): void {
68
+ writeStdout("");
69
+ writeStdout(`${brandTitle()} ${name}`);
70
+ writeStdout(summary);
71
+ divider();
72
+ }
73
+
74
+ export function printCommandHelp(command: string): void {
75
+ switch (command) {
76
+ case "search":
77
+ printCommandHeader("search", "Hybrid semantic + keyword search.");
78
+ section("Usage");
79
+ writeStdout(" miru search <query> [path] [options]");
80
+ section("Options");
81
+ writeStdout(" -k, --top-k N Number of results (default: 5)");
82
+ writeStdout(" --content TYPE code | docs | config | all");
83
+ writeStdout(" --json JSON output (default when piped)");
84
+ section("Example");
85
+ writeStdout(' miru search "where is auth" ./src -k 10 --content code docs');
86
+ writeStdout("");
87
+ return;
88
+ case "find-related":
89
+ printCommandHeader("find-related", "Semantic neighbors of a file location.");
90
+ section("Usage");
91
+ writeStdout(" miru find-related <file> <line> [path] [options]");
92
+ section("Example");
93
+ writeStdout(" miru find-related src/auth.ts 42 . -k 8");
94
+ writeStdout("");
95
+ return;
96
+ case "setup":
97
+ printCommandHeader("setup", "Store and validate your Takara API key.");
98
+ section("Usage");
99
+ writeStdout(" miru setup [--key TOKEN] [--force] [--clear]");
100
+ section("Options");
101
+ writeStdout(" --key, -k TOKEN Non-interactive key entry");
102
+ writeStdout(" --force Replace an existing stored key");
103
+ writeStdout(" --clear Remove stored credentials");
104
+ writeStdout("");
105
+ return;
106
+ case "install":
107
+ printCommandHeader("install", "Interactive global agent setup.");
108
+ writeStdout("Configures MCP server, instructions, and sub-agent files under");
109
+ writeStdout("your user config (~/.claude, ~/.cursor, etc.).");
110
+ writeStdout("");
111
+ writeStdout("Run miru setup first, or set TAKARA_API_KEY for MCP env expansion.");
112
+ writeStdout("");
113
+ return;
114
+ case "uninstall":
115
+ printCommandHeader("uninstall", "Remove miru configuration from agents.");
116
+ writeStdout("Removes MCP entries, marked instruction blocks, and global sub-agents.");
117
+ writeStdout("");
118
+ return;
119
+ case "init":
120
+ printCommandHeader("init", "Project-local sub-agent file.");
121
+ writeStdout("Prefer miru install for global setup. Use init to commit into a repo.");
122
+ section("Usage");
123
+ writeStdout(" miru init --agent AGENT [--force]");
124
+ section("Agents");
125
+ writeStdout(` ${AGENT_LIST}`);
126
+ section("Example");
127
+ writeStdout(" miru init --agent claude --force");
128
+ writeStdout("");
129
+ return;
130
+ case "clear":
131
+ printCommandHeader("clear", "Drop the on-disk index cache.");
132
+ section("Usage");
133
+ writeStdout(" miru clear [path]");
134
+ section("Example");
135
+ writeStdout(" miru clear .");
136
+ writeStdout("");
137
+ return;
138
+ case "mcp":
139
+ printCommandHeader("mcp", "Stdio MCP server (default with no subcommand).");
140
+ section("Usage");
141
+ writeStdout(" miru [--ref BRANCH] [--content TYPE ...]");
142
+ writeStdout("");
143
+ writeStdout("Indexes on the first search/find_related tool call (repo argument).");
144
+ writeStdout("");
145
+ return;
146
+ default:
147
+ failHelp(command);
148
+ }
149
+ }
150
+
151
+ function failHelp(command: string): never {
152
+ fail(`Unknown command: ${command}`);
153
+ writeStdout("");
154
+ printMainHelp();
155
+ process.exit(1);
156
+ }
157
+
158
+ export function formatUnknownAgent(agent: string): string {
159
+ return `Unknown agent "${agent}". Choose one of: ${AGENT_LIST}`;
160
+ }
@@ -0,0 +1,246 @@
1
+ import { existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import type { AgentId } from "../agents.ts";
5
+
6
+ export type InstallAction =
7
+ | "created"
8
+ | "updated"
9
+ | "unchanged"
10
+ | "not-found"
11
+ | "removed"
12
+ | "error"
13
+ | "skipped";
14
+
15
+ export type InstallMode = "install" | "uninstall";
16
+
17
+ export const MIRU_START = "<!-- miru:start -->";
18
+ export const MIRU_END = "<!-- miru:end -->";
19
+
20
+ const HOME = homedir();
21
+
22
+ const TAKARA_KEY_PLACEHOLDER = "$" + "{TAKARA_API_KEY}";
23
+
24
+ const MCP_ENV: Record<string, string> = {
25
+ TAKARA_API_KEY: TAKARA_KEY_PLACEHOLDER,
26
+ };
27
+
28
+ const STDIO_SERVER_CONFIG: Record<string, unknown> = {
29
+ command: "bunx",
30
+ args: ["@takara-ai/miru-code"],
31
+ type: "stdio",
32
+ env: MCP_ENV,
33
+ };
34
+
35
+ const BARE_STDIO_SERVER_CONFIG: Record<string, unknown> = {
36
+ command: "bunx",
37
+ args: ["@takara-ai/miru-code"],
38
+ env: MCP_ENV,
39
+ };
40
+
41
+ const OPENCODE_SERVER_CONFIG: Record<string, unknown> = {
42
+ command: ["bunx", "@takara-ai/miru-code"],
43
+ type: "local",
44
+ enabled: true,
45
+ environment: MCP_ENV,
46
+ };
47
+
48
+ export const INSTRUCTIONS = `${MIRU_START}
49
+ ## Miru Code Search
50
+
51
+ A \`miru\` MCP server is available with two tools:
52
+ - \`search\` — search the codebase with a natural-language or code query.
53
+ - \`find_related\` — find code similar to a specific file and line.
54
+
55
+ Always call \`search\` before using Grep, Glob, or Read to explore the codebase. Use Grep/Glob/Read only for exact path lookup, exhaustive literal matches, or when the returned chunk lacks enough context.
56
+
57
+ Set \`TAKARA_API_KEY\` in your MCP config (or run \`miru setup\` for CLI use). MCP does not inherit shell env reliably.
58
+
59
+ Pass \`--content docs\` to search documentation, \`--content config\` for config files, or \`--content all\` for everything.
60
+
61
+ For CLI fallback or sub-agents without MCP access:
62
+
63
+ \`\`\`bash
64
+ miru search "authentication flow" ./my-project
65
+ miru search "deployment guide" ./my-project --content docs
66
+ miru find-related src/auth.ts 42 ./my-project
67
+ miru search "save model to disk" ./my-project --top-k 10
68
+ \`\`\`
69
+
70
+ First run builds a disk cache. The MCP server updates local indexes while it runs; after CLI-only use or large refactors, run \`miru clear <path>\`.
71
+
72
+ If \`miru\` is not on \`$PATH\`, use \`bunx @takara-ai/miru-code\` in its place.
73
+
74
+ ### Workflow
75
+
76
+ 1. Start with \`search\` (MCP) or \`miru search\` (CLI) to find relevant chunks.
77
+ 2. Use \`--content docs\` / \`--content config\` / \`--content all\` when appropriate.
78
+ 3. Inspect full files only when the chunk is not enough.
79
+ 4. Optionally use \`find_related\` with a hit's \`file_path\` and \`line\`.
80
+ 5. Use Grep only for exhaustive literal matches.
81
+ ${MIRU_END}
82
+ `;
83
+
84
+ export type McpConfigFormat = "json" | "toml";
85
+
86
+ export interface McpConfig {
87
+ path: string;
88
+ key: string;
89
+ memberKey: string;
90
+ entry: Record<string, unknown>;
91
+ format: McpConfigFormat;
92
+ }
93
+
94
+ export interface AgentTarget {
95
+ id: AgentId | "codex" | "vscode";
96
+ displayName: string;
97
+ binary: string | null;
98
+ configDir: string | null;
99
+ mcp: McpConfig | null;
100
+ instructionsPath: string | null;
101
+ subagentPath: string | null;
102
+ subagentId: AgentId | null;
103
+ }
104
+
105
+ export function opencodeMcpPath(): string {
106
+ const xdg = process.env.XDG_CONFIG_HOME;
107
+ const base = xdg ? join(xdg, "opencode") : join(HOME, ".config", "opencode");
108
+ const jsonc = join(base, "opencode.jsonc");
109
+ const json = join(base, "opencode.json");
110
+ if (existsSync(jsonc)) {
111
+ return jsonc;
112
+ }
113
+ if (existsSync(json)) {
114
+ return json;
115
+ }
116
+ return jsonc;
117
+ }
118
+
119
+ export function vscodeMcpPath(): string {
120
+ if (process.platform === "darwin") {
121
+ return join(HOME, "Library", "Application Support", "Code", "User", "mcp.json");
122
+ }
123
+ if (process.platform === "win32") {
124
+ const appData = process.env.APPDATA ?? HOME;
125
+ return join(appData, "Code", "User", "mcp.json");
126
+ }
127
+ const xdg = process.env.XDG_CONFIG_HOME ?? join(HOME, ".config");
128
+ return join(xdg, "Code", "User", "mcp.json");
129
+ }
130
+
131
+ function jsonMcp(path: string, key: string, entry: Record<string, unknown>): McpConfig {
132
+ return { path, key, memberKey: "miru", entry, format: "json" };
133
+ }
134
+
135
+ export const AGENT_TARGETS: AgentTarget[] = [
136
+ {
137
+ id: "claude",
138
+ displayName: "Claude Code",
139
+ binary: "claude",
140
+ configDir: join(HOME, ".claude"),
141
+ mcp: jsonMcp(join(HOME, ".claude.json"), "mcpServers", STDIO_SERVER_CONFIG),
142
+ instructionsPath: join(HOME, ".claude", "CLAUDE.md"),
143
+ subagentPath: join(HOME, ".claude", "agents", "miru-code.md"),
144
+ subagentId: "claude",
145
+ },
146
+ {
147
+ id: "cursor",
148
+ displayName: "Cursor",
149
+ binary: "cursor",
150
+ configDir: join(HOME, ".cursor"),
151
+ mcp: jsonMcp(join(HOME, ".cursor", "mcp.json"), "mcpServers", STDIO_SERVER_CONFIG),
152
+ instructionsPath: null,
153
+ subagentPath: join(HOME, ".cursor", "agents", "miru-code.md"),
154
+ subagentId: "cursor",
155
+ },
156
+ {
157
+ id: "gemini",
158
+ displayName: "Gemini CLI",
159
+ binary: "gemini",
160
+ configDir: join(HOME, ".gemini"),
161
+ mcp: jsonMcp(join(HOME, ".gemini", "settings.json"), "mcpServers", STDIO_SERVER_CONFIG),
162
+ instructionsPath: join(HOME, ".gemini", "GEMINI.md"),
163
+ subagentPath: join(HOME, ".gemini", "agents", "miru-code.md"),
164
+ subagentId: "gemini",
165
+ },
166
+ {
167
+ id: "kiro",
168
+ displayName: "Kiro",
169
+ binary: "kiro",
170
+ configDir: join(HOME, ".kiro"),
171
+ mcp: jsonMcp(join(HOME, ".kiro", "settings", "mcp.json"), "mcpServers", {
172
+ ...STDIO_SERVER_CONFIG,
173
+ env: {
174
+ ...MCP_ENV,
175
+ PATH: "/usr/local/bin:/usr/bin:/bin",
176
+ },
177
+ }),
178
+ instructionsPath: join(HOME, ".kiro", "steering", "miru.md"),
179
+ subagentPath: join(HOME, ".kiro", "agents", "miru-code.md"),
180
+ subagentId: "kiro",
181
+ },
182
+ {
183
+ id: "opencode",
184
+ displayName: "OpenCode",
185
+ binary: "opencode",
186
+ configDir: join(HOME, ".config", "opencode"),
187
+ mcp: jsonMcp(opencodeMcpPath(), "mcp", OPENCODE_SERVER_CONFIG),
188
+ instructionsPath: join(HOME, ".config", "opencode", "AGENTS.md"),
189
+ subagentPath: join(HOME, ".config", "opencode", "agents", "miru-code.md"),
190
+ subagentId: "opencode",
191
+ },
192
+ {
193
+ id: "copilot",
194
+ displayName: "GitHub Copilot",
195
+ binary: null,
196
+ configDir: join(HOME, ".config", "github-copilot"),
197
+ mcp: jsonMcp(join(HOME, ".copilot", "mcp-config.json"), "mcpServers", BARE_STDIO_SERVER_CONFIG),
198
+ instructionsPath: null,
199
+ subagentPath: join(HOME, ".copilot", "agents", "miru-code.agent.md"),
200
+ subagentId: "copilot",
201
+ },
202
+ {
203
+ id: "codex",
204
+ displayName: "Codex",
205
+ binary: "codex",
206
+ configDir: join(HOME, ".codex"),
207
+ mcp: {
208
+ path: join(HOME, ".codex", "config.toml"),
209
+ key: "mcp_servers",
210
+ memberKey: "miru",
211
+ entry: {},
212
+ format: "toml",
213
+ },
214
+ instructionsPath: join(HOME, ".codex", "AGENTS.md"),
215
+ subagentPath: null,
216
+ subagentId: null,
217
+ },
218
+ {
219
+ id: "vscode",
220
+ displayName: "VS Code",
221
+ binary: "code",
222
+ configDir: null,
223
+ mcp: jsonMcp(vscodeMcpPath(), "servers", STDIO_SERVER_CONFIG),
224
+ instructionsPath: null,
225
+ subagentPath: null,
226
+ subagentId: null,
227
+ },
228
+ ];
229
+
230
+ export async function isAgentDetected(agent: AgentTarget): Promise<boolean> {
231
+ if (agent.binary) {
232
+ try {
233
+ const proc = Bun.spawn(["which", agent.binary], { stdout: "pipe", stderr: "ignore" });
234
+ const code = await proc.exited;
235
+ if (code === 0) {
236
+ return true;
237
+ }
238
+ } catch {
239
+ // which unavailable
240
+ }
241
+ }
242
+ if (agent.configDir) {
243
+ return Bun.file(agent.configDir).exists();
244
+ }
245
+ return false;
246
+ }
@@ -0,0 +1,273 @@
1
+ import { mkdir, unlink, writeFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ import { type InstallAction, MIRU_END, MIRU_START } from "./agents.ts";
4
+
5
+ const CODEX_MCP_HEADER = "[mcp_servers.miru]";
6
+ const CODEX_MCP_BLOCK = `[mcp_servers.miru]
7
+ command = "bunx"
8
+ args = ["@takara-ai/miru-code"]
9
+ env = { TAKARA_API_KEY = "\${TAKARA_API_KEY}" }
10
+ `;
11
+
12
+ /** Strip line and block comments so JSONC configs can be parsed. */
13
+ export function stripJsonComments(text: string): string {
14
+ let out = "";
15
+ let i = 0;
16
+ let inString = false;
17
+ let stringQuote = '"';
18
+ while (i < text.length) {
19
+ const ch = text[i];
20
+ const next = text[i + 1];
21
+ if (inString) {
22
+ out += ch;
23
+ if (ch === "\\" && next !== undefined) {
24
+ out += next;
25
+ i += 2;
26
+ continue;
27
+ }
28
+ if (ch === stringQuote) {
29
+ inString = false;
30
+ }
31
+ i++;
32
+ continue;
33
+ }
34
+ if (ch === '"' || ch === "'") {
35
+ inString = true;
36
+ stringQuote = ch;
37
+ out += ch;
38
+ i++;
39
+ continue;
40
+ }
41
+ if (ch === "/" && next === "/") {
42
+ while (i < text.length && text[i] !== "\n") {
43
+ i++;
44
+ }
45
+ continue;
46
+ }
47
+ if (ch === "/" && next === "*") {
48
+ i += 2;
49
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) {
50
+ i++;
51
+ }
52
+ i += 2;
53
+ continue;
54
+ }
55
+ out += ch;
56
+ i++;
57
+ }
58
+ return out;
59
+ }
60
+
61
+ function parseJsonObject(text: string): Record<string, unknown> | "error" {
62
+ const trimmed = text.trim();
63
+ if (!trimmed) {
64
+ return {};
65
+ }
66
+ try {
67
+ const parsed: unknown = JSON.parse(stripJsonComments(trimmed));
68
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
69
+ return "error";
70
+ }
71
+ return parsed as Record<string, unknown>;
72
+ } catch {
73
+ return "error";
74
+ }
75
+ }
76
+
77
+ function sectionObject(
78
+ root: Record<string, unknown>,
79
+ sectionKey: string,
80
+ ): Record<string, unknown> | "error" {
81
+ const section = root[sectionKey];
82
+ if (section === undefined) {
83
+ return {};
84
+ }
85
+ if (!section || typeof section !== "object" || Array.isArray(section)) {
86
+ return "error";
87
+ }
88
+ return section as Record<string, unknown>;
89
+ }
90
+
91
+ export async function mergeJsonMember(
92
+ path: string,
93
+ sectionKey: string,
94
+ memberKey: string,
95
+ value: Record<string, unknown>,
96
+ ): Promise<InstallAction> {
97
+ const existed = await Bun.file(path).exists();
98
+ const text = existed ? await Bun.file(path).text() : "";
99
+ const parsed = parseJsonObject(text);
100
+ if (parsed === "error") {
101
+ return "error";
102
+ }
103
+
104
+ const section = sectionObject(parsed, sectionKey);
105
+ if (section === "error") {
106
+ return "error";
107
+ }
108
+
109
+ if (JSON.stringify(section[memberKey]) === JSON.stringify(value)) {
110
+ return "unchanged";
111
+ }
112
+
113
+ section[memberKey] = value;
114
+ parsed[sectionKey] = section;
115
+
116
+ const next = `${JSON.stringify(parsed, null, 2)}\n`;
117
+
118
+ await mkdir(dirname(path), { recursive: true });
119
+ await writeFile(path, next, "utf-8");
120
+ return existed ? "updated" : "created";
121
+ }
122
+
123
+ export async function removeJsonMember(
124
+ path: string,
125
+ sectionKey: string,
126
+ memberKey: string,
127
+ ): Promise<InstallAction> {
128
+ if (!(await Bun.file(path).exists())) {
129
+ return "not-found";
130
+ }
131
+
132
+ const text = await Bun.file(path).text();
133
+ const parsed = parseJsonObject(text);
134
+ if (parsed === "error") {
135
+ return "error";
136
+ }
137
+
138
+ const section = sectionObject(parsed, sectionKey);
139
+ if (section === "error" || !(memberKey in section)) {
140
+ return "not-found";
141
+ }
142
+
143
+ delete section[memberKey];
144
+ if (Object.keys(section).length === 0) {
145
+ delete parsed[sectionKey];
146
+ } else {
147
+ parsed[sectionKey] = section;
148
+ }
149
+
150
+ const next = `${JSON.stringify(parsed, null, 2)}\n`;
151
+ if (next === `${text.endsWith("\n") ? text : `${text}\n`}`) {
152
+ return "not-found";
153
+ }
154
+
155
+ if (Object.keys(parsed).length === 0) {
156
+ await unlink(path);
157
+ return "removed";
158
+ }
159
+
160
+ await writeFile(path, next, "utf-8");
161
+ return "removed";
162
+ }
163
+
164
+ export async function replaceOrAppendMarked(path: string, content: string): Promise<InstallAction> {
165
+ await mkdir(dirname(path), { recursive: true });
166
+ const existed = await Bun.file(path).exists();
167
+ const existing = existed ? await Bun.file(path).text() : "";
168
+
169
+ const startIdx = existing.indexOf(MIRU_START);
170
+ const endIdx = existing.indexOf(MIRU_END);
171
+
172
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
173
+ const before = existing.slice(0, startIdx);
174
+ const after = existing.slice(endIdx + MIRU_END.length);
175
+ const updated = `${before}${content.trim()}\n${after.replace(/^\n+/, "")}`;
176
+ if (updated === existing) {
177
+ return "unchanged";
178
+ }
179
+ await writeFile(path, updated, "utf-8");
180
+ return "updated";
181
+ }
182
+
183
+ const separator =
184
+ existing && !existing.endsWith("\n\n") ? (existing.endsWith("\n") ? "\n" : "\n\n") : "";
185
+ await writeFile(path, `${existing}${separator}${content}`, "utf-8");
186
+ return existed ? "updated" : "created";
187
+ }
188
+
189
+ export async function removeMarked(path: string): Promise<InstallAction> {
190
+ if (!(await Bun.file(path).exists())) {
191
+ return "not-found";
192
+ }
193
+
194
+ const existing = await Bun.file(path).text();
195
+ const startIdx = existing.indexOf(MIRU_START);
196
+ const endIdx = existing.indexOf(MIRU_END);
197
+
198
+ if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) {
199
+ return "not-found";
200
+ }
201
+
202
+ const before = existing.slice(0, startIdx).replace(/\n+$/, "");
203
+ const after = existing.slice(endIdx + MIRU_END.length).replace(/^\n+/, "");
204
+ const updated = [before, after].filter((part) => part.length > 0).join("\n");
205
+ const withNewline = updated.length > 0 ? `${updated}\n` : "";
206
+
207
+ if (withNewline.trim().length === 0) {
208
+ await unlink(path);
209
+ return "removed";
210
+ }
211
+
212
+ await writeFile(path, withNewline, "utf-8");
213
+ return "removed";
214
+ }
215
+
216
+ function stripTomlSection(text: string, header: string): string {
217
+ const prefix = header.trim().slice(1, -1);
218
+ const lines = text.split("\n");
219
+ const result: string[] = [];
220
+ let skipping = false;
221
+
222
+ for (const line of lines) {
223
+ const tableKey = line.split("#")[0]?.trim() ?? "";
224
+ if (tableKey.startsWith("[") && tableKey.endsWith("]")) {
225
+ const tableName = tableKey.slice(1, -1);
226
+ if (tableName === prefix || tableName.startsWith(`${prefix}.`)) {
227
+ skipping = true;
228
+ continue;
229
+ }
230
+ skipping = false;
231
+ }
232
+ if (!skipping) {
233
+ result.push(line);
234
+ }
235
+ }
236
+
237
+ return result.join("\n");
238
+ }
239
+
240
+ export async function mergeTomlBlock(path: string): Promise<InstallAction> {
241
+ await mkdir(dirname(path), { recursive: true });
242
+ const existed = await Bun.file(path).exists();
243
+ const existing = existed ? await Bun.file(path).text() : "";
244
+
245
+ if (existing.includes(CODEX_MCP_BLOCK.trim())) {
246
+ return "unchanged";
247
+ }
248
+
249
+ const base = stripTomlSection(existing, CODEX_MCP_HEADER).replace(/\n+$/, "");
250
+ const next = base.length > 0 ? `${base}\n\n${CODEX_MCP_BLOCK}` : CODEX_MCP_BLOCK;
251
+ await writeFile(path, next.endsWith("\n") ? next : `${next}\n`, "utf-8");
252
+ return existed ? "updated" : "created";
253
+ }
254
+
255
+ export async function removeTomlBlock(path: string): Promise<InstallAction> {
256
+ if (!(await Bun.file(path).exists())) {
257
+ return "not-found";
258
+ }
259
+
260
+ const existing = await Bun.file(path).text();
261
+ if (!existing.includes(CODEX_MCP_HEADER)) {
262
+ return "not-found";
263
+ }
264
+
265
+ const remaining = stripTomlSection(existing, CODEX_MCP_HEADER).trim();
266
+ if (remaining.length === 0) {
267
+ await unlink(path);
268
+ return "removed";
269
+ }
270
+
271
+ await writeFile(path, `${remaining}\n`, "utf-8");
272
+ return "removed";
273
+ }