nomoreide 0.1.68 → 0.1.70
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/dist/core/agent-env/antigravity-reader.d.ts +7 -0
- package/dist/core/agent-env/antigravity-reader.js +86 -0
- package/dist/core/agent-env/antigravity-reader.js.map +1 -0
- package/dist/core/agent-env/availability.d.ts +6 -0
- package/dist/core/agent-env/availability.js +79 -0
- package/dist/core/agent-env/availability.js.map +1 -0
- package/dist/core/agent-env/claude-reader.d.ts +7 -0
- package/dist/core/agent-env/claude-reader.js +93 -0
- package/dist/core/agent-env/claude-reader.js.map +1 -0
- package/dist/core/agent-env/codex-reader.d.ts +9 -0
- package/dist/core/agent-env/codex-reader.js +145 -0
- package/dist/core/agent-env/codex-reader.js.map +1 -0
- package/dist/core/agent-env/index.d.ts +9 -0
- package/dist/core/agent-env/index.js +29 -0
- package/dist/core/agent-env/index.js.map +1 -0
- package/dist/core/agent-env/managed-plugins.d.ts +13 -0
- package/dist/core/agent-env/managed-plugins.js +36 -0
- package/dist/core/agent-env/managed-plugins.js.map +1 -0
- package/dist/core/agent-env/plugin-reader.d.ts +9 -0
- package/dist/core/agent-env/plugin-reader.js +174 -0
- package/dist/core/agent-env/plugin-reader.js.map +1 -0
- package/dist/core/agent-env/shared.d.ts +25 -0
- package/dist/core/agent-env/shared.js +115 -0
- package/dist/core/agent-env/shared.js.map +1 -0
- package/dist/core/agent-env/types.d.ts +66 -0
- package/dist/core/agent-env/types.js +7 -0
- package/dist/core/agent-env/types.js.map +1 -0
- package/dist/core/agent-env-actions.d.ts +117 -0
- package/dist/core/agent-env-actions.js +234 -0
- package/dist/core/agent-env-actions.js.map +1 -0
- package/dist/core/agent-env-writers.d.ts +46 -0
- package/dist/core/agent-env-writers.js +385 -0
- package/dist/core/agent-env-writers.js.map +1 -0
- package/dist/mcp/tools/agent-env.d.ts +9 -0
- package/dist/mcp/tools/agent-env.js +160 -0
- package/dist/mcp/tools/agent-env.js.map +1 -0
- package/dist/mcp/tools/index.d.ts +1 -1
- package/dist/mcp/tools/index.js +3 -0
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/web/client/assets/{code-editor-woFvM3aJ.js → code-editor-tF2NdMJV.js} +1 -1
- package/dist/web/client/assets/index-C7lI87cF.css +1 -0
- package/dist/web/client/assets/{index-B4hwsHPU.js → index-CQmdoyuK.js} +111 -111
- package/dist/web/client/index.html +2 -2
- package/dist/web/routes/agent-env-routes.d.ts +9 -0
- package/dist/web/routes/agent-env-routes.js +86 -0
- package/dist/web/routes/agent-env-routes.js.map +1 -0
- package/dist/web/routes/index.js +2 -0
- package/dist/web/routes/index.js.map +1 -1
- package/dist/web/routes/shell-routes.js +1 -0
- package/dist/web/routes/shell-routes.js.map +1 -1
- package/package.json +1 -1
- package/dist/web/client/assets/index-CeyRPbeV.css +0 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Environments staged writes (ROR-61) — the write-guarded surface over
|
|
3
|
+
* `agent-env-writers.ts`. Mirrors the GitManager/GitActions and DbPeek/DbWrite
|
|
4
|
+
* split: `core/agent-env/` only reads; every mutation flows through this
|
|
5
|
+
* module's preview → apply gate and reports the backup files it created.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { readAllAgentConfigs, AGENT_NAMES, } from "./agent-env/index.js";
|
|
9
|
+
import { addMcp, copySkill, moveSkill, removeMcp, removeSkill, snapshotAgentConfig, } from "./agent-env-writers.js";
|
|
10
|
+
const agentNameSchema = z.enum(["claude", "codex", "antigravity"]);
|
|
11
|
+
const scopeSchema = z.enum(["user", "project"]);
|
|
12
|
+
/** One staged mutation. `name` is the MCP key or the skill directory name. */
|
|
13
|
+
export const pendingChangeSchema = z.object({
|
|
14
|
+
category: z.enum(["mcp", "skill"]),
|
|
15
|
+
action: z.enum(["copy", "move", "remove"]),
|
|
16
|
+
name: z.string().min(1),
|
|
17
|
+
sourceAgent: agentNameSchema,
|
|
18
|
+
sourceScope: scopeSchema.default("user"),
|
|
19
|
+
targetAgent: agentNameSchema.optional(),
|
|
20
|
+
targetScope: scopeSchema.optional(),
|
|
21
|
+
});
|
|
22
|
+
export const pendingChangesSchema = z.array(pendingChangeSchema).min(1).max(50);
|
|
23
|
+
const AGENT_LABELS = {
|
|
24
|
+
claude: "Claude Code",
|
|
25
|
+
codex: "Codex CLI",
|
|
26
|
+
antigravity: "Antigravity",
|
|
27
|
+
};
|
|
28
|
+
export async function previewChanges(options) {
|
|
29
|
+
const configs = await readAllAgentConfigs(options);
|
|
30
|
+
const byAgent = new Map(configs.map((config) => [config.agent, config]));
|
|
31
|
+
const items = options.changes.map((change) => previewOne(change, byAgent));
|
|
32
|
+
const agents = AGENT_NAMES.map((agent) => ({
|
|
33
|
+
agent,
|
|
34
|
+
add: [],
|
|
35
|
+
remove: [],
|
|
36
|
+
}));
|
|
37
|
+
const diffFor = (agent) => agents.find((entry) => entry.agent === agent);
|
|
38
|
+
for (const item of items) {
|
|
39
|
+
if (!item.ok)
|
|
40
|
+
continue;
|
|
41
|
+
const { change } = item;
|
|
42
|
+
const label = `${change.category} "${change.name}"`;
|
|
43
|
+
if (change.action !== "remove" && change.targetAgent) {
|
|
44
|
+
diffFor(change.targetAgent).add.push(label);
|
|
45
|
+
}
|
|
46
|
+
if (change.action !== "copy") {
|
|
47
|
+
diffFor(change.sourceAgent).remove.push(label);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
valid: items.every((item) => item.ok),
|
|
52
|
+
items,
|
|
53
|
+
agents: agents.filter((entry) => entry.add.length > 0 || entry.remove.length > 0),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function previewOne(change, byAgent) {
|
|
57
|
+
const warnings = [];
|
|
58
|
+
const fail = (error) => ({
|
|
59
|
+
change,
|
|
60
|
+
ok: false,
|
|
61
|
+
summary: describeChange(change),
|
|
62
|
+
warnings,
|
|
63
|
+
error,
|
|
64
|
+
});
|
|
65
|
+
const source = byAgent.get(change.sourceAgent);
|
|
66
|
+
if (!source)
|
|
67
|
+
return fail(`Unknown agent "${change.sourceAgent}".`);
|
|
68
|
+
if (change.action !== "remove") {
|
|
69
|
+
if (!change.targetAgent)
|
|
70
|
+
return fail(`A ${change.action} needs a target agent.`);
|
|
71
|
+
const targetScope = change.targetScope ?? "user";
|
|
72
|
+
if (change.targetAgent === change.sourceAgent && targetScope === change.sourceScope) {
|
|
73
|
+
return fail(`Source and target are the same; nothing to ${change.action}.`);
|
|
74
|
+
}
|
|
75
|
+
if (change.category === "skill" && targetScope === "project" && change.targetAgent !== "claude") {
|
|
76
|
+
return fail(`${AGENT_LABELS[change.targetAgent]} has no project-scoped skills; only Claude reads .claude/skills/.`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else if (change.targetAgent) {
|
|
80
|
+
return fail("A remove cannot have a target agent.");
|
|
81
|
+
}
|
|
82
|
+
if (change.category === "mcp") {
|
|
83
|
+
const { entry, remoteEntry } = findMcp(source, change.name, change.sourceScope);
|
|
84
|
+
if (!entry && !remoteEntry) {
|
|
85
|
+
return fail(`MCP "${change.name}" not found in ${AGENT_LABELS[change.sourceAgent]} ${change.sourceScope} scope.`);
|
|
86
|
+
}
|
|
87
|
+
if (change.targetAgent) {
|
|
88
|
+
const target = byAgent.get(change.targetAgent);
|
|
89
|
+
const targetScope = change.targetScope ?? "user";
|
|
90
|
+
if (target) {
|
|
91
|
+
const existing = findMcp(target, change.name, targetScope);
|
|
92
|
+
if (existing.entry || existing.remoteEntry) {
|
|
93
|
+
warnings.push(`${AGENT_LABELS[change.targetAgent]} already has an MCP named "${change.name}"; it will be overwritten.`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (change.targetAgent === "codex" &&
|
|
97
|
+
targetScope === "user" &&
|
|
98
|
+
remoteEntry &&
|
|
99
|
+
(remoteEntry.headers || remoteEntry.transport === "sse")) {
|
|
100
|
+
warnings.push("Codex config only stores a URL for remote MCPs; transport and headers will be dropped.");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
const skill = source.skills.find((candidate) => candidate.name === change.name && candidate.scope === change.sourceScope);
|
|
106
|
+
if (!skill) {
|
|
107
|
+
return fail(`Skill "${change.name}" not found in ${AGENT_LABELS[change.sourceAgent]} ${change.sourceScope} scope.`);
|
|
108
|
+
}
|
|
109
|
+
if (skill.kind === "plugin") {
|
|
110
|
+
return fail(`"${change.name}" is a plugin managed by ${AGENT_LABELS[change.sourceAgent]}; install it through the plugin marketplace instead of copying files.`);
|
|
111
|
+
}
|
|
112
|
+
if (change.targetAgent) {
|
|
113
|
+
const target = byAgent.get(change.targetAgent);
|
|
114
|
+
const targetScope = change.targetScope ?? "user";
|
|
115
|
+
const collision = target?.skills.some((candidate) => candidate.name === change.name && candidate.scope === targetScope);
|
|
116
|
+
if (collision) {
|
|
117
|
+
warnings.push(`${AGENT_LABELS[change.targetAgent]} already has a skill named "${change.name}"; it will be replaced.`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { change, ok: true, summary: describeChange(change), warnings };
|
|
122
|
+
}
|
|
123
|
+
export function describeChange(change) {
|
|
124
|
+
const kind = change.category === "mcp" ? "MCP" : "skill";
|
|
125
|
+
const from = `${AGENT_LABELS[change.sourceAgent]} (${change.sourceScope})`;
|
|
126
|
+
if (change.action === "remove") {
|
|
127
|
+
return `Remove ${kind} "${change.name}" from ${from}`;
|
|
128
|
+
}
|
|
129
|
+
const targetScope = change.targetScope ?? "user";
|
|
130
|
+
const target = change.targetAgent ? AGENT_LABELS[change.targetAgent] : "?";
|
|
131
|
+
const verb = change.action === "copy" ? "Copy" : "Move";
|
|
132
|
+
return `${verb} ${kind} "${change.name}" from ${from} to ${target} (${targetScope})`;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Validate the full batch, then apply changes in order. Validation failures
|
|
136
|
+
* abort before anything is written; a mid-batch write failure stops the batch
|
|
137
|
+
* (earlier changes stay applied — their backups are in the result).
|
|
138
|
+
*/
|
|
139
|
+
export async function applyChanges(options) {
|
|
140
|
+
const preview = await previewChanges(options);
|
|
141
|
+
if (!preview.valid) {
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
applied: 0,
|
|
145
|
+
failed: preview.items.filter((item) => !item.ok).length,
|
|
146
|
+
results: preview.items.map((item) => ({
|
|
147
|
+
change: item.change,
|
|
148
|
+
ok: false,
|
|
149
|
+
summary: item.summary,
|
|
150
|
+
backups: [],
|
|
151
|
+
error: item.error ?? "Not applied — another staged change failed validation.",
|
|
152
|
+
})),
|
|
153
|
+
backups: [],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const configs = await readAllAgentConfigs(options);
|
|
157
|
+
const byAgent = new Map(configs.map((config) => [config.agent, config]));
|
|
158
|
+
const results = [];
|
|
159
|
+
for (const change of options.changes) {
|
|
160
|
+
const summary = describeChange(change);
|
|
161
|
+
try {
|
|
162
|
+
const outcome = await applyOne(change, byAgent, options);
|
|
163
|
+
results.push({ change, ok: true, summary, backups: outcome.backups });
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
results.push({
|
|
167
|
+
change,
|
|
168
|
+
ok: false,
|
|
169
|
+
summary,
|
|
170
|
+
backups: [],
|
|
171
|
+
error: error instanceof Error ? error.message : String(error),
|
|
172
|
+
});
|
|
173
|
+
break; // stop the batch on the first write failure
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const applied = results.filter((result) => result.ok).length;
|
|
177
|
+
return {
|
|
178
|
+
ok: applied === options.changes.length,
|
|
179
|
+
applied,
|
|
180
|
+
failed: results.length - applied,
|
|
181
|
+
results,
|
|
182
|
+
backups: results.flatMap((result) => result.backups),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
async function applyOne(change, byAgent, options) {
|
|
186
|
+
const base = { cwd: options.cwd, homeDir: options.homeDir };
|
|
187
|
+
if (change.category === "mcp") {
|
|
188
|
+
const source = byAgent.get(change.sourceAgent);
|
|
189
|
+
const { entry, remoteEntry } = findMcp(source, change.name, change.sourceScope);
|
|
190
|
+
if (change.action === "remove") {
|
|
191
|
+
return removeMcp({ ...base, agent: change.sourceAgent, key: change.name, scope: change.sourceScope });
|
|
192
|
+
}
|
|
193
|
+
const added = await addMcp({
|
|
194
|
+
...base,
|
|
195
|
+
agent: change.targetAgent,
|
|
196
|
+
key: change.name,
|
|
197
|
+
entry,
|
|
198
|
+
remoteEntry,
|
|
199
|
+
scope: change.targetScope ?? "user",
|
|
200
|
+
});
|
|
201
|
+
if (change.action === "copy")
|
|
202
|
+
return added;
|
|
203
|
+
const removed = await removeMcp({
|
|
204
|
+
...base,
|
|
205
|
+
agent: change.sourceAgent,
|
|
206
|
+
key: change.name,
|
|
207
|
+
scope: change.sourceScope,
|
|
208
|
+
});
|
|
209
|
+
return { backups: [...added.backups, ...removed.backups] };
|
|
210
|
+
}
|
|
211
|
+
const source = { agent: change.sourceAgent, skillName: change.name, scope: change.sourceScope };
|
|
212
|
+
if (change.action === "remove") {
|
|
213
|
+
return removeSkill({ ...base, location: source });
|
|
214
|
+
}
|
|
215
|
+
const target = {
|
|
216
|
+
agent: change.targetAgent,
|
|
217
|
+
skillName: change.name,
|
|
218
|
+
scope: (change.targetScope ?? "user"),
|
|
219
|
+
};
|
|
220
|
+
return change.action === "copy"
|
|
221
|
+
? copySkill({ ...base, source, target })
|
|
222
|
+
: moveSkill({ ...base, source, target });
|
|
223
|
+
}
|
|
224
|
+
function findMcp(config, name, scope) {
|
|
225
|
+
const local = scope === "project" ? config.projectMcpServers : config.mcpServers;
|
|
226
|
+
const remote = scope === "project" ? config.projectRemoteMcpServers : config.remoteMcpServers;
|
|
227
|
+
return { entry: local[name], remoteEntry: remote[name] };
|
|
228
|
+
}
|
|
229
|
+
/** Back up an agent's live config file (`<file>.bak.<ts>`) before bulk edits. */
|
|
230
|
+
export async function snapshotAgent(options) {
|
|
231
|
+
const outcome = await snapshotAgentConfig(options);
|
|
232
|
+
return { agent: options.agent, backups: outcome.backups };
|
|
233
|
+
}
|
|
234
|
+
//# sourceMappingURL=agent-env-actions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-env-actions.js","sourceRoot":"","sources":["../../src/core/agent-env-actions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type AgentMcpEntry, type AgentName, type RemoteMcpEntry } from "./agent-env/index.js";
|
|
2
|
+
export type AgentEnvScope = "user" | "project";
|
|
3
|
+
export interface WriterOptions {
|
|
4
|
+
cwd: string;
|
|
5
|
+
/** Injectable for tmpdir tests, like the readers. */
|
|
6
|
+
homeDir?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Paths of `.bak.*` copies created before the mutation (empty for fresh files). */
|
|
9
|
+
export interface WriteOutcome {
|
|
10
|
+
backups: string[];
|
|
11
|
+
}
|
|
12
|
+
export declare function formatTimestamp(): string;
|
|
13
|
+
export interface McpMutationOptions extends WriterOptions {
|
|
14
|
+
agent: AgentName;
|
|
15
|
+
key: string;
|
|
16
|
+
scope: AgentEnvScope;
|
|
17
|
+
}
|
|
18
|
+
export interface AddMcpOptions extends McpMutationOptions {
|
|
19
|
+
entry?: AgentMcpEntry;
|
|
20
|
+
remoteEntry?: RemoteMcpEntry;
|
|
21
|
+
}
|
|
22
|
+
export declare function addMcp(options: AddMcpOptions): Promise<WriteOutcome>;
|
|
23
|
+
export declare function removeMcp(options: McpMutationOptions): Promise<WriteOutcome>;
|
|
24
|
+
export interface SkillLocation {
|
|
25
|
+
agent: AgentName;
|
|
26
|
+
skillName: string;
|
|
27
|
+
scope: AgentEnvScope;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the directory holding a skill's SKILL.md. Project scope is
|
|
31
|
+
* Claude-only — Codex and Antigravity have no project-skills concept.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getSkillDir(location: SkillLocation, cwd: string, homeDir?: string): string;
|
|
34
|
+
export interface CopySkillOptions extends WriterOptions {
|
|
35
|
+
source: SkillLocation;
|
|
36
|
+
target: SkillLocation;
|
|
37
|
+
}
|
|
38
|
+
export declare function copySkill(options: CopySkillOptions): Promise<WriteOutcome>;
|
|
39
|
+
export interface RemoveSkillOptions extends WriterOptions {
|
|
40
|
+
location: SkillLocation;
|
|
41
|
+
}
|
|
42
|
+
export declare function removeSkill(options: RemoveSkillOptions): Promise<WriteOutcome>;
|
|
43
|
+
export declare function moveSkill(options: CopySkillOptions): Promise<WriteOutcome>;
|
|
44
|
+
export declare function snapshotAgentConfig(options: WriterOptions & {
|
|
45
|
+
agent: AgentName;
|
|
46
|
+
}): Promise<WriteOutcome>;
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Environments low-level writers — the ONLY code that mutates coding
|
|
3
|
+
* agents' config files. Ported from brainctl's agent-config-service (ROR-61).
|
|
4
|
+
* Kept separate from the read-safe `core/agent-env/` module, mirroring the
|
|
5
|
+
* GitManager/GitActions and DbPeek/DbWrite splits; callers go through the
|
|
6
|
+
* staged preview → apply flow in `agent-env-actions.ts`.
|
|
7
|
+
*
|
|
8
|
+
* Every mutation backs up the touched file to a timestamped sibling
|
|
9
|
+
* (`<file>.bak.<ts>`) and writes atomically (temp file + rename). Skill
|
|
10
|
+
* directory removals are backed up under
|
|
11
|
+
* `<home>/.config/nomoreide/agent-env-backups/` first.
|
|
12
|
+
*/
|
|
13
|
+
import { constants } from "node:fs";
|
|
14
|
+
import { copyFile, cp, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { readAntigravityConfig, readCodexConfig, } from "./agent-env/index.js";
|
|
18
|
+
export function formatTimestamp() {
|
|
19
|
+
const now = new Date();
|
|
20
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
21
|
+
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
22
|
+
}
|
|
23
|
+
async function backupFile(filePath) {
|
|
24
|
+
const base = `${filePath}.bak.${formatTimestamp()}`;
|
|
25
|
+
// COPYFILE_EXCL + suffix loop: two writes within the same second (e.g. the
|
|
26
|
+
// add+remove halves of a move) must not overwrite each other's backup.
|
|
27
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
28
|
+
const backupPath = attempt === 0 ? base : `${base}-${attempt}`;
|
|
29
|
+
try {
|
|
30
|
+
await copyFile(filePath, backupPath, constants.COPYFILE_EXCL);
|
|
31
|
+
return backupPath;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const code = error.code;
|
|
35
|
+
if (code === "EEXIST")
|
|
36
|
+
continue;
|
|
37
|
+
return null; // source file does not exist yet — nothing to back up
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
async function atomicWrite(filePath, content) {
|
|
43
|
+
const tmpPath = `${filePath}.tmp.${Date.now()}`;
|
|
44
|
+
await writeFile(tmpPath, content, "utf8");
|
|
45
|
+
await rename(tmpPath, filePath);
|
|
46
|
+
}
|
|
47
|
+
async function atomicWriteJson(filePath, data) {
|
|
48
|
+
await atomicWrite(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
49
|
+
}
|
|
50
|
+
export async function addMcp(options) {
|
|
51
|
+
const { agent, key, entry, remoteEntry, scope } = options;
|
|
52
|
+
if (!entry && !remoteEntry) {
|
|
53
|
+
throw new Error(`MCP "${key}" has neither a command nor a URL; nothing to add.`);
|
|
54
|
+
}
|
|
55
|
+
if (agent === "claude") {
|
|
56
|
+
return mutateClaudeConfig(options, scope, (servers) => {
|
|
57
|
+
servers[key] = remoteEntry ? toClaudeRemoteEntry(remoteEntry) : toClaudeEntry(entry);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (scope === "project") {
|
|
61
|
+
// Codex and Antigravity have no native per-project config; brainctl's
|
|
62
|
+
// `.brainctl/project-mcps.json` convention is kept for compatibility.
|
|
63
|
+
return mutateBrainctlProjectMcps(options, agent, (servers) => {
|
|
64
|
+
servers[key] = remoteEntry
|
|
65
|
+
? { transport: remoteEntry.transport, url: remoteEntry.url }
|
|
66
|
+
: {
|
|
67
|
+
command: entry.command,
|
|
68
|
+
...(entry.args ? { args: entry.args } : {}),
|
|
69
|
+
...(entry.env ? { env: entry.env } : {}),
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (agent === "codex") {
|
|
74
|
+
return mutateCodexConfig(options, (state) => {
|
|
75
|
+
delete state.mcpServers[key];
|
|
76
|
+
delete state.remoteMcpServers[key];
|
|
77
|
+
if (remoteEntry)
|
|
78
|
+
state.remoteMcpServers[key] = remoteEntry;
|
|
79
|
+
else
|
|
80
|
+
state.mcpServers[key] = entry;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return mutateAntigravityConfig(options, (servers) => {
|
|
84
|
+
servers[key] = remoteEntry ? toAntigravityRemoteEntry(remoteEntry) : toAntigravityEntry(entry);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
export async function removeMcp(options) {
|
|
88
|
+
const { agent, key, scope } = options;
|
|
89
|
+
if (agent === "claude") {
|
|
90
|
+
return mutateClaudeConfig(options, scope, (servers) => {
|
|
91
|
+
delete servers[key];
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (scope === "project") {
|
|
95
|
+
return mutateBrainctlProjectMcps(options, agent, (servers) => {
|
|
96
|
+
delete servers[key];
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (agent === "codex") {
|
|
100
|
+
return mutateCodexConfig(options, (state) => {
|
|
101
|
+
delete state.mcpServers[key];
|
|
102
|
+
delete state.remoteMcpServers[key];
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return mutateAntigravityConfig(options, (servers) => {
|
|
106
|
+
delete servers[key];
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/* ---- Claude: ~/.claude.json with top-level and projects[cwd] mcpServers ---- */
|
|
110
|
+
async function mutateClaudeConfig(options, scope, mutate) {
|
|
111
|
+
const homeDir = options.homeDir ?? homedir();
|
|
112
|
+
const configPath = path.join(homeDir, ".claude.json");
|
|
113
|
+
const backups = [];
|
|
114
|
+
let existing = {};
|
|
115
|
+
try {
|
|
116
|
+
existing = JSON.parse(await readFile(configPath, "utf8"));
|
|
117
|
+
const backup = await backupFile(configPath);
|
|
118
|
+
if (backup)
|
|
119
|
+
backups.push(backup);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// fresh config
|
|
123
|
+
}
|
|
124
|
+
if (scope === "user") {
|
|
125
|
+
const servers = (existing.mcpServers ?? {});
|
|
126
|
+
mutate(servers);
|
|
127
|
+
existing.mcpServers = servers;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
const projects = (existing.projects ?? {});
|
|
131
|
+
const projectConfig = (projects[options.cwd] ?? {});
|
|
132
|
+
const servers = (projectConfig.mcpServers ?? {});
|
|
133
|
+
mutate(servers);
|
|
134
|
+
projectConfig.mcpServers = servers;
|
|
135
|
+
projects[options.cwd] = projectConfig;
|
|
136
|
+
existing.projects = projects;
|
|
137
|
+
}
|
|
138
|
+
await atomicWriteJson(configPath, existing);
|
|
139
|
+
return { backups };
|
|
140
|
+
}
|
|
141
|
+
function toClaudeEntry(entry) {
|
|
142
|
+
return {
|
|
143
|
+
type: "stdio",
|
|
144
|
+
command: entry.command,
|
|
145
|
+
args: entry.args ?? [],
|
|
146
|
+
...(entry.env ? { env: entry.env } : {}),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function toClaudeRemoteEntry(entry) {
|
|
150
|
+
return {
|
|
151
|
+
type: entry.transport === "sse" ? "sse" : "http",
|
|
152
|
+
url: entry.url,
|
|
153
|
+
...(entry.headers ? { headers: entry.headers } : {}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/* ---- Codex: [mcp_servers.*] TOML sections in ~/.codex/config.toml ---- */
|
|
157
|
+
async function mutateCodexConfig(options, mutate) {
|
|
158
|
+
const homeDir = options.homeDir ?? homedir();
|
|
159
|
+
const configPath = path.join(homeDir, ".codex", "config.toml");
|
|
160
|
+
const backups = [];
|
|
161
|
+
let existingContent = "";
|
|
162
|
+
try {
|
|
163
|
+
existingContent = await readFile(configPath, "utf8");
|
|
164
|
+
const backup = await backupFile(configPath);
|
|
165
|
+
if (backup)
|
|
166
|
+
backups.push(backup);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// fresh config
|
|
170
|
+
}
|
|
171
|
+
const current = await readCodexConfig({ cwd: options.cwd, homeDir });
|
|
172
|
+
const state = {
|
|
173
|
+
mcpServers: { ...current.mcpServers },
|
|
174
|
+
remoteMcpServers: { ...current.remoteMcpServers },
|
|
175
|
+
};
|
|
176
|
+
mutate(state);
|
|
177
|
+
// Rebuild the file: everything that isn't an mcp_servers section, then ours.
|
|
178
|
+
const nonMcp = stripCodexMcpSections(existingContent).trim();
|
|
179
|
+
const mcpToml = buildCodexMcpToml(state);
|
|
180
|
+
const final = [nonMcp, mcpToml].filter((part) => part.length > 0).join("\n\n");
|
|
181
|
+
await mkdir(path.dirname(configPath), { recursive: true });
|
|
182
|
+
await atomicWrite(configPath, final + "\n");
|
|
183
|
+
return { backups };
|
|
184
|
+
}
|
|
185
|
+
function stripCodexMcpSections(content) {
|
|
186
|
+
const lines = content.split("\n");
|
|
187
|
+
const result = [];
|
|
188
|
+
let inMcp = false;
|
|
189
|
+
for (const line of lines) {
|
|
190
|
+
if (/^\[mcp_servers[\].]/.test(line)) {
|
|
191
|
+
inMcp = true;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (inMcp && /^\[/.test(line) && !/^\[mcp_servers[\].]/.test(line)) {
|
|
195
|
+
inMcp = false;
|
|
196
|
+
}
|
|
197
|
+
if (!inMcp)
|
|
198
|
+
result.push(line);
|
|
199
|
+
}
|
|
200
|
+
return result.join("\n");
|
|
201
|
+
}
|
|
202
|
+
function buildCodexMcpToml(state) {
|
|
203
|
+
const lines = [];
|
|
204
|
+
for (const [name, entry] of Object.entries(state.mcpServers)) {
|
|
205
|
+
lines.push(`[mcp_servers.${name}]`);
|
|
206
|
+
lines.push(`command = ${tomlStr(entry.command)}`);
|
|
207
|
+
if (entry.args && entry.args.length > 0) {
|
|
208
|
+
lines.push(`args = [${entry.args.map(tomlStr).join(", ")}]`);
|
|
209
|
+
}
|
|
210
|
+
if (entry.env && Object.keys(entry.env).length > 0) {
|
|
211
|
+
lines.push("");
|
|
212
|
+
lines.push(`[mcp_servers.${name}.env]`);
|
|
213
|
+
for (const [key, value] of Object.entries(entry.env)) {
|
|
214
|
+
lines.push(`${key} = ${tomlStr(value)}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
lines.push("");
|
|
218
|
+
}
|
|
219
|
+
// Codex TOML only carries a URL for remote servers; transport/headers are
|
|
220
|
+
// Claude/Antigravity concepts and are dropped (preview warns about this).
|
|
221
|
+
for (const [name, entry] of Object.entries(state.remoteMcpServers)) {
|
|
222
|
+
lines.push(`[mcp_servers.${name}]`);
|
|
223
|
+
lines.push(`url = ${tomlStr(entry.url)}`);
|
|
224
|
+
lines.push("");
|
|
225
|
+
}
|
|
226
|
+
return lines.join("\n").trim();
|
|
227
|
+
}
|
|
228
|
+
function tomlStr(value) {
|
|
229
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
230
|
+
}
|
|
231
|
+
/* ---- Antigravity: JSON mcpServers at the live candidate path ---- */
|
|
232
|
+
async function mutateAntigravityConfig(options, mutate) {
|
|
233
|
+
const homeDir = options.homeDir ?? homedir();
|
|
234
|
+
// Antigravity has shipped several config locations; write to whichever one
|
|
235
|
+
// is live (the reader resolves it), falling back to the current default.
|
|
236
|
+
const live = await readAntigravityConfig({ cwd: options.cwd, homeDir });
|
|
237
|
+
const configPath = live.configPath;
|
|
238
|
+
const backups = [];
|
|
239
|
+
let existing = {};
|
|
240
|
+
try {
|
|
241
|
+
const source = await readFile(configPath, "utf8");
|
|
242
|
+
if (source.trim().length > 0) {
|
|
243
|
+
existing = JSON.parse(source);
|
|
244
|
+
}
|
|
245
|
+
const backup = await backupFile(configPath);
|
|
246
|
+
if (backup)
|
|
247
|
+
backups.push(backup);
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// fresh config
|
|
251
|
+
}
|
|
252
|
+
const servers = (existing.mcpServers ?? {});
|
|
253
|
+
mutate(servers);
|
|
254
|
+
existing.mcpServers = servers;
|
|
255
|
+
await mkdir(path.dirname(configPath), { recursive: true });
|
|
256
|
+
await atomicWriteJson(configPath, existing);
|
|
257
|
+
return { backups };
|
|
258
|
+
}
|
|
259
|
+
function toAntigravityEntry(entry) {
|
|
260
|
+
return {
|
|
261
|
+
command: entry.command,
|
|
262
|
+
args: entry.args ?? [],
|
|
263
|
+
...(entry.env ? { env: entry.env } : {}),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function toAntigravityRemoteEntry(entry) {
|
|
267
|
+
return {
|
|
268
|
+
...(entry.transport === "http" ? { httpUrl: entry.url } : { url: entry.url }),
|
|
269
|
+
...(entry.headers ? { headers: entry.headers } : {}),
|
|
270
|
+
...(entry.env ? { env: entry.env } : {}),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/* ---- brainctl-compat project MCPs: <cwd>/.brainctl/project-mcps.json ---- */
|
|
274
|
+
async function mutateBrainctlProjectMcps(options, agent, mutate) {
|
|
275
|
+
const filePath = path.join(options.cwd, ".brainctl", "project-mcps.json");
|
|
276
|
+
const backups = [];
|
|
277
|
+
let data = {};
|
|
278
|
+
try {
|
|
279
|
+
data = JSON.parse(await readFile(filePath, "utf8"));
|
|
280
|
+
const backup = await backupFile(filePath);
|
|
281
|
+
if (backup)
|
|
282
|
+
backups.push(backup);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
// fresh file
|
|
286
|
+
}
|
|
287
|
+
const agentData = (data[agent] ?? {});
|
|
288
|
+
const servers = (agentData.mcpServers ?? {});
|
|
289
|
+
mutate(servers);
|
|
290
|
+
agentData.mcpServers = servers;
|
|
291
|
+
data[agent] = agentData;
|
|
292
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
293
|
+
await atomicWriteJson(filePath, data);
|
|
294
|
+
return { backups };
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Resolve the directory holding a skill's SKILL.md. Project scope is
|
|
298
|
+
* Claude-only — Codex and Antigravity have no project-skills concept.
|
|
299
|
+
*/
|
|
300
|
+
export function getSkillDir(location, cwd, homeDir) {
|
|
301
|
+
const { agent, scope } = location;
|
|
302
|
+
const safeName = path.basename(location.skillName);
|
|
303
|
+
if (scope === "project") {
|
|
304
|
+
if (agent !== "claude") {
|
|
305
|
+
throw new Error(`Project-scoped skills are not supported for ${agent}; only Claude reads .claude/skills/ from the workspace.`);
|
|
306
|
+
}
|
|
307
|
+
return path.join(cwd, ".claude", "skills", safeName);
|
|
308
|
+
}
|
|
309
|
+
const home = homeDir ?? homedir();
|
|
310
|
+
const agentDir = { claude: ".claude", codex: ".codex", antigravity: ".gemini" }[agent];
|
|
311
|
+
return path.join(home, agentDir, "skills", safeName);
|
|
312
|
+
}
|
|
313
|
+
export async function copySkill(options) {
|
|
314
|
+
const sourceDir = getSkillDir(options.source, options.cwd, options.homeDir);
|
|
315
|
+
const targetDir = getSkillDir(options.target, options.cwd, options.homeDir);
|
|
316
|
+
try {
|
|
317
|
+
await stat(sourceDir);
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
throw new Error(`Skill "${options.source.skillName}" not found for ${options.source.agent} at ${sourceDir}.`);
|
|
321
|
+
}
|
|
322
|
+
await mkdir(path.dirname(targetDir), { recursive: true });
|
|
323
|
+
await rm(targetDir, { recursive: true, force: true });
|
|
324
|
+
await cp(sourceDir, targetDir, { recursive: true });
|
|
325
|
+
return { backups: [] };
|
|
326
|
+
}
|
|
327
|
+
export async function removeSkill(options) {
|
|
328
|
+
const skillDir = getSkillDir(options.location, options.cwd, options.homeDir);
|
|
329
|
+
const backup = await backupSkillDir(options, skillDir);
|
|
330
|
+
await rm(skillDir, { recursive: true, force: true });
|
|
331
|
+
return { backups: backup ? [backup] : [] };
|
|
332
|
+
}
|
|
333
|
+
export async function moveSkill(options) {
|
|
334
|
+
const sourceDir = getSkillDir(options.source, options.cwd, options.homeDir);
|
|
335
|
+
const targetDir = getSkillDir(options.target, options.cwd, options.homeDir);
|
|
336
|
+
try {
|
|
337
|
+
await stat(sourceDir);
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
// Idempotent: source already gone but destination present → done.
|
|
341
|
+
try {
|
|
342
|
+
await stat(targetDir);
|
|
343
|
+
return { backups: [] };
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
throw new Error(`Skill "${options.source.skillName}" not found for ${options.source.agent} at ${sourceDir}.`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
await mkdir(path.dirname(targetDir), { recursive: true });
|
|
350
|
+
await rm(targetDir, { recursive: true, force: true });
|
|
351
|
+
await cp(sourceDir, targetDir, { recursive: true });
|
|
352
|
+
const backup = await backupSkillDir(options, sourceDir);
|
|
353
|
+
await rm(sourceDir, { recursive: true, force: true });
|
|
354
|
+
return { backups: backup ? [backup] : [] };
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Copy a skill dir that is about to be deleted into the central backup area.
|
|
358
|
+
* Deliberately NOT a sibling `.bak.*` directory — the readers would list that
|
|
359
|
+
* as a new skill.
|
|
360
|
+
*/
|
|
361
|
+
async function backupSkillDir(options, skillDir) {
|
|
362
|
+
try {
|
|
363
|
+
await stat(skillDir);
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
const homeDir = options.homeDir ?? homedir();
|
|
369
|
+
const backupDir = path.join(homeDir, ".config", "nomoreide", "agent-env-backups", `${path.basename(skillDir)}.${formatTimestamp()}`);
|
|
370
|
+
await mkdir(path.dirname(backupDir), { recursive: true });
|
|
371
|
+
await cp(skillDir, backupDir, { recursive: true });
|
|
372
|
+
return backupDir;
|
|
373
|
+
}
|
|
374
|
+
/* ---- Snapshot: back up an agent's config file before bulk operations ---- */
|
|
375
|
+
export async function snapshotAgentConfig(options) {
|
|
376
|
+
const homeDir = options.homeDir ?? homedir();
|
|
377
|
+
const configPath = options.agent === "claude"
|
|
378
|
+
? path.join(homeDir, ".claude.json")
|
|
379
|
+
: options.agent === "codex"
|
|
380
|
+
? path.join(homeDir, ".codex", "config.toml")
|
|
381
|
+
: (await readAntigravityConfig({ cwd: options.cwd, homeDir })).configPath;
|
|
382
|
+
const backup = await backupFile(configPath);
|
|
383
|
+
return { backups: backup ? [backup] : [] };
|
|
384
|
+
}
|
|
385
|
+
//# sourceMappingURL=agent-env-writers.js.map
|