@kuznai/inception-engine 0.12.0 → 0.14.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
@@ -48,8 +48,8 @@ Managed skills overwrite their previous version. If a target exists but was not
48
48
  | Skills (SKILL.md) | All agents via manifest and CLI | All agents |
49
49
  | File write | All agents via manifest and CLI | All agents |
50
50
  | Config patch (JSON merge) | All agents via manifest and CLI | All agents |
51
- | MCP Servers | claude-code, gemini-cli; other agents are warned and skipped | claude-code, gemini-cli |
52
- | Global Rules Files | claude-code, codex, gemini-cli, opencode; github-copilot reads CLAUDE.md natively (deploy via claude-code); other agents are warned and skipped | claude-code, codex, gemini-cli, opencode |
51
+ | MCP Servers | claude-code, gemini-cli, codex, antigravity, opencode; github-copilot repo-scoped surfaces are warned and skipped | claude-code, gemini-cli, codex, antigravity, opencode |
52
+ | Global/Repo Rules Files | All agents (antigravity uses repo-local `.agents/rules/`); github-copilot reads CLAUDE.md natively (deploy via claude-code) | All agents |
53
53
  | `init` manifest generation | Scans `SKILL.md` directories and writes starter `skills` entries | N/A |
54
54
 
55
55
  Features that depend on agent-specific config surfaces are intentionally conservative: if a target path or schema is not implemented with enough confidence, inception-engine warns and skips it rather than guessing.
@@ -130,7 +130,12 @@ Each **mcpServer** entry registers an MCP server into the agent's config file by
130
130
  - **agents** - Array of agent IDs to register this server with
131
131
  - **config** - Raw server descriptor object. For the currently supported JSON-backed adapters, inception-engine requires at least one non-empty `command` or `url` field, validates `args` as an array of strings when present, and validates `env` as a string-to-string object when present. Additional keys are passed through verbatim.
132
132
 
133
- MCP server registration is currently supported for `claude-code` (`~/.claude.json`) and `gemini-cli` (`~/.gemini/settings.json`). Other agents emit a schema-aware warning and are skipped when their support would require a non-JSON adapter, such as Codex `config.toml`, Antigravity frontmatter in repo-scoped rules files, OpenCode `opencode.json`, or GitHub Copilot repo-scoped MCP surfaces. Revert removes the registered server entry from the config file.
133
+ MCP server registration is supported for all agents except GitHub Copilot. Inception-engine automatically uses the correct adapter for each agent's configuration schema:
134
+ - **JSON (Merge Patch)**: `claude-code` (`~/.claude.json`), `gemini-cli` (`~/.gemini/settings.json`), and `opencode` (`~/.config/opencode/opencode.json` using the custom `"mcp"` key).
135
+ - **TOML (Patch)**: `codex` (`~/.codex/config.toml`).
136
+ - **Markdown Frontmatter (Emit)**: `antigravity` (repo-local `.agents/rules/{name}.md` files).
137
+
138
+ Revert removes the registered server entry from the respective configuration file. GitHub Copilot, which uses repo-scoped MCP surfaces not yet implemented by inception-engine, continues to emit a schema-aware warning and is skipped.
134
139
 
135
140
  Each **agentRules** entry deploys a Markdown instruction file to an agent's supported global rules file location:
136
141
 
@@ -138,7 +143,7 @@ Each **agentRules** entry deploys a Markdown instruction file to an agent's supp
138
143
  - **path** - Relative path to the source Markdown file within the repo; supported global rules adapters require a `.md` or `.markdown` source path
139
144
  - **agents** - Array of agent IDs to deploy this file to
140
145
 
141
- Global rules-file deployment is currently supported for `claude-code` (`~/.claude/CLAUDE.md`), `codex` (`~/.codex/AGENTS.md`), `gemini-cli` (`~/.gemini/GEMINI.md`), and `opencode` (`~/.config/opencode/AGENTS.md`). For `github-copilot`, no separate deployment is needed because Copilot reads `CLAUDE.md` natively — target it via the `claude-code` agentRules entry and it reaches Copilot automatically. Other agents emit schema-aware warnings and are skipped when their instruction surfaces are repo-scoped, frontmatter-driven, or otherwise not implemented as a single global Markdown file. Revert removes the deployed rules file.
146
+ Instruction rule deployment is supported for all agents. For most agents, this targets a single global rules file (e.g., `~/.claude/CLAUDE.md` for `claude-code`, `~/.codex/AGENTS.md` for `codex`, `~/.gemini/GEMINI.md` for `gemini-cli`, and `~/.config/opencode/AGENTS.md` for `opencode`). For `antigravity`, inception-engine deploys to repo-local instruction surfaces at `{repo}/.agents/rules/{name}.md`. For `github-copilot`, no separate deployment is needed because Copilot reads `CLAUDE.md` natively — target it via the `claude-code` agentRules entry and it reaches Copilot automatically. Revert removes the deployed rules file.
142
147
 
143
148
  ## Creating Skills
144
149
 
@@ -54,9 +54,12 @@ export const AGENT_REGISTRY = [
54
54
  agentRules: "documented",
55
55
  },
56
56
  mcpSupport: {
57
- status: "unsupported",
58
- schemaLabel: "config.toml MCP schema",
59
- reason: "Codex stores MCP servers in config.toml, so JSON merge-patch deployment is not sufficient yet",
57
+ status: "supported",
58
+ schemaLabel: "TOML mcpServers config",
59
+ path: {
60
+ posix: ["{home}", ".codex", "config.toml"],
61
+ windows: ["{home}", ".codex", "config.toml"],
62
+ },
60
63
  },
61
64
  agentRulesSupport: {
62
65
  status: "supported",
@@ -121,14 +124,20 @@ export const AGENT_REGISTRY = [
121
124
  detectBinary: "provisional",
122
125
  },
123
126
  mcpSupport: {
124
- status: "unsupported",
125
- schemaLabel: "frontmatter-driven MCP rules",
126
- reason: "Antigravity MCP support depends on frontmatter inside repo-scoped .agents/rules files, which this release does not translate or validate yet",
127
+ status: "supported",
128
+ schemaLabel: "repo-local frontmatter-emit MCP rules",
129
+ path: {
130
+ posix: ["{repo}", ".agents", "rules", "{name}.md"],
131
+ windows: ["{repo}", ".agents", "rules", "{name}.md"],
132
+ },
127
133
  },
128
134
  agentRulesSupport: {
129
- status: "unsupported",
130
- schemaLabel: "repo-scoped rules files",
131
- reason: "Antigravity instructions live in repo-scoped .agents/rules files rather than one global Markdown target",
135
+ status: "supported",
136
+ schemaLabel: "repo-local Markdown rules file",
137
+ path: {
138
+ posix: ["{repo}", ".agents", "rules", "{name}.md"],
139
+ windows: ["{repo}", ".agents", "rules", "{name}.md"],
140
+ },
132
141
  },
133
142
  },
134
143
  {
@@ -150,9 +159,13 @@ export const AGENT_REGISTRY = [
150
159
  agentRules: "documented",
151
160
  },
152
161
  mcpSupport: {
153
- status: "unsupported",
154
- schemaLabel: "opencode.json MCP schema",
155
- reason: "OpenCode keeps MCP config in opencode.json under its own shape, so plain JSON merge-patch support is not implemented here",
162
+ status: "supported",
163
+ schemaLabel: "opencode.json MCP config",
164
+ path: {
165
+ posix: ["{xdg_config}", "opencode", "opencode.json"],
166
+ windows: ["{appdata}", "opencode", "opencode.json"],
167
+ },
168
+ mcpPatchKey: "mcp",
156
169
  },
157
170
  agentRulesSupport: {
158
171
  status: "supported",
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Serializes a flat (one-level) object to a YAML block suitable for use inside
3
+ * `---` frontmatter delimiters.
4
+ */
5
+ export declare function serializeFrontmatter(data: Record<string, unknown>): string;
6
+ /**
7
+ * Builds the full content of a frontmatter-bearing Markdown file.
8
+ * If `body` is provided it is appended after the closing delimiter.
9
+ */
10
+ export declare function buildFrontmatterDocument(frontmatter: Record<string, unknown>, body?: string): string;
11
+ /**
12
+ * Reads an existing `.md` file and parses its frontmatter (using the
13
+ * `front-matter` package). Returns the parsed attributes and raw body.
14
+ * Returns `{ attributes: {}, body: "" }` if the file does not exist.
15
+ */
16
+ export declare function readFrontmatterFile(filePath: string): Promise<{
17
+ attributes: Record<string, unknown>;
18
+ body: string;
19
+ }>;
20
+ /**
21
+ * Atomically writes a Markdown file with the given frontmatter and optional
22
+ * body. Preserves any existing body content when `preserveBody` is true.
23
+ */
24
+ export declare function writeFrontmatterFile(filePath: string, frontmatter: Record<string, unknown>, options?: {
25
+ preserveBody?: boolean;
26
+ }): Promise<void>;
@@ -0,0 +1,120 @@
1
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
+ import { createRequire } from "node:module";
3
+ import path from "node:path";
4
+ const _require = createRequire(import.meta.url);
5
+ // front-matter is a CommonJS module whose export is the callable parse function.
6
+ const parseFrontmatter = _require("front-matter");
7
+ // ---------------------------------------------------------------------------
8
+ // Hand-rolled flat-YAML serializer
9
+ // Handles the shapes found in MCP server descriptors: strings, numbers,
10
+ // booleans, string arrays, and one-level nested objects (e.g. "env").
11
+ // ---------------------------------------------------------------------------
12
+ function serializeScalar(value) {
13
+ if (typeof value === "string") {
14
+ const needsQuotes = /[:#[\]{},|>&*!'"@`]|^\s|\s$/.test(value);
15
+ return needsQuotes
16
+ ? `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
17
+ : value;
18
+ }
19
+ return String(value);
20
+ }
21
+ function serializeArray(key, arr, pad) {
22
+ if (arr.length === 0)
23
+ return `${pad}${key}: []`;
24
+ const items = arr.map((item) => {
25
+ const scalar = typeof item === "string" ||
26
+ typeof item === "number" ||
27
+ typeof item === "boolean"
28
+ ? serializeScalar(item)
29
+ : JSON.stringify(item);
30
+ return `${pad} - ${scalar}`;
31
+ });
32
+ return [`${pad}${key}:`, ...items].join("\n");
33
+ }
34
+ function serializeObject(key, obj, pad) {
35
+ const inner = serializeFrontmatterAtDepth(obj, `${pad} `);
36
+ return inner ? `${pad}${key}:\n${inner}` : `${pad}${key}: {}`;
37
+ }
38
+ function serializeFrontmatterAtDepth(data, pad) {
39
+ const lines = [];
40
+ for (const [key, value] of Object.entries(data)) {
41
+ if (value === null || value === undefined)
42
+ continue;
43
+ if (Array.isArray(value)) {
44
+ lines.push(serializeArray(key, value, pad));
45
+ }
46
+ else if (typeof value === "object") {
47
+ lines.push(serializeObject(key, value, pad));
48
+ }
49
+ else {
50
+ lines.push(`${pad}${key}: ${serializeScalar(value)}`);
51
+ }
52
+ }
53
+ return lines.join("\n");
54
+ }
55
+ /**
56
+ * Serializes a flat (one-level) object to a YAML block suitable for use inside
57
+ * `---` frontmatter delimiters.
58
+ */
59
+ export function serializeFrontmatter(data) {
60
+ return serializeFrontmatterAtDepth(data, "");
61
+ }
62
+ /**
63
+ * Builds the full content of a frontmatter-bearing Markdown file.
64
+ * If `body` is provided it is appended after the closing delimiter.
65
+ */
66
+ export function buildFrontmatterDocument(frontmatter, body = "") {
67
+ const serialized = serializeFrontmatter(frontmatter);
68
+ return `---\n${serialized}\n---\n${body}`;
69
+ }
70
+ function createAtomicTempPath(targetPath) {
71
+ return `${targetPath}.inception-tmp-${process.pid}-${Date.now()}-${Math.random()
72
+ .toString(36)
73
+ .slice(2)}`;
74
+ }
75
+ /**
76
+ * Reads an existing `.md` file and parses its frontmatter (using the
77
+ * `front-matter` package). Returns the parsed attributes and raw body.
78
+ * Returns `{ attributes: {}, body: "" }` if the file does not exist.
79
+ */
80
+ export async function readFrontmatterFile(filePath) {
81
+ let raw;
82
+ try {
83
+ raw = await readFile(filePath, "utf-8");
84
+ }
85
+ catch (err) {
86
+ const code = err.code;
87
+ if (code === "ENOENT")
88
+ return { attributes: {}, body: "" };
89
+ throw err;
90
+ }
91
+ const parsed = parseFrontmatter(raw);
92
+ return { attributes: parsed.attributes, body: parsed.body };
93
+ }
94
+ /**
95
+ * Atomically writes a Markdown file with the given frontmatter and optional
96
+ * body. Preserves any existing body content when `preserveBody` is true.
97
+ */
98
+ export async function writeFrontmatterFile(filePath, frontmatter, options = {}) {
99
+ let body = "";
100
+ if (options.preserveBody) {
101
+ const existing = await readFrontmatterFile(filePath);
102
+ body = existing.body;
103
+ }
104
+ const content = buildFrontmatterDocument(frontmatter, body);
105
+ const tempPath = createAtomicTempPath(filePath);
106
+ try {
107
+ await mkdir(path.dirname(filePath), { recursive: true });
108
+ await writeFile(tempPath, content, "utf-8");
109
+ await rename(tempPath, filePath);
110
+ }
111
+ catch (err) {
112
+ try {
113
+ await rm(tempPath, { force: true });
114
+ }
115
+ catch {
116
+ /* best-effort cleanup */
117
+ }
118
+ throw err;
119
+ }
120
+ }
@@ -1,11 +1,11 @@
1
1
  import type { AgentRuleEntry, McpServerEntry } from "../../schemas/manifest.ts";
2
- import type { AgentId, ConfigPatchDeployAction, FileWriteDeployAction, PlanWarning } from "../../types.ts";
2
+ import type { AgentId, ConfigPatchDeployAction, FileWriteDeployAction, FrontmatterEmitDeployAction, PlanWarning, TomlPatchDeployAction } from "../../types.ts";
3
3
  import { compileMcpServerReverts } from "./mcp.ts";
4
4
  import { compileAgentRuleReverts } from "./rules.ts";
5
5
  export { compileAgentRuleReverts, compileMcpServerReverts };
6
- export type AdapterAction = ConfigPatchDeployAction | FileWriteDeployAction;
6
+ export type AdapterAction = ConfigPatchDeployAction | FileWriteDeployAction | TomlPatchDeployAction | FrontmatterEmitDeployAction;
7
7
  export interface AdapterResult {
8
8
  actions: AdapterAction[];
9
9
  warnings: PlanWarning[];
10
10
  }
11
- export declare function compileAdapterActions(mcpServers: McpServerEntry[], agentRules: AgentRuleEntry[], sourceDir: string, resolvedSourceDir: string, realRoot: string, detectedAgents: AgentId[], home: string): Promise<AdapterResult>;
11
+ export declare function compileAdapterActions(mcpServers: McpServerEntry[], agentRules: AgentRuleEntry[], sourceDir: string, resolvedSourceDir: string, realRoot: string, detectedAgents: AgentId[], home: string, repo?: string): Promise<AdapterResult>;
@@ -1,16 +1,16 @@
1
1
  import { compileMcpServerActions, compileMcpServerReverts } from "./mcp.js";
2
2
  import { compileAgentRuleActions, compileAgentRuleReverts } from "./rules.js";
3
3
  export { compileAgentRuleReverts, compileMcpServerReverts };
4
- export async function compileAdapterActions(mcpServers, agentRules, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home) {
4
+ export async function compileAdapterActions(mcpServers, agentRules, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, repo) {
5
5
  const actions = [];
6
6
  const warnings = [];
7
7
  for (const entry of mcpServers) {
8
- const r = compileMcpServerActions(entry, detectedAgents, home);
8
+ const r = compileMcpServerActions(entry, detectedAgents, home, repo);
9
9
  actions.push(...r.actions);
10
10
  warnings.push(...r.warnings);
11
11
  }
12
12
  for (const entry of agentRules) {
13
- const r = await compileAgentRuleActions(entry, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home);
13
+ const r = await compileAgentRuleActions(entry, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, repo);
14
14
  actions.push(...r.actions);
15
15
  warnings.push(...r.warnings);
16
16
  }
@@ -1,8 +1,8 @@
1
1
  import type { McpServerEntry } from "../../schemas/manifest.ts";
2
- import type { AgentId, ConfigPatchDeployAction, ConfigPatchRevertAction, PlanWarning } from "../../types.ts";
2
+ import type { AgentId, ConfigPatchDeployAction, ConfigPatchRevertAction, FrontmatterEmitDeployAction, FrontmatterEmitRevertAction, PlanWarning, TomlPatchDeployAction, TomlPatchRevertAction } from "../../types.ts";
3
3
  export interface McpAdapterResult {
4
- actions: ConfigPatchDeployAction[];
4
+ actions: Array<ConfigPatchDeployAction | TomlPatchDeployAction | FrontmatterEmitDeployAction>;
5
5
  warnings: PlanWarning[];
6
6
  }
7
- export declare function compileMcpServerActions(entry: McpServerEntry, detectedAgents: AgentId[], home: string): McpAdapterResult;
8
- export declare function compileMcpServerReverts(entry: McpServerEntry, agentFilter: AgentId[] | null, home: string): ConfigPatchRevertAction[];
7
+ export declare function compileMcpServerActions(entry: McpServerEntry, detectedAgents: AgentId[], home: string, repo?: string): McpAdapterResult;
8
+ export declare function compileMcpServerReverts(entry: McpServerEntry, agentFilter: AgentId[] | null, home: string, repo?: string): Array<ConfigPatchRevertAction | TomlPatchRevertAction | FrontmatterEmitRevertAction>;
@@ -2,7 +2,51 @@ import path from "node:path";
2
2
  import { AGENT_REGISTRY_BY_ID } from "../../config/agents.js";
3
3
  import { getPlatformKey, resolvePlaceholders } from "../resolve.js";
4
4
  import { validateMcpServerConfigShape } from "../validation.js";
5
- export function compileMcpServerActions(entry, detectedAgents, home) {
5
+ /**
6
+ * Determines whether a resolved config path targets a TOML file.
7
+ */
8
+ function isTomlTarget(targetPath) {
9
+ return path.extname(targetPath).toLowerCase() === ".toml";
10
+ }
11
+ /**
12
+ * Determines whether a resolved config path targets a Markdown file
13
+ * (indicates a frontmatter-emit surface, e.g. Antigravity's .agents/rules/).
14
+ */
15
+ function isMarkdownTarget(targetPath) {
16
+ const ext = path.extname(targetPath).toLowerCase();
17
+ return ext === ".md" || ext === ".markdown";
18
+ }
19
+ function buildMcpDeployAction(entry, agentId, resolvedTarget, confidence, mcpPatchKey) {
20
+ if (isMarkdownTarget(resolvedTarget)) {
21
+ return {
22
+ kind: "frontmatter-emit",
23
+ skill: entry.name,
24
+ agent: agentId,
25
+ target: resolvedTarget,
26
+ frontmatter: { "mcp-servers": { [entry.name]: entry.config } },
27
+ confidence,
28
+ };
29
+ }
30
+ if (isTomlTarget(resolvedTarget)) {
31
+ return {
32
+ kind: "toml-patch",
33
+ skill: entry.name,
34
+ agent: agentId,
35
+ target: resolvedTarget,
36
+ config: entry.config,
37
+ confidence,
38
+ };
39
+ }
40
+ return {
41
+ kind: "config-patch",
42
+ skill: entry.name,
43
+ agent: agentId,
44
+ target: resolvedTarget,
45
+ patch: { [mcpPatchKey]: { [entry.name]: entry.config } },
46
+ confidence,
47
+ };
48
+ }
49
+ export function compileMcpServerActions(entry, detectedAgents, home, repo) {
6
50
  const actions = [];
7
51
  const warnings = [];
8
52
  const platform = getPlatformKey();
@@ -14,26 +58,20 @@ export function compileMcpServerActions(entry, detectedAgents, home) {
14
58
  if (!support || support.status === "unsupported") {
15
59
  warnings.push({
16
60
  kind: "confidence",
17
- message: `mcpServers: agent "${agentId}" uses ${support?.schemaLabel ?? "an unsupported MCP schema"} and ${support?.reason ?? "does not expose a supported MCP adapter"} — skipping "${entry.name}"`,
61
+ message: `mcpServers: agent "${agentId}" uses ${support?.schemaLabel ?? "an unsupported MCP schema"} and ${support?.status === "unsupported" ? support.reason : "does not expose a supported MCP adapter"} — skipping "${entry.name}"`,
18
62
  });
19
63
  continue;
20
64
  }
21
65
  validateMcpServerConfigShape(entry.config, entry.name, agentId);
22
- const target = resolvePlaceholders(support.path[platform], "", home);
23
- // Ensure no stray empty segments collapse the path unexpectedly
24
- const resolvedTarget = path.resolve(target);
25
- actions.push({
26
- kind: "config-patch",
27
- skill: entry.name,
28
- agent: agentId,
29
- target: resolvedTarget,
30
- patch: { mcpServers: { [entry.name]: entry.config } },
31
- confidence: agent.provenance.mcpConfig ?? "provisional",
32
- });
66
+ const rawTarget = resolvePlaceholders(support.path[platform], entry.name, home, repo);
67
+ const resolvedTarget = path.resolve(rawTarget);
68
+ const confidence = agent.provenance.mcpConfig ?? "provisional";
69
+ const mcpPatchKey = support.mcpPatchKey ?? "mcpServers";
70
+ actions.push(buildMcpDeployAction(entry, agentId, resolvedTarget, confidence, mcpPatchKey));
33
71
  }
34
72
  return { actions, warnings };
35
73
  }
36
- export function compileMcpServerReverts(entry, agentFilter, home) {
74
+ export function compileMcpServerReverts(entry, agentFilter, home, repo) {
37
75
  const actions = [];
38
76
  const platform = getPlatformKey();
39
77
  for (const agentId of entry.agents) {
@@ -43,13 +81,32 @@ export function compileMcpServerReverts(entry, agentFilter, home) {
43
81
  const support = agent?.mcpSupport;
44
82
  if (!support || support.status === "unsupported")
45
83
  continue;
46
- const target = path.resolve(resolvePlaceholders(support.path[platform], "", home));
47
- actions.push({
48
- kind: "config-patch",
49
- skill: entry.name,
50
- agent: agentId,
51
- target,
52
- });
84
+ const rawTarget = resolvePlaceholders(support.path[platform], entry.name, home, repo);
85
+ const target = path.resolve(rawTarget);
86
+ if (isMarkdownTarget(target)) {
87
+ actions.push({
88
+ kind: "frontmatter-emit",
89
+ skill: entry.name,
90
+ agent: agentId,
91
+ target,
92
+ });
93
+ }
94
+ else if (isTomlTarget(target)) {
95
+ actions.push({
96
+ kind: "toml-patch",
97
+ skill: entry.name,
98
+ agent: agentId,
99
+ target,
100
+ });
101
+ }
102
+ else {
103
+ actions.push({
104
+ kind: "config-patch",
105
+ skill: entry.name,
106
+ agent: agentId,
107
+ target,
108
+ });
109
+ }
53
110
  }
54
111
  return actions;
55
112
  }
@@ -4,5 +4,5 @@ export interface RulesAdapterResult {
4
4
  actions: FileWriteDeployAction[];
5
5
  warnings: PlanWarning[];
6
6
  }
7
- export declare function compileAgentRuleActions(entry: AgentRuleEntry, sourceDir: string, resolvedSourceDir: string, realRoot: string, detectedAgents: AgentId[], home: string): Promise<RulesAdapterResult>;
8
- export declare function compileAgentRuleReverts(entry: AgentRuleEntry, agentFilter: AgentId[] | null, home: string): FileWriteRevertAction[];
7
+ export declare function compileAgentRuleActions(entry: AgentRuleEntry, sourceDir: string, resolvedSourceDir: string, realRoot: string, detectedAgents: AgentId[], home: string, repo?: string): Promise<RulesAdapterResult>;
8
+ export declare function compileAgentRuleReverts(entry: AgentRuleEntry, agentFilter: AgentId[] | null, home: string, repo?: string): FileWriteRevertAction[];
@@ -2,7 +2,7 @@ import path from "node:path";
2
2
  import { AGENT_REGISTRY_BY_ID } from "../../config/agents.js";
3
3
  import { getPlatformKey, resolvePlaceholders } from "../resolve.js";
4
4
  import { validateAgentRuleMarkdownPath, validateSourceFile, validateSourcePath, } from "../validation.js";
5
- export async function compileAgentRuleActions(entry, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home) {
5
+ export async function compileAgentRuleActions(entry, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, repo) {
6
6
  const actions = [];
7
7
  const warnings = [];
8
8
  const platform = getPlatformKey();
@@ -17,14 +17,14 @@ export async function compileAgentRuleActions(entry, sourceDir, resolvedSourceDi
17
17
  if (!support || support.status === "unsupported") {
18
18
  warnings.push({
19
19
  kind: "confidence",
20
- message: `agentRules: agent "${agentId}" uses ${support?.schemaLabel ?? "an unsupported instruction schema"} and ${support?.reason ?? "does not expose a supported rules adapter"} — skipping "${entry.name}"`,
20
+ message: `agentRules: agent "${agentId}" uses ${support?.schemaLabel ?? "an unsupported instruction schema"} and ${support?.status === "unsupported" ? support.reason : "does not expose a supported rules adapter"} — skipping "${entry.name}"`,
21
21
  });
22
22
  continue;
23
23
  }
24
24
  supportedTargets.push({
25
25
  agentId,
26
26
  confidence: agent.provenance.agentRules ?? "provisional",
27
- target: resolvePlaceholders(support.path[platform], "", home),
27
+ target: resolvePlaceholders(support.path[platform], entry.name, home, repo),
28
28
  });
29
29
  }
30
30
  if (supportedTargets.length === 0) {
@@ -48,7 +48,7 @@ export async function compileAgentRuleActions(entry, sourceDir, resolvedSourceDi
48
48
  }
49
49
  return { actions, warnings };
50
50
  }
51
- export function compileAgentRuleReverts(entry, agentFilter, home) {
51
+ export function compileAgentRuleReverts(entry, agentFilter, home, repo) {
52
52
  const actions = [];
53
53
  const platform = getPlatformKey();
54
54
  for (const agentId of entry.agents) {
@@ -58,7 +58,7 @@ export function compileAgentRuleReverts(entry, agentFilter, home) {
58
58
  const support = agent?.agentRulesSupport;
59
59
  if (!support || support.status === "unsupported")
60
60
  continue;
61
- const target = resolvePlaceholders(support.path[platform], "", home);
61
+ const target = resolvePlaceholders(support.path[platform], entry.name, home, repo);
62
62
  actions.push({
63
63
  kind: "file-write",
64
64
  skill: entry.name,
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Reads and parses a TOML file. Returns an empty object if the file does not
3
+ * exist (treat a missing config.toml as an empty config).
4
+ */
5
+ export declare function readTomlConfig(filePath: string): Promise<Record<string, unknown>>;
6
+ /**
7
+ * Merges a named MCP server entry into `config.toml`'s `[mcpServers]` table.
8
+ * Returns the undo record (the previous value under that key, or null if it
9
+ * was absent) so revert can restore the exact prior state.
10
+ */
11
+ export declare function applyTomlMcpPatch(filePath: string, name: string, config: Record<string, unknown>): Promise<{
12
+ previousValue: unknown | null;
13
+ }>;
14
+ /**
15
+ * Removes a named MCP server entry from `config.toml`'s `[mcpServers]` table.
16
+ * If the entry is absent, this is a no-op. If the table becomes empty after
17
+ * removal, the `mcpServers` key itself is removed from the document.
18
+ */
19
+ export declare function revertTomlMcpPatch(filePath: string, name: string): Promise<void>;
@@ -0,0 +1,96 @@
1
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { parse, stringify } from "smol-toml";
4
+ /**
5
+ * Reads and parses a TOML file. Returns an empty object if the file does not
6
+ * exist (treat a missing config.toml as an empty config).
7
+ */
8
+ export async function readTomlConfig(filePath) {
9
+ let raw;
10
+ try {
11
+ raw = await readFile(filePath, "utf-8");
12
+ }
13
+ catch (err) {
14
+ const code = err.code;
15
+ if (code === "ENOENT")
16
+ return {};
17
+ throw err;
18
+ }
19
+ const parsed = parse(raw);
20
+ return parsed;
21
+ }
22
+ function createAtomicTempPath(targetPath) {
23
+ return `${targetPath}.inception-tmp-${process.pid}-${Date.now()}-${Math.random()
24
+ .toString(36)
25
+ .slice(2)}`;
26
+ }
27
+ async function writeTomlConfigAtomic(filePath, obj) {
28
+ const tempPath = createAtomicTempPath(filePath);
29
+ try {
30
+ await mkdir(path.dirname(filePath), { recursive: true });
31
+ await writeFile(tempPath, stringify(obj), "utf-8");
32
+ await rename(tempPath, filePath);
33
+ }
34
+ catch (err) {
35
+ try {
36
+ await rm(tempPath, { force: true });
37
+ }
38
+ catch {
39
+ /* best-effort cleanup */
40
+ }
41
+ throw err;
42
+ }
43
+ }
44
+ /**
45
+ * Merges a named MCP server entry into `config.toml`'s `[mcpServers]` table.
46
+ * Returns the undo record (the previous value under that key, or null if it
47
+ * was absent) so revert can restore the exact prior state.
48
+ */
49
+ export async function applyTomlMcpPatch(filePath, name, config) {
50
+ const current = await readTomlConfig(filePath);
51
+ const mcpServers = typeof current.mcpServers === "object" &&
52
+ current.mcpServers !== null &&
53
+ !Array.isArray(current.mcpServers)
54
+ ? current.mcpServers
55
+ : {};
56
+ const previousValue = Object.hasOwn(mcpServers, name)
57
+ ? mcpServers[name]
58
+ : null;
59
+ const patched = {
60
+ ...current,
61
+ mcpServers: { ...mcpServers, [name]: config },
62
+ };
63
+ await writeTomlConfigAtomic(filePath, patched);
64
+ return { previousValue };
65
+ }
66
+ /**
67
+ * Removes a named MCP server entry from `config.toml`'s `[mcpServers]` table.
68
+ * If the entry is absent, this is a no-op. If the table becomes empty after
69
+ * removal, the `mcpServers` key itself is removed from the document.
70
+ */
71
+ export async function revertTomlMcpPatch(filePath, name) {
72
+ let current;
73
+ try {
74
+ current = await readTomlConfig(filePath);
75
+ }
76
+ catch {
77
+ // File gone — nothing to revert.
78
+ return;
79
+ }
80
+ const mcpServers = typeof current.mcpServers === "object" &&
81
+ current.mcpServers !== null &&
82
+ !Array.isArray(current.mcpServers)
83
+ ? current.mcpServers
84
+ : {};
85
+ if (!Object.hasOwn(mcpServers, name))
86
+ return; // already absent
87
+ const { [name]: _removed, ...remainingServers } = mcpServers;
88
+ const reverted = { ...current };
89
+ if (Object.keys(remainingServers).length === 0) {
90
+ delete reverted.mcpServers;
91
+ }
92
+ else {
93
+ reverted.mcpServers = remainingServers;
94
+ }
95
+ await writeTomlConfigAtomic(filePath, reverted);
96
+ }