@kuznai/inception-engine 0.8.0 → 0.9.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
@@ -2,7 +2,7 @@
2
2
 
3
3
  Plant skills directly into the minds of your installed AI coding agents — Claude Code, Codex, Gemini CLI, Antigravity, OpenCode, and GitHub Copilot. One command. They'll think they thought of it themselves.
4
4
 
5
- Today, inception-engine deploys skills, single files, and JSON config patches to AI coding agents. MCP configuration and agent rules remain unimplemented at the manifest level.
5
+ Today, inception-engine deploys skills, single files, JSON config patches, MCP server registrations, and agent instruction files to AI coding agents.
6
6
 
7
7
  ## Quick Start
8
8
 
@@ -38,13 +38,13 @@ Managed skills overwrite their previous version. If a target exists but was not
38
38
 
39
39
  ### Feature Support
40
40
 
41
- | Feature | Status |
42
- |---|---|
43
- | Skills (SKILL.md) | Supported via manifest and CLI |
44
- | File write | Supported via manifest and CLI |
45
- | Config patch (JSON merge) | Supported via manifest and CLI |
46
- | MCP Servers | Accepted in manifest for forward compatibility, not implemented |
47
- | Agent Rules | Accepted in manifest for forward compatibility, not implemented |
41
+ | Feature | Deploy | Revert |
42
+ |---|---|---|
43
+ | Skills (SKILL.md) | All agents via manifest and CLI | All agents |
44
+ | File write | All agents via manifest and CLI | All agents |
45
+ | Config patch (JSON merge) | All agents via manifest and CLI | All agents |
46
+ | MCP Servers | claude-code, gemini-cli; stub warning for other agents | claude-code, gemini-cli |
47
+ | Agent Rules | claude-code, codex, gemini-cli, opencode; stub warning for other agents | claude-code, codex, gemini-cli, opencode |
48
48
 
49
49
  ## Manifest Format
50
50
 
@@ -75,8 +75,20 @@ Create an `inception.json` file at the root of your skills directory:
75
75
  "agents": ["claude-code"]
76
76
  }
77
77
  ],
78
- "mcpServers": [],
79
- "agentRules": []
78
+ "mcpServers": [
79
+ {
80
+ "name": "my-server",
81
+ "agents": ["claude-code", "gemini-cli"],
82
+ "config": { "command": "npx", "args": ["-y", "my-mcp-server"] }
83
+ }
84
+ ],
85
+ "agentRules": [
86
+ {
87
+ "name": "my-rules",
88
+ "path": "rules/CLAUDE.md",
89
+ "agents": ["claude-code"]
90
+ }
91
+ ]
80
92
  }
81
93
  ```
82
94
 
@@ -102,7 +114,21 @@ Each **config** entry applies a [JSON merge patch (RFC 7386)](https://datatracke
102
114
 
103
115
  The engine records an undo-patch for each config-patch deployment so that `revert` can restore the original values.
104
116
 
105
- `mcpServers` and `agentRules` are currently parsed for forward compatibility, but the deployment engine ignores them today.
117
+ Each **mcpServer** entry registers an MCP server into the agent's config file by applying a JSON merge patch under the `mcpServers` key:
118
+
119
+ - **name** - Unique identifier (same format as skill names); used as the server's key in the config
120
+ - **agents** - Array of agent IDs to register this server with
121
+ - **config** - Raw server descriptor object (e.g. `{ "command": "...", "args": [...] }` for stdio transport). The exact shape is passed through verbatim; agent adapters handle agent-specific requirements.
122
+
123
+ MCP server registration is currently supported for `claude-code` (`~/.claude.json`) and `gemini-cli` (`~/.gemini/settings.json`). Other agents emit a warning and are skipped. Revert removes the registered server entry from the config file.
124
+
125
+ Each **agentRules** entry deploys a Markdown instruction file to the agent's global rules location:
126
+
127
+ - **name** - Unique identifier (same format as skill names)
128
+ - **path** - Relative path to the source Markdown file within the repo
129
+ - **agents** - Array of agent IDs to deploy this file to
130
+
131
+ Agent rules 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`). Other agents emit a warning and are skipped. Revert removes the deployed rules file.
106
132
 
107
133
  ## Creating Skills
108
134
 
@@ -119,7 +145,7 @@ description: What this skill does and when to use it
119
145
  Instructions for the AI agent...
120
146
  ```
121
147
 
122
- The `name` and `description` fields in the frontmatter are required by most agents. The description determines when the agent activates the skill.
148
+ The `name` and `description` fields in the frontmatter are used by most agents. The description determines when the agent activates the skill. inception-engine does not currently validate the frontmatter — missing or malformed fields may cause the skill to be ignored or misbehave at the agent level.
123
149
 
124
150
  ## CLI Reference
125
151
 
@@ -201,7 +227,7 @@ inception-engine maintains a centralized deployment registry at `~/.inception-en
201
227
 
202
228
  - **Strong binding**: Each registry entry binds a specific target path to its skill, agent, and action kind, with action-specific provenance fields (`source` and `method` for skill-dir and file-write; `patch` and `undoPatch` for config-patch). A target is only considered managed if all relevant fields match — a stray entry or a different deployment cannot satisfy the check.
203
229
 
204
- - **Atomic redeploy**: When overwriting an existing managed target, the engine renames the old target to a backup, creates the new deployment, and only removes the backup on success. If the new deployment fails, the backup is restored.
230
+ - **Atomic redeploy**: When overwriting an existing managed `skill-dir` target, the engine renames the old target to a backup, creates the new deployment, and only removes the backup on success. If the new deployment fails, the backup is restored. `file-write` and `config-patch` deployments write directly to the target without this backup/rollback model.
205
231
 
206
232
  - **Cross-platform**: The registry uses the same resolved home directory as the rest of the tool, including sudo scenarios on POSIX and elevated PowerShell on Windows.
207
233
 
@@ -15,6 +15,16 @@ export const AGENT_REGISTRY = [
15
15
  skills: "documented",
16
16
  detectPaths: "documented",
17
17
  detectBinary: "documented",
18
+ mcpConfig: "documented",
19
+ agentRules: "documented",
20
+ },
21
+ mcpConfigPath: {
22
+ posix: ["{home}", ".claude.json"],
23
+ windows: ["{home}", ".claude.json"],
24
+ },
25
+ agentRulesPath: {
26
+ posix: ["{home}", ".claude", "CLAUDE.md"],
27
+ windows: ["{home}", ".claude", "CLAUDE.md"],
18
28
  },
19
29
  },
20
30
  {
@@ -33,6 +43,12 @@ export const AGENT_REGISTRY = [
33
43
  skills: "documented",
34
44
  detectPaths: "documented",
35
45
  detectBinary: "documented",
46
+ agentRules: "documented",
47
+ },
48
+ // mcpConfigPath omitted: Codex uses TOML config, not JSON — not supported by config-patch
49
+ agentRulesPath: {
50
+ posix: ["{home}", ".codex", "AGENTS.md"],
51
+ windows: ["{home}", ".codex", "AGENTS.md"],
36
52
  },
37
53
  },
38
54
  {
@@ -51,6 +67,16 @@ export const AGENT_REGISTRY = [
51
67
  skills: "documented",
52
68
  detectPaths: "documented",
53
69
  detectBinary: "documented",
70
+ mcpConfig: "documented",
71
+ agentRules: "documented",
72
+ },
73
+ mcpConfigPath: {
74
+ posix: ["{home}", ".gemini", "settings.json"],
75
+ windows: ["{home}", ".gemini", "settings.json"],
76
+ },
77
+ agentRulesPath: {
78
+ posix: ["{home}", ".gemini", "GEMINI.md"],
79
+ windows: ["{home}", ".gemini", "GEMINI.md"],
54
80
  },
55
81
  },
56
82
  {
@@ -70,6 +96,7 @@ export const AGENT_REGISTRY = [
70
96
  detectPaths: "implementation-only",
71
97
  detectBinary: "provisional",
72
98
  },
99
+ // mcpConfigPath and agentRulesPath omitted: paths not documented strongly enough for this release
73
100
  },
74
101
  {
75
102
  id: "opencode",
@@ -87,6 +114,12 @@ export const AGENT_REGISTRY = [
87
114
  skills: "documented",
88
115
  detectPaths: "documented",
89
116
  detectBinary: "documented",
117
+ agentRules: "documented",
118
+ },
119
+ // mcpConfigPath omitted: OpenCode uses opencode.json (TOML-adjacent format), not plain JSON
120
+ agentRulesPath: {
121
+ posix: ["{xdg_config}", "opencode", "AGENTS.md"],
122
+ windows: ["{appdata}", "opencode", "AGENTS.md"],
90
123
  },
91
124
  },
92
125
  {
@@ -106,6 +139,7 @@ export const AGENT_REGISTRY = [
106
139
  detectPaths: "documented",
107
140
  detectBinary: "documented",
108
141
  },
142
+ // mcpConfigPath and agentRulesPath omitted: rules live in repo, not home dir; MCP config unclear
109
143
  policyNote: "Organization policies may override locally deployed skills. Verify with your GitHub org admin if deployed skills are not active.",
110
144
  },
111
145
  ];
@@ -0,0 +1,11 @@
1
+ import type { McpServerEntry, AgentRuleEntry } from "../../schemas/manifest.ts";
2
+ import type { AgentId, ConfigPatchDeployAction, FileWriteDeployAction, PlanWarning } from "../../types.ts";
3
+ import { compileMcpServerReverts } from "./mcp.ts";
4
+ import { compileAgentRuleReverts } from "./rules.ts";
5
+ export { compileMcpServerReverts, compileAgentRuleReverts };
6
+ export type AdapterAction = ConfigPatchDeployAction | FileWriteDeployAction;
7
+ export interface AdapterResult {
8
+ actions: AdapterAction[];
9
+ warnings: PlanWarning[];
10
+ }
11
+ export declare function compileAdapterActions(mcpServers: McpServerEntry[], agentRules: AgentRuleEntry[], sourceDir: string, resolvedSourceDir: string, realRoot: string, detectedAgents: AgentId[], home: string): Promise<AdapterResult>;
@@ -0,0 +1,18 @@
1
+ import { compileMcpServerActions, compileMcpServerReverts } from "./mcp.js";
2
+ import { compileAgentRuleActions, compileAgentRuleReverts } from "./rules.js";
3
+ export { compileMcpServerReverts, compileAgentRuleReverts };
4
+ export async function compileAdapterActions(mcpServers, agentRules, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home) {
5
+ const actions = [];
6
+ const warnings = [];
7
+ for (const entry of mcpServers) {
8
+ const r = compileMcpServerActions(entry, detectedAgents, home);
9
+ actions.push(...r.actions);
10
+ warnings.push(...r.warnings);
11
+ }
12
+ for (const entry of agentRules) {
13
+ const r = await compileAgentRuleActions(entry, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home);
14
+ actions.push(...r.actions);
15
+ warnings.push(...r.warnings);
16
+ }
17
+ return { actions, warnings };
18
+ }
@@ -0,0 +1,8 @@
1
+ import type { McpServerEntry } from "../../schemas/manifest.ts";
2
+ import type { AgentId, ConfigPatchDeployAction, ConfigPatchRevertAction, PlanWarning } from "../../types.ts";
3
+ export interface McpAdapterResult {
4
+ actions: ConfigPatchDeployAction[];
5
+ warnings: PlanWarning[];
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[];
@@ -0,0 +1,51 @@
1
+ import path from "node:path";
2
+ import { AGENT_REGISTRY_BY_ID } from "../../config/agents.js";
3
+ import { getPlatformKey, resolvePlaceholders } from "../resolve.js";
4
+ export function compileMcpServerActions(entry, detectedAgents, home) {
5
+ const actions = [];
6
+ const warnings = [];
7
+ const platform = getPlatformKey();
8
+ for (const agentId of entry.agents) {
9
+ if (!detectedAgents.includes(agentId))
10
+ continue;
11
+ const agent = AGENT_REGISTRY_BY_ID[agentId];
12
+ if (!agent?.mcpConfigPath) {
13
+ warnings.push({
14
+ kind: "confidence",
15
+ message: `mcpServers: agent "${agentId}" does not have a documented MCP config path — skipping "${entry.name}"`,
16
+ });
17
+ continue;
18
+ }
19
+ const target = resolvePlaceholders(agent.mcpConfigPath[platform], "", home);
20
+ // Ensure no stray empty segments collapse the path unexpectedly
21
+ const resolvedTarget = path.resolve(target);
22
+ actions.push({
23
+ kind: "config-patch",
24
+ skill: entry.name,
25
+ agent: agentId,
26
+ target: resolvedTarget,
27
+ patch: { mcpServers: { [entry.name]: entry.config } },
28
+ confidence: agent.provenance.mcpConfig ?? "provisional",
29
+ });
30
+ }
31
+ return { actions, warnings };
32
+ }
33
+ export function compileMcpServerReverts(entry, agentFilter, home) {
34
+ const actions = [];
35
+ const platform = getPlatformKey();
36
+ for (const agentId of entry.agents) {
37
+ if (agentFilter && !agentFilter.includes(agentId))
38
+ continue;
39
+ const agent = AGENT_REGISTRY_BY_ID[agentId];
40
+ if (!agent?.mcpConfigPath)
41
+ continue;
42
+ const target = path.resolve(resolvePlaceholders(agent.mcpConfigPath[platform], "", home));
43
+ actions.push({
44
+ kind: "config-patch",
45
+ skill: entry.name,
46
+ agent: agentId,
47
+ target,
48
+ });
49
+ }
50
+ return actions;
51
+ }
@@ -0,0 +1,8 @@
1
+ import type { AgentRuleEntry } from "../../schemas/manifest.ts";
2
+ import type { AgentId, FileWriteDeployAction, FileWriteRevertAction, PlanWarning } from "../../types.ts";
3
+ export interface RulesAdapterResult {
4
+ actions: FileWriteDeployAction[];
5
+ warnings: PlanWarning[];
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[];
@@ -0,0 +1,54 @@
1
+ import path from "node:path";
2
+ import { AGENT_REGISTRY_BY_ID } from "../../config/agents.js";
3
+ import { getPlatformKey, resolvePlaceholders } from "../resolve.js";
4
+ import { validateSourceFile, validateSourcePath } from "../validation.js";
5
+ export async function compileAgentRuleActions(entry, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home) {
6
+ const actions = [];
7
+ const warnings = [];
8
+ const platform = getPlatformKey();
9
+ // Validate the source file once (before iterating agents) since it is shared.
10
+ const source = path.resolve(sourceDir, entry.path);
11
+ await validateSourcePath(source, entry.path, resolvedSourceDir, realRoot);
12
+ await validateSourceFile(source, entry.path);
13
+ for (const agentId of entry.agents) {
14
+ if (!detectedAgents.includes(agentId))
15
+ continue;
16
+ const agent = AGENT_REGISTRY_BY_ID[agentId];
17
+ if (!agent?.agentRulesPath) {
18
+ warnings.push({
19
+ kind: "confidence",
20
+ message: `agentRules: agent "${agentId}" does not have a documented rules file path — skipping "${entry.name}"`,
21
+ });
22
+ continue;
23
+ }
24
+ const target = resolvePlaceholders(agent.agentRulesPath[platform], "", home);
25
+ actions.push({
26
+ kind: "file-write",
27
+ skill: entry.name,
28
+ agent: agentId,
29
+ source,
30
+ target,
31
+ confidence: agent.provenance.agentRules ?? "provisional",
32
+ });
33
+ }
34
+ return { actions, warnings };
35
+ }
36
+ export function compileAgentRuleReverts(entry, agentFilter, home) {
37
+ const actions = [];
38
+ const platform = getPlatformKey();
39
+ for (const agentId of entry.agents) {
40
+ if (agentFilter && !agentFilter.includes(agentId))
41
+ continue;
42
+ const agent = AGENT_REGISTRY_BY_ID[agentId];
43
+ if (!agent?.agentRulesPath)
44
+ continue;
45
+ const target = resolvePlaceholders(agent.agentRulesPath[platform], "", home);
46
+ actions.push({
47
+ kind: "file-write",
48
+ skill: entry.name,
49
+ agent: agentId,
50
+ target,
51
+ });
52
+ }
53
+ return actions;
54
+ }
@@ -5,7 +5,9 @@ import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
5
5
  import { UserError } from "../errors.js";
6
6
  import { logger } from "../logger.js";
7
7
  import { lookupDeployment, registerDeployment, verifyDeployment, } from "./ownership.js";
8
+ import { compileAdapterActions } from "./adapters/index.js";
8
9
  import { getDeployMethod, resolveAgentSkillPath } from "./resolve.js";
10
+ import { sourceAccessError, validateSourceFile, validateSourcePath, } from "./validation.js";
9
11
  function isPlainObject(v) {
10
12
  return typeof v === "object" && v !== null && !Array.isArray(v);
11
13
  }
@@ -35,7 +37,13 @@ async function readJsonConfig(filePath) {
35
37
  function computeUndoPatch(original, patch) {
36
38
  const undoPatch = {};
37
39
  for (const key of Object.keys(patch)) {
38
- undoPatch[key] = key in original ? original[key] : null;
40
+ const patchVal = patch[key];
41
+ if (isPlainObject(patchVal) && isPlainObject(original[key])) {
42
+ undoPatch[key] = computeUndoPatch(original[key], patchVal);
43
+ }
44
+ else {
45
+ undoPatch[key] = key in original ? original[key] : null;
46
+ }
39
47
  }
40
48
  return undoPatch;
41
49
  }
@@ -45,21 +53,15 @@ function applyMergePatch(original, patch) {
45
53
  if (value === null) {
46
54
  delete patched[key];
47
55
  }
56
+ else if (isPlainObject(value) && isPlainObject(patched[key])) {
57
+ patched[key] = applyMergePatch(patched[key], value);
58
+ }
48
59
  else {
49
60
  patched[key] = value;
50
61
  }
51
62
  }
52
63
  return patched;
53
64
  }
54
- function sourceAccessError(err, sourcePath) {
55
- const code = err.code;
56
- if (code === "ENOENT")
57
- return `Source not found: ${sourcePath}`;
58
- if (code === "EACCES" || code === "EPERM")
59
- return `Permission denied accessing source: ${sourcePath}`;
60
- const detail = err instanceof Error ? err.message : String(err);
61
- return `Failed to access source ${sourcePath}: ${detail}`;
62
- }
63
65
  function resolveTargetTemplate(template, home) {
64
66
  const appdata = process.env.APPDATA ?? path.join(home, "AppData", "Roaming");
65
67
  const xdgRaw = process.env.XDG_CONFIG_HOME;
@@ -69,18 +71,6 @@ function resolveTargetTemplate(template, home) {
69
71
  .replace("{appdata}", appdata)
70
72
  .replace("{xdg_config}", xdgConfig);
71
73
  }
72
- async function validateSourceFile(sourcePath, manifestPath) {
73
- let stat;
74
- try {
75
- stat = await lstat(sourcePath);
76
- }
77
- catch (err) {
78
- throw new UserError("DEPLOY_FAILED", sourceAccessError(err, manifestPath));
79
- }
80
- if (!stat.isFile()) {
81
- throw new UserError("DEPLOY_FAILED", `Source is not a file: ${manifestPath}`);
82
- }
83
- }
84
74
  function detectCollisions(actions) {
85
75
  const seen = new Map();
86
76
  const warnings = [];
@@ -194,9 +184,12 @@ export async function planDeploy(manifest, sourceDir, detectedAgents, home) {
194
184
  ...(await planFileWriteActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home)),
195
185
  ...planConfigPatchActions(manifest, detectedAgents, home),
196
186
  ];
187
+ const adapterResult = await compileAdapterActions(manifest.mcpServers, manifest.agentRules, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home);
188
+ actions.push(...adapterResult.actions);
197
189
  const warnings = [
198
190
  ...detectAmbiguities(detectedAgents),
199
191
  ...detectCollisions(actions),
192
+ ...adapterResult.warnings,
200
193
  ];
201
194
  return { actions, warnings };
202
195
  }
@@ -386,23 +379,6 @@ async function deployConfigPatch(action, dryRun, verbose, home, planned) {
386
379
  return { error: msg };
387
380
  }
388
381
  }
389
- async function validateSourcePath(source, skillPath, resolvedSourceDir, realRoot) {
390
- if (!source.startsWith(resolvedSourceDir + path.sep)) {
391
- throw new UserError("DEPLOY_FAILED", `Skill path "${skillPath}" resolves outside the repository root: ${source}`);
392
- }
393
- try {
394
- const realSource = await realpath(source);
395
- if (realSource !== realRoot &&
396
- !realSource.startsWith(realRoot + path.sep)) {
397
- throw new UserError("DEPLOY_FAILED", `Skill path "${skillPath}" resolves outside the repository root via symlink: ${source} -> ${realSource}`);
398
- }
399
- }
400
- catch (err) {
401
- if (err instanceof UserError)
402
- throw err;
403
- // Source doesn't exist yet — will be caught during execute
404
- }
405
- }
406
382
  async function validateSkillContract(source, skillPath) {
407
383
  let stat;
408
384
  try {
@@ -9,3 +9,4 @@ export declare function resolveAgentSkillPathFor(agent: AgentConfig, skillName:
9
9
  export declare function resolveAgentDetectPathFor(agent: AgentConfig, home: string, platform: "posix" | "windows"): string;
10
10
  export declare function resolveAgentSkillPath(agent: AgentConfig, skillName: string, home: string): string;
11
11
  export declare function resolveAgentDetectPath(agent: AgentConfig, home: string): string;
12
+ export declare function resolvePlaceholders(segments: string[], skillName: string, home: string): string;
@@ -96,7 +96,7 @@ export function resolveAgentSkillPath(agent, skillName, home) {
96
96
  export function resolveAgentDetectPath(agent, home) {
97
97
  return resolveAgentDetectPathFor(agent, home, getPlatformKey());
98
98
  }
99
- function resolvePlaceholders(segments, skillName, home) {
99
+ export function resolvePlaceholders(segments, skillName, home) {
100
100
  const appdata = process.env.APPDATA ?? path.join(home, "AppData", "Roaming");
101
101
  const xdgRaw = process.env.XDG_CONFIG_HOME;
102
102
  const xdgConfig = xdgRaw && path.isAbsolute(xdgRaw) ? xdgRaw : path.join(home, ".config");
@@ -2,6 +2,7 @@ import { lstat, readFile, rm, unlink, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
4
4
  import { logger } from "../logger.js";
5
+ import { compileMcpServerReverts, compileAgentRuleReverts, } from "./adapters/index.js";
5
6
  import { lookupDeployment, unregisterDeployment } from "./ownership.js";
6
7
  import { resolveAgentSkillPath } from "./resolve.js";
7
8
  function resolveTargetTemplate(template, home) {
@@ -75,6 +76,8 @@ export function planRevert(manifest, detectedAgents, home) {
75
76
  ...buildSkillDirReverts(manifest, home, detectedAgents),
76
77
  ...buildFileWriteReverts(manifest, home, detectedAgents),
77
78
  ...buildConfigPatchReverts(manifest, home, detectedAgents),
79
+ ...(manifest.mcpServers ?? []).flatMap((e) => compileMcpServerReverts(e, detectedAgents, home)),
80
+ ...(manifest.agentRules ?? []).flatMap((e) => compileAgentRuleReverts(e, detectedAgents, home)),
78
81
  ];
79
82
  }
80
83
  export function planRevertAll(manifest, home) {
@@ -82,6 +85,8 @@ export function planRevertAll(manifest, home) {
82
85
  ...buildSkillDirReverts(manifest, home, null),
83
86
  ...buildFileWriteReverts(manifest, home, null),
84
87
  ...buildConfigPatchReverts(manifest, home, null),
88
+ ...(manifest.mcpServers ?? []).flatMap((e) => compileMcpServerReverts(e, null, home)),
89
+ ...(manifest.agentRules ?? []).flatMap((e) => compileAgentRuleReverts(e, null, home)),
85
90
  ];
86
91
  }
87
92
  function recordOutcome(result, action, counts, failed) {
@@ -109,12 +114,18 @@ async function readJsonConfig(filePath) {
109
114
  }
110
115
  return parsed;
111
116
  }
117
+ function isPlainObject(v) {
118
+ return typeof v === "object" && v !== null && !Array.isArray(v);
119
+ }
112
120
  function applyUndoPatch(current, undoPatch) {
113
121
  const restored = { ...current };
114
122
  for (const [key, originalValue] of Object.entries(undoPatch)) {
115
123
  if (originalValue === null) {
116
124
  delete restored[key];
117
125
  }
126
+ else if (isPlainObject(originalValue) && isPlainObject(restored[key])) {
127
+ restored[key] = applyUndoPatch(restored[key], originalValue);
128
+ }
118
129
  else {
119
130
  restored[key] = originalValue;
120
131
  }
@@ -0,0 +1,3 @@
1
+ export declare function sourceAccessError(err: unknown, sourcePath: string): string;
2
+ export declare function validateSourcePath(source: string, skillPath: string, resolvedSourceDir: string, realRoot: string): Promise<void>;
3
+ export declare function validateSourceFile(sourcePath: string, manifestPath: string): Promise<void>;
@@ -0,0 +1,41 @@
1
+ import { lstat, realpath } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { UserError } from "../errors.js";
4
+ export function sourceAccessError(err, sourcePath) {
5
+ const code = err.code;
6
+ if (code === "ENOENT")
7
+ return `Source not found: ${sourcePath}`;
8
+ if (code === "EACCES" || code === "EPERM")
9
+ return `Permission denied accessing source: ${sourcePath}`;
10
+ const detail = err instanceof Error ? err.message : String(err);
11
+ return `Failed to access source ${sourcePath}: ${detail}`;
12
+ }
13
+ export async function validateSourcePath(source, skillPath, resolvedSourceDir, realRoot) {
14
+ if (!source.startsWith(resolvedSourceDir + path.sep)) {
15
+ throw new UserError("DEPLOY_FAILED", `Skill path "${skillPath}" resolves outside the repository root: ${source}`);
16
+ }
17
+ try {
18
+ const realSource = await realpath(source);
19
+ if (realSource !== realRoot &&
20
+ !realSource.startsWith(realRoot + path.sep)) {
21
+ throw new UserError("DEPLOY_FAILED", `Skill path "${skillPath}" resolves outside the repository root via symlink: ${source} -> ${realSource}`);
22
+ }
23
+ }
24
+ catch (err) {
25
+ if (err instanceof UserError)
26
+ throw err;
27
+ // Source doesn't exist yet — will be caught during execute
28
+ }
29
+ }
30
+ export async function validateSourceFile(sourcePath, manifestPath) {
31
+ let stat;
32
+ try {
33
+ stat = await lstat(sourcePath);
34
+ }
35
+ catch (err) {
36
+ throw new UserError("DEPLOY_FAILED", sourceAccessError(err, manifestPath));
37
+ }
38
+ if (!stat.isFile()) {
39
+ throw new UserError("DEPLOY_FAILED", `Source is not a file: ${manifestPath}`);
40
+ }
41
+ }
@@ -50,10 +50,28 @@ export declare const ConfigEntrySchema: z.ZodObject<{
50
50
  }, z.core.$strip>;
51
51
  export declare const McpServerEntrySchema: z.ZodObject<{
52
52
  name: z.ZodString;
53
- }, z.core.$loose>;
53
+ agents: z.ZodPipe<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodEnum<{
54
+ "claude-code": "claude-code";
55
+ codex: "codex";
56
+ "gemini-cli": "gemini-cli";
57
+ antigravity: "antigravity";
58
+ opencode: "opencode";
59
+ "github-copilot": "github-copilot";
60
+ }>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
61
+ config: z.ZodRecord<z.ZodString, z.ZodUnknown>;
62
+ }, z.core.$strip>;
54
63
  export declare const AgentRuleEntrySchema: z.ZodObject<{
55
64
  name: z.ZodString;
56
- }, z.core.$loose>;
65
+ agents: z.ZodPipe<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodEnum<{
66
+ "claude-code": "claude-code";
67
+ codex: "codex";
68
+ "gemini-cli": "gemini-cli";
69
+ antigravity: "antigravity";
70
+ opencode: "opencode";
71
+ "github-copilot": "github-copilot";
72
+ }>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
73
+ path: z.ZodString;
74
+ }, z.core.$strip>;
57
75
  export declare const ManifestSchema: z.ZodObject<{
58
76
  skills: z.ZodArray<z.ZodObject<{
59
77
  name: z.ZodString;
@@ -95,10 +113,28 @@ export declare const ManifestSchema: z.ZodObject<{
95
113
  }, z.core.$strip>>>;
96
114
  mcpServers: z.ZodDefault<z.ZodArray<z.ZodObject<{
97
115
  name: z.ZodString;
98
- }, z.core.$loose>>>;
116
+ agents: z.ZodPipe<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodEnum<{
117
+ "claude-code": "claude-code";
118
+ codex: "codex";
119
+ "gemini-cli": "gemini-cli";
120
+ antigravity: "antigravity";
121
+ opencode: "opencode";
122
+ "github-copilot": "github-copilot";
123
+ }>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
124
+ config: z.ZodRecord<z.ZodString, z.ZodUnknown>;
125
+ }, z.core.$strip>>>;
99
126
  agentRules: z.ZodDefault<z.ZodArray<z.ZodObject<{
100
127
  name: z.ZodString;
101
- }, z.core.$loose>>>;
128
+ agents: z.ZodPipe<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodEnum<{
129
+ "claude-code": "claude-code";
130
+ codex: "codex";
131
+ "gemini-cli": "gemini-cli";
132
+ antigravity: "antigravity";
133
+ opencode: "opencode";
134
+ "github-copilot": "github-copilot";
135
+ }>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
136
+ path: z.ZodString;
137
+ }, z.core.$strip>>>;
102
138
  }, z.core.$strip>;
103
139
  export type SkillEntry = z.infer<typeof SkillEntrySchema>;
104
140
  export type FileEntry = z.infer<typeof FileEntrySchema>;
@@ -71,20 +71,19 @@ export const ConfigEntrySchema = z.object({
71
71
  patch: z.record(z.string(), z.unknown()),
72
72
  agents: agentsField,
73
73
  });
74
- export const McpServerEntrySchema = z
75
- .object({
76
- name: z
77
- .string({ message: "mcpServers entry name must be a non-empty string" })
78
- .min(1, { message: "mcpServers entry name must be a non-empty string" }),
79
- })
80
- .passthrough();
81
- export const AgentRuleEntrySchema = z
82
- .object({
83
- name: z
84
- .string({ message: "agentRules entry name must be a non-empty string" })
85
- .min(1, { message: "agentRules entry name must be a non-empty string" }),
86
- })
87
- .passthrough();
74
+ export const McpServerEntrySchema = z.object({
75
+ name: nameField,
76
+ agents: agentsField,
77
+ // Raw server descriptor passed verbatim to each agent adapter.
78
+ // Adapters are responsible for validating the shape they need.
79
+ config: z.record(z.string(), z.unknown()),
80
+ });
81
+ export const AgentRuleEntrySchema = z.object({
82
+ name: nameField,
83
+ agents: agentsField,
84
+ // Relative path to the rules/instruction file within the source bundle.
85
+ path: sourcePathField,
86
+ });
88
87
  export const ManifestSchema = z.object({
89
88
  skills: z.array(SkillEntrySchema).superRefine((skills, ctx) => {
90
89
  const seen = new Set();
package/dist/types.d.ts CHANGED
@@ -9,6 +9,8 @@ export interface AgentProvenance {
9
9
  skills: Confidence;
10
10
  detectPaths: Confidence;
11
11
  detectBinary: Confidence;
12
+ mcpConfig?: Confidence;
13
+ agentRules?: Confidence;
12
14
  }
13
15
  export interface AgentConfig {
14
16
  id: AgentId;
@@ -17,6 +19,8 @@ export interface AgentConfig {
17
19
  detectPaths: AgentPaths;
18
20
  detectBinary: string | null;
19
21
  provenance: AgentProvenance;
22
+ mcpConfigPath?: AgentPaths;
23
+ agentRulesPath?: AgentPaths;
20
24
  policyNote?: string;
21
25
  }
22
26
  export interface PlanWarning {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kuznai/inception-engine",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Deploy AI agent skills from a git repo to user home directories",
5
5
  "license": "MIT",
6
6
  "author": "Damian Piątkowski",