@kuznai/inception-engine 0.7.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 is a skills deployer. The public manifest and CLI planner currently deploy and revert skill directories only. The core executor also has internal support for single-file write and JSON config patch actions, but the manifest format does not expose them yet. MCP configuration and agent-specific config patching 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 | Internal executor support only; not exposed in manifest or CLI planning |
45
- | Config patch (JSON merge) | Internal executor support only; not exposed in manifest or CLI planning |
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
 
@@ -59,18 +59,76 @@ Create an `inception.json` file at the root of your skills directory:
59
59
  "agents": ["claude-code", "codex", "gemini-cli", "antigravity", "opencode", "github-copilot"]
60
60
  }
61
61
  ],
62
- "mcpServers": [],
63
- "agentRules": []
62
+ "files": [
63
+ {
64
+ "name": "my-settings",
65
+ "path": "files/settings.json",
66
+ "target": "{home}/.claude/settings.json",
67
+ "agents": ["claude-code"]
68
+ }
69
+ ],
70
+ "configs": [
71
+ {
72
+ "name": "enable-feature",
73
+ "target": "{home}/.claude/settings.json",
74
+ "patch": { "someFeature": true },
75
+ "agents": ["claude-code"]
76
+ }
77
+ ],
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
+ ]
64
92
  }
65
93
  ```
66
94
 
67
- Each skill entry has:
95
+ Each **skill** entry has:
68
96
 
69
- - **name** - Unique skill identifier using letters, digits, dots, underscores, or hyphens; it must not start with a dot
97
+ - **name** - Unique identifier using letters, digits, dots, underscores, or hyphens; must not start with a dot
70
98
  - **path** - Relative path to the skill directory within the repo
71
99
  - **agents** - Array of agent IDs to deploy this skill to. If an agent isn't installed, it's skipped.
72
100
 
73
- `mcpServers` and `agentRules` are currently parsed for forward compatibility, but the deployment engine ignores them today. The manifest-driven planner and revert flow currently derive actions from `skills` entries only.
101
+ Each **file** entry deploys a single file to an agent's configuration location:
102
+
103
+ - **name** - Unique identifier (same format as skill names)
104
+ - **path** - Relative path to the source file within the repo
105
+ - **target** - Destination path using a placeholder prefix: `{home}`, `{appdata}` (Windows), or `{xdg_config}` (Linux). For example: `{home}/.claude/settings.json`
106
+ - **agents** - Array of agent IDs to deploy this file to
107
+
108
+ Each **config** entry applies a [JSON merge patch (RFC 7386)](https://datatracker.ietf.org/doc/html/rfc7386) to an existing agent config file:
109
+
110
+ - **name** - Unique identifier (same format as skill names)
111
+ - **target** - Config file to patch, using the same placeholder prefix as file entries
112
+ - **patch** - JSON object of keys to set. A `null` value removes the key from the target file. Non-null values are set directly (deep merge is not applied).
113
+ - **agents** - Array of agent IDs to apply this patch to
114
+
115
+ The engine records an undo-patch for each config-patch deployment so that `revert` can restore the original values.
116
+
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.
74
132
 
75
133
  ## Creating Skills
76
134
 
@@ -87,7 +145,7 @@ description: What this skill does and when to use it
87
145
  Instructions for the AI agent...
88
146
  ```
89
147
 
90
- 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.
91
149
 
92
150
  ## CLI Reference
93
151
 
@@ -169,7 +227,7 @@ inception-engine maintains a centralized deployment registry at `~/.inception-en
169
227
 
170
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.
171
229
 
172
- - **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.
173
231
 
174
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.
175
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,8 @@ 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
143
+ policyNote: "Organization policies may override locally deployed skills. Verify with your GitHub org admin if deployed skills are not active.",
109
144
  },
110
145
  ];
111
146
  export const AGENT_REGISTRY_BY_ID = Object.fromEntries(AGENT_REGISTRY.map((a) => [a.id, a]));
@@ -38,9 +38,12 @@ function validateManifest(data, filePath) {
38
38
  if (issuePath.length === 1 && issuePath[0] === "skills") {
39
39
  throw new UserError("MANIFEST_INVALID", `${filePath}: "skills" must be an array`);
40
40
  }
41
- // Top-level "mcpServers" or "agentRules": wrong type → uniform message
41
+ // Top-level array fields with wrong type → uniform message
42
42
  if (issuePath.length === 1 &&
43
- (issuePath[0] === "mcpServers" || issuePath[0] === "agentRules")) {
43
+ (issuePath[0] === "mcpServers" ||
44
+ issuePath[0] === "agentRules" ||
45
+ issuePath[0] === "files" ||
46
+ issuePath[0] === "configs")) {
44
47
  throw new UserError("MANIFEST_INVALID", `${filePath}: "${issuePath[0]}" must be an array`);
45
48
  }
46
49
  throw new UserError("MANIFEST_INVALID", `${filePath}: ${formatZodPath(issuePath)}${issue.message}`);
@@ -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
+ }
@@ -1,10 +1,13 @@
1
+ import { constants } from "node:fs";
1
2
  import { access, copyFile, cp, lstat, mkdir, readFile, realpath, rename, rm, symlink, unlink, writeFile, } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
4
5
  import { UserError } from "../errors.js";
5
6
  import { logger } from "../logger.js";
6
7
  import { lookupDeployment, registerDeployment, verifyDeployment, } from "./ownership.js";
8
+ import { compileAdapterActions } from "./adapters/index.js";
7
9
  import { getDeployMethod, resolveAgentSkillPath } from "./resolve.js";
10
+ import { sourceAccessError, validateSourceFile, validateSourcePath, } from "./validation.js";
8
11
  function isPlainObject(v) {
9
12
  return typeof v === "object" && v !== null && !Array.isArray(v);
10
13
  }
@@ -34,7 +37,13 @@ async function readJsonConfig(filePath) {
34
37
  function computeUndoPatch(original, patch) {
35
38
  const undoPatch = {};
36
39
  for (const key of Object.keys(patch)) {
37
- 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
+ }
38
47
  }
39
48
  return undoPatch;
40
49
  }
@@ -44,20 +53,23 @@ function applyMergePatch(original, patch) {
44
53
  if (value === null) {
45
54
  delete patched[key];
46
55
  }
56
+ else if (isPlainObject(value) && isPlainObject(patched[key])) {
57
+ patched[key] = applyMergePatch(patched[key], value);
58
+ }
47
59
  else {
48
60
  patched[key] = value;
49
61
  }
50
62
  }
51
63
  return patched;
52
64
  }
53
- function sourceAccessError(err, sourcePath) {
54
- const code = err.code;
55
- if (code === "ENOENT")
56
- return `Source not found: ${sourcePath}`;
57
- if (code === "EACCES" || code === "EPERM")
58
- return `Permission denied accessing source: ${sourcePath}`;
59
- const detail = err instanceof Error ? err.message : String(err);
60
- return `Failed to access source ${sourcePath}: ${detail}`;
65
+ function resolveTargetTemplate(template, home) {
66
+ const appdata = process.env.APPDATA ?? path.join(home, "AppData", "Roaming");
67
+ const xdgRaw = process.env.XDG_CONFIG_HOME;
68
+ const xdgConfig = xdgRaw && path.isAbsolute(xdgRaw) ? xdgRaw : path.join(home, ".config");
69
+ return template
70
+ .replace("{home}", home)
71
+ .replace("{appdata}", appdata)
72
+ .replace("{xdg_config}", xdgConfig);
61
73
  }
62
74
  function detectCollisions(actions) {
63
75
  const seen = new Map();
@@ -87,17 +99,9 @@ function detectAmbiguities(detectedAgents) {
87
99
  }
88
100
  return warnings;
89
101
  }
90
- export async function planDeploy(manifest, sourceDir, detectedAgents, home) {
102
+ async function planSkillDirActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home) {
91
103
  const method = getDeployMethod();
92
104
  const actions = [];
93
- const resolvedSourceDir = path.resolve(sourceDir);
94
- let realRoot;
95
- try {
96
- realRoot = await realpath(resolvedSourceDir);
97
- }
98
- catch {
99
- realRoot = resolvedSourceDir;
100
- }
101
105
  for (const skill of manifest.skills) {
102
106
  const source = path.resolve(sourceDir, skill.path);
103
107
  await validateSourcePath(source, skill.path, resolvedSourceDir, realRoot);
@@ -108,21 +112,84 @@ export async function planDeploy(manifest, sourceDir, detectedAgents, home) {
108
112
  const agent = AGENT_REGISTRY_BY_ID[agentId];
109
113
  if (!agent)
110
114
  continue;
111
- const target = resolveAgentSkillPath(agent, skill.name, home);
112
115
  actions.push({
113
116
  kind: "skill-dir",
114
117
  skill: skill.name,
115
118
  agent: agentId,
116
119
  source,
117
- target,
120
+ target: resolveAgentSkillPath(agent, skill.name, home),
118
121
  method,
119
122
  confidence: agent.provenance.skills,
120
123
  });
121
124
  }
122
125
  }
126
+ return actions;
127
+ }
128
+ async function planFileWriteActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home) {
129
+ const actions = [];
130
+ for (const fileEntry of manifest.files ?? []) {
131
+ const source = path.resolve(sourceDir, fileEntry.path);
132
+ await validateSourcePath(source, fileEntry.path, resolvedSourceDir, realRoot);
133
+ await validateSourceFile(source, fileEntry.path);
134
+ for (const agentId of fileEntry.agents) {
135
+ if (!detectedAgents.includes(agentId))
136
+ continue;
137
+ const agent = AGENT_REGISTRY_BY_ID[agentId];
138
+ if (!agent)
139
+ continue;
140
+ actions.push({
141
+ kind: "file-write",
142
+ skill: fileEntry.name,
143
+ agent: agentId,
144
+ source,
145
+ target: resolveTargetTemplate(fileEntry.target, home),
146
+ confidence: agent.provenance.skills,
147
+ });
148
+ }
149
+ }
150
+ return actions;
151
+ }
152
+ function planConfigPatchActions(manifest, detectedAgents, home) {
153
+ const actions = [];
154
+ for (const configEntry of manifest.configs ?? []) {
155
+ for (const agentId of configEntry.agents) {
156
+ if (!detectedAgents.includes(agentId))
157
+ continue;
158
+ const agent = AGENT_REGISTRY_BY_ID[agentId];
159
+ if (!agent)
160
+ continue;
161
+ actions.push({
162
+ kind: "config-patch",
163
+ skill: configEntry.name,
164
+ agent: agentId,
165
+ target: resolveTargetTemplate(configEntry.target, home),
166
+ patch: configEntry.patch,
167
+ confidence: agent.provenance.skills,
168
+ });
169
+ }
170
+ }
171
+ return actions;
172
+ }
173
+ export async function planDeploy(manifest, sourceDir, detectedAgents, home) {
174
+ const resolvedSourceDir = path.resolve(sourceDir);
175
+ let realRoot;
176
+ try {
177
+ realRoot = await realpath(resolvedSourceDir);
178
+ }
179
+ catch {
180
+ realRoot = resolvedSourceDir;
181
+ }
182
+ const actions = [
183
+ ...(await planSkillDirActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home)),
184
+ ...(await planFileWriteActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home)),
185
+ ...planConfigPatchActions(manifest, detectedAgents, home),
186
+ ];
187
+ const adapterResult = await compileAdapterActions(manifest.mcpServers, manifest.agentRules, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home);
188
+ actions.push(...adapterResult.actions);
123
189
  const warnings = [
124
190
  ...detectAmbiguities(detectedAgents),
125
191
  ...detectCollisions(actions),
192
+ ...adapterResult.warnings,
126
193
  ];
127
194
  return { actions, warnings };
128
195
  }
@@ -180,8 +247,6 @@ async function deploySkillDir(action, dryRun, verbose, home, planned) {
180
247
  return { error: msg };
181
248
  }
182
249
  if (dryRun) {
183
- logger.plan(label);
184
- logger.detail(`${action.method}: ${action.source} -> ${action.target}`);
185
250
  planned.push({
186
251
  verb: action.method === "symlink" ? "create-symlink" : "copy-dir",
187
252
  kind: "skill-dir",
@@ -215,8 +280,6 @@ async function deployFileWrite(action, dryRun, verbose, home, planned) {
215
280
  return { error: msg };
216
281
  }
217
282
  if (dryRun) {
218
- logger.plan(label);
219
- logger.detail(`write-file: ${action.source} -> ${action.target}`);
220
283
  planned.push({
221
284
  verb: "write-file",
222
285
  kind: "file-write",
@@ -275,8 +338,6 @@ async function deployConfigPatch(action, dryRun, verbose, home, planned) {
275
338
  }
276
339
  const patch = action.patch;
277
340
  if (dryRun) {
278
- logger.plan(label);
279
- logger.detail(`patch-config: ${JSON.stringify(patch)} -> ${action.target}`);
280
341
  planned.push({
281
342
  verb: "patch-config",
282
343
  kind: "config-patch",
@@ -318,23 +379,6 @@ async function deployConfigPatch(action, dryRun, verbose, home, planned) {
318
379
  return { error: msg };
319
380
  }
320
381
  }
321
- async function validateSourcePath(source, skillPath, resolvedSourceDir, realRoot) {
322
- if (!source.startsWith(resolvedSourceDir + path.sep)) {
323
- throw new UserError("DEPLOY_FAILED", `Skill path "${skillPath}" resolves outside the repository root: ${source}`);
324
- }
325
- try {
326
- const realSource = await realpath(source);
327
- if (realSource !== realRoot &&
328
- !realSource.startsWith(realRoot + path.sep)) {
329
- throw new UserError("DEPLOY_FAILED", `Skill path "${skillPath}" resolves outside the repository root via symlink: ${source} -> ${realSource}`);
330
- }
331
- }
332
- catch (err) {
333
- if (err instanceof UserError)
334
- throw err;
335
- // Source doesn't exist yet — will be caught during execute
336
- }
337
- }
338
382
  async function validateSkillContract(source, skillPath) {
339
383
  let stat;
340
384
  try {
@@ -354,9 +398,23 @@ async function validateSkillContract(source, skillPath) {
354
398
  throw new UserError("DEPLOY_FAILED", `Skill "${skillPath}" source is not a directory: ${source}`);
355
399
  }
356
400
  try {
357
- await access(path.join(source, "SKILL.md"));
401
+ await access(source, constants.R_OK);
358
402
  }
359
- catch {
403
+ catch (err) {
404
+ const code = err.code;
405
+ if (code === "EACCES" || code === "EPERM") {
406
+ throw new UserError("DEPLOY_FAILED", `Permission denied reading skill directory "${skillPath}": ${source}`);
407
+ }
408
+ throw new UserError("DEPLOY_FAILED", `Cannot read skill directory "${skillPath}": ${source}`);
409
+ }
410
+ try {
411
+ await access(path.join(source, "SKILL.md"), constants.R_OK);
412
+ }
413
+ catch (err) {
414
+ const code = err.code;
415
+ if (code === "EACCES" || code === "EPERM") {
416
+ throw new UserError("DEPLOY_FAILED", `Permission denied reading SKILL.md in skill "${skillPath}": ${source}`);
417
+ }
360
418
  throw new UserError("DEPLOY_FAILED", `Skill "${skillPath}" source is missing SKILL.md: ${source}`);
361
419
  }
362
420
  }
@@ -437,17 +495,15 @@ async function backupExisting(targetPath, verbose, home, expected) {
437
495
  throw new Error(`Target "${targetPath}" exists but is not managed by inception-engine — refusing to overwrite`);
438
496
  }
439
497
  const backupPath = `${targetPath}.inception-backup`;
440
- // Clean up any stale backup from a previous failed attempt
441
- try {
442
- await lstat(backupPath);
443
- await removeTarget(backupPath);
444
- }
445
- catch {
446
- // No stale backup — expected
447
- }
448
498
  if (verbose) {
449
499
  logger.detail(`backing up existing target: ${targetPath}`);
450
500
  }
501
+ // Remove any stale backup from a previous failed attempt. Using rm with
502
+ // { force: true } avoids a separate lstat existence check and handles
503
+ // the case where the stale backup is a directory (which rename cannot
504
+ // atomically replace on POSIX). This reduces the window between the
505
+ // stale-backup removal and the rename to a single step.
506
+ await rm(backupPath, { recursive: true, force: true });
451
507
  await rename(targetPath, backupPath);
452
508
  return backupPath;
453
509
  }
@@ -17,6 +17,12 @@ export async function runPreflight(_options, _manifest, _home, detectedAgents) {
17
17
  message: `Agent "${agentId}" skill support is provisional: behavior has not been independently verified.`,
18
18
  });
19
19
  }
20
+ if (agent.policyNote) {
21
+ warnings.push({
22
+ kind: "policy",
23
+ message: `Agent "${agentId}": ${agent.policyNote}`,
24
+ });
25
+ }
20
26
  }
21
27
  return warnings;
22
28
  }
@@ -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");
@@ -1,46 +1,94 @@
1
1
  import { lstat, readFile, rm, unlink, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
2
3
  import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
3
4
  import { logger } from "../logger.js";
5
+ import { compileMcpServerReverts, compileAgentRuleReverts, } from "./adapters/index.js";
4
6
  import { lookupDeployment, unregisterDeployment } from "./ownership.js";
5
7
  import { resolveAgentSkillPath } from "./resolve.js";
6
- export function planRevert(manifest, detectedAgents, home) {
8
+ function resolveTargetTemplate(template, home) {
9
+ const appdata = process.env.APPDATA ?? path.join(home, "AppData", "Roaming");
10
+ const xdgRaw = process.env.XDG_CONFIG_HOME;
11
+ const xdgConfig = xdgRaw && path.isAbsolute(xdgRaw) ? xdgRaw : path.join(home, ".config");
12
+ return template
13
+ .replace("{home}", home)
14
+ .replace("{appdata}", appdata)
15
+ .replace("{xdg_config}", xdgConfig);
16
+ }
17
+ function buildSkillDirReverts(manifest, home, agentFilter) {
7
18
  const actions = [];
8
19
  for (const skill of manifest.skills) {
9
20
  for (const agentId of skill.agents) {
10
- if (!detectedAgents.includes(agentId))
21
+ if (agentFilter && !agentFilter.includes(agentId))
11
22
  continue;
12
23
  const agent = AGENT_REGISTRY_BY_ID[agentId];
13
24
  if (!agent)
14
25
  continue;
15
- const target = resolveAgentSkillPath(agent, skill.name, home);
16
26
  actions.push({
17
27
  kind: "skill-dir",
18
28
  skill: skill.name,
19
29
  agent: agentId,
20
- target,
30
+ target: resolveAgentSkillPath(agent, skill.name, home),
21
31
  });
22
32
  }
23
33
  }
24
34
  return actions;
25
35
  }
26
- export function planRevertAll(manifest, home) {
36
+ function buildFileWriteReverts(manifest, home, agentFilter) {
27
37
  const actions = [];
28
- for (const skill of manifest.skills) {
29
- for (const agentId of skill.agents) {
38
+ for (const fileEntry of manifest.files ?? []) {
39
+ for (const agentId of fileEntry.agents) {
40
+ if (agentFilter && !agentFilter.includes(agentId))
41
+ continue;
30
42
  const agent = AGENT_REGISTRY_BY_ID[agentId];
31
43
  if (!agent)
32
44
  continue;
33
- const target = resolveAgentSkillPath(agent, skill.name, home);
34
45
  actions.push({
35
- kind: "skill-dir",
36
- skill: skill.name,
46
+ kind: "file-write",
47
+ skill: fileEntry.name,
48
+ agent: agentId,
49
+ target: resolveTargetTemplate(fileEntry.target, home),
50
+ });
51
+ }
52
+ }
53
+ return actions;
54
+ }
55
+ function buildConfigPatchReverts(manifest, home, agentFilter) {
56
+ const actions = [];
57
+ for (const configEntry of manifest.configs ?? []) {
58
+ for (const agentId of configEntry.agents) {
59
+ if (agentFilter && !agentFilter.includes(agentId))
60
+ continue;
61
+ const agent = AGENT_REGISTRY_BY_ID[agentId];
62
+ if (!agent)
63
+ continue;
64
+ actions.push({
65
+ kind: "config-patch",
66
+ skill: configEntry.name,
37
67
  agent: agentId,
38
- target,
68
+ target: resolveTargetTemplate(configEntry.target, home),
39
69
  });
40
70
  }
41
71
  }
42
72
  return actions;
43
73
  }
74
+ export function planRevert(manifest, detectedAgents, home) {
75
+ return [
76
+ ...buildSkillDirReverts(manifest, home, detectedAgents),
77
+ ...buildFileWriteReverts(manifest, home, detectedAgents),
78
+ ...buildConfigPatchReverts(manifest, home, detectedAgents),
79
+ ...(manifest.mcpServers ?? []).flatMap((e) => compileMcpServerReverts(e, detectedAgents, home)),
80
+ ...(manifest.agentRules ?? []).flatMap((e) => compileAgentRuleReverts(e, detectedAgents, home)),
81
+ ];
82
+ }
83
+ export function planRevertAll(manifest, home) {
84
+ return [
85
+ ...buildSkillDirReverts(manifest, home, null),
86
+ ...buildFileWriteReverts(manifest, home, null),
87
+ ...buildConfigPatchReverts(manifest, home, null),
88
+ ...(manifest.mcpServers ?? []).flatMap((e) => compileMcpServerReverts(e, null, home)),
89
+ ...(manifest.agentRules ?? []).flatMap((e) => compileAgentRuleReverts(e, null, home)),
90
+ ];
91
+ }
44
92
  function recordOutcome(result, action, counts, failed) {
45
93
  if (result.outcome === "fail") {
46
94
  failed.push({ action, error: result.error });
@@ -66,12 +114,18 @@ async function readJsonConfig(filePath) {
66
114
  }
67
115
  return parsed;
68
116
  }
117
+ function isPlainObject(v) {
118
+ return typeof v === "object" && v !== null && !Array.isArray(v);
119
+ }
69
120
  function applyUndoPatch(current, undoPatch) {
70
121
  const restored = { ...current };
71
122
  for (const [key, originalValue] of Object.entries(undoPatch)) {
72
123
  if (originalValue === null) {
73
124
  delete restored[key];
74
125
  }
126
+ else if (isPlainObject(originalValue) && isPlainObject(restored[key])) {
127
+ restored[key] = applyUndoPatch(restored[key], originalValue);
128
+ }
75
129
  else {
76
130
  restored[key] = originalValue;
77
131
  }
@@ -114,9 +168,8 @@ export async function executeRevert(actions, dryRun, verbose, home) {
114
168
  }
115
169
  async function executeRevertAction(action, dryRun, verbose, home, planned) {
116
170
  const label = `${action.skill} -> ${action.agent}`;
117
- let stat;
118
171
  try {
119
- stat = await lstat(action.target);
172
+ await lstat(action.target);
120
173
  }
121
174
  catch (err) {
122
175
  const result = lstatOutcome(err);
@@ -133,8 +186,6 @@ async function executeRevertAction(action, dryRun, verbose, home, planned) {
133
186
  return { outcome: "skip" };
134
187
  }
135
188
  if (dryRun) {
136
- logger.plan(label);
137
- logger.detail(`would remove: ${action.target}`);
138
189
  planned.push({
139
190
  verb: "remove",
140
191
  kind: "skill-dir",
@@ -145,7 +196,10 @@ async function executeRevertAction(action, dryRun, verbose, home, planned) {
145
196
  return { outcome: "ok" };
146
197
  }
147
198
  try {
148
- if (stat.isSymbolicLink()) {
199
+ // Re-stat immediately before deletion to minimise the window between the
200
+ // type-check and the removal syscall.
201
+ const currentStat = await lstat(action.target);
202
+ if (currentStat.isSymbolicLink()) {
149
203
  await unlink(action.target);
150
204
  }
151
205
  else {
@@ -159,6 +213,11 @@ async function executeRevertAction(action, dryRun, verbose, home, planned) {
159
213
  return { outcome: "ok" };
160
214
  }
161
215
  catch (err) {
216
+ if (err.code === "ENOENT") {
217
+ // Target disappeared between ownership check and removal — treat as skip.
218
+ logger.skip(label, "(disappeared before removal, skipping)");
219
+ return { outcome: "skip" };
220
+ }
162
221
  const msg = err instanceof Error ? err.message : String(err);
163
222
  logger.fail(label, msg);
164
223
  return { outcome: "fail", error: msg };
@@ -184,8 +243,6 @@ async function revertFileWrite(action, dryRun, verbose, home, planned) {
184
243
  return { outcome: "skip" };
185
244
  }
186
245
  if (dryRun) {
187
- logger.plan(label);
188
- logger.detail(`would remove: ${action.target}`);
189
246
  planned.push({
190
247
  verb: "remove",
191
248
  kind: "file-write",
@@ -196,6 +253,9 @@ async function revertFileWrite(action, dryRun, verbose, home, planned) {
196
253
  return { outcome: "ok" };
197
254
  }
198
255
  try {
256
+ // Re-stat immediately before deletion to minimise the type-check to
257
+ // removal window; handle the case where the file has since disappeared.
258
+ await lstat(action.target);
199
259
  await unlink(action.target);
200
260
  await unregisterDeployment(home, action.target);
201
261
  logger.ok(label);
@@ -205,6 +265,10 @@ async function revertFileWrite(action, dryRun, verbose, home, planned) {
205
265
  return { outcome: "ok" };
206
266
  }
207
267
  catch (err) {
268
+ if (err.code === "ENOENT") {
269
+ logger.skip(label, "(disappeared before removal, skipping)");
270
+ return { outcome: "skip" };
271
+ }
208
272
  const msg = err instanceof Error ? err.message : String(err);
209
273
  logger.fail(label, msg);
210
274
  return { outcome: "fail", error: msg };
@@ -234,8 +298,6 @@ async function revertConfigPatch(action, dryRun, verbose, home, planned) {
234
298
  }
235
299
  const configPatchEntry = entry;
236
300
  if (dryRun) {
237
- logger.plan(label);
238
- logger.detail(`would unapply patch: ${JSON.stringify(configPatchEntry.undoPatch)} -> ${action.target}`);
239
301
  planned.push({
240
302
  verb: "unapply-patch",
241
303
  kind: "config-patch",
@@ -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
+ }
package/dist/index.js CHANGED
@@ -111,6 +111,21 @@ async function main() {
111
111
  }
112
112
  return runRevert(options, manifest, home);
113
113
  }
114
+ function renderDryRunPlan(planned) {
115
+ for (const change of planned) {
116
+ logger.plan(`[${change.agent}] ${change.verb} ${change.skill}`);
117
+ if (change.source !== undefined) {
118
+ logger.detail(`source: ${change.source}`);
119
+ }
120
+ logger.detail(`target: ${change.target}`);
121
+ if (change.verb === "patch-config" && change.patch !== undefined) {
122
+ logger.detail(`patch: ${JSON.stringify(change.patch)}`);
123
+ }
124
+ else if (change.verb === "unapply-patch" && change.patch !== undefined) {
125
+ logger.detail(`undo: ${JSON.stringify(change.patch)}`);
126
+ }
127
+ }
128
+ }
114
129
  async function runDeploy(options, manifest, home) {
115
130
  let detectedAgents;
116
131
  if (options.agents) {
@@ -132,7 +147,8 @@ async function runDeploy(options, manifest, home) {
132
147
  }
133
148
  const preflightWarnings = await runPreflight(options, manifest, home, detectedAgents);
134
149
  for (const w of preflightWarnings) {
135
- logger.warn("preflight", w.message);
150
+ const label = w.kind === "policy" ? "policy" : "preflight";
151
+ logger.warn(label, w.message);
136
152
  }
137
153
  const { actions, warnings: planWarnings } = await planDeploy(manifest, options.directory, detectedAgents, home);
138
154
  for (const w of planWarnings) {
@@ -142,14 +158,21 @@ async function runDeploy(options, manifest, home) {
142
158
  logger.info("No skills to deploy for detected agents.");
143
159
  return 0;
144
160
  }
145
- logger.info(`${dryRunPrefix(options.dryRun)}Deploying ${actions.length} skill(s):`);
146
- const { succeeded, failed } = await executeDeploy(actions, options.dryRun, options.verbose, home);
161
+ logger.info(`${dryRunPrefix(options.dryRun)}Deploying ${actions.length} action(s):`);
162
+ const { succeeded, failed, planned } = await executeDeploy(actions, options.dryRun, options.verbose, home);
163
+ if (options.dryRun) {
164
+ logger.info("");
165
+ renderDryRunPlan(planned);
166
+ logger.info("");
167
+ logger.info(`${planned.length} action(s) would be applied (dry-run)`);
168
+ return 0;
169
+ }
147
170
  logger.info("");
148
171
  if (failed.length > 0) {
149
172
  logger.info(`${succeeded} succeeded, ${failed.length} failed`);
150
173
  return 1;
151
174
  }
152
- logger.info(`${succeeded} skill(s) deployed${options.dryRun ? " (dry-run)" : ""}`);
175
+ logger.info(`${succeeded} action(s) deployed`);
153
176
  return 0;
154
177
  }
155
178
  async function runRevert(options, manifest, home) {
@@ -160,21 +183,28 @@ async function runRevert(options, manifest, home) {
160
183
  logger.info("No skills to revert.");
161
184
  return 0;
162
185
  }
163
- logger.info(`${dryRunPrefix(options.dryRun)}Reverting ${actions.length} skill(s):`);
164
- const { succeeded, skipped, failed } = await executeRevert(actions, options.dryRun, options.verbose, home);
186
+ logger.info(`${dryRunPrefix(options.dryRun)}Reverting ${actions.length} action(s):`);
187
+ const { succeeded, skipped, failed, planned } = await executeRevert(actions, options.dryRun, options.verbose, home);
188
+ if (options.dryRun) {
189
+ logger.info("");
190
+ renderDryRunPlan(planned);
191
+ logger.info("");
192
+ logger.info(`${planned.length} action(s) would be removed (dry-run)`);
193
+ return 0;
194
+ }
165
195
  logger.info("");
166
196
  if (failed.length > 0) {
167
197
  const parts = [`${succeeded} removed`];
168
198
  if (skipped > 0)
169
199
  parts.push(`${skipped} skipped`);
170
200
  parts.push(`${failed.length} failed`);
171
- logger.info(`${parts.join(", ")}${options.dryRun ? " (dry-run)" : ""}`);
201
+ logger.info(parts.join(", "));
172
202
  return 1;
173
203
  }
174
204
  const parts = [`${succeeded} removed`];
175
205
  if (skipped > 0)
176
206
  parts.push(`${skipped} skipped`);
177
- logger.info(`${parts.join(", ")}${options.dryRun ? " (dry-run)" : ""}`);
207
+ logger.info(parts.join(", "));
178
208
  return 0;
179
209
  }
180
210
  const USER_ERROR_EXIT = {
@@ -22,6 +22,56 @@ export declare const SkillEntrySchema: z.ZodObject<{
22
22
  "github-copilot": "github-copilot";
23
23
  }>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
24
24
  }, z.core.$strip>;
25
+ export declare const FileEntrySchema: z.ZodObject<{
26
+ name: z.ZodString;
27
+ path: z.ZodString;
28
+ target: z.ZodString;
29
+ agents: z.ZodPipe<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodEnum<{
30
+ "claude-code": "claude-code";
31
+ codex: "codex";
32
+ "gemini-cli": "gemini-cli";
33
+ antigravity: "antigravity";
34
+ opencode: "opencode";
35
+ "github-copilot": "github-copilot";
36
+ }>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
37
+ }, z.core.$strip>;
38
+ export declare const ConfigEntrySchema: z.ZodObject<{
39
+ name: z.ZodString;
40
+ target: z.ZodString;
41
+ patch: z.ZodRecord<z.ZodString, z.ZodUnknown>;
42
+ agents: z.ZodPipe<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodEnum<{
43
+ "claude-code": "claude-code";
44
+ codex: "codex";
45
+ "gemini-cli": "gemini-cli";
46
+ antigravity: "antigravity";
47
+ opencode: "opencode";
48
+ "github-copilot": "github-copilot";
49
+ }>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
50
+ }, z.core.$strip>;
51
+ export declare const McpServerEntrySchema: z.ZodObject<{
52
+ name: z.ZodString;
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>;
63
+ export declare const AgentRuleEntrySchema: z.ZodObject<{
64
+ name: z.ZodString;
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>;
25
75
  export declare const ManifestSchema: z.ZodObject<{
26
76
  skills: z.ZodArray<z.ZodObject<{
27
77
  name: z.ZodString;
@@ -35,10 +85,62 @@ export declare const ManifestSchema: z.ZodObject<{
35
85
  "github-copilot": "github-copilot";
36
86
  }>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
37
87
  }, z.core.$strip>>;
38
- mcpServers: z.ZodDefault<z.ZodArray<z.ZodUnknown>>;
39
- agentRules: z.ZodDefault<z.ZodArray<z.ZodUnknown>>;
88
+ files: z.ZodDefault<z.ZodArray<z.ZodObject<{
89
+ name: z.ZodString;
90
+ path: z.ZodString;
91
+ target: z.ZodString;
92
+ agents: z.ZodPipe<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodEnum<{
93
+ "claude-code": "claude-code";
94
+ codex: "codex";
95
+ "gemini-cli": "gemini-cli";
96
+ antigravity: "antigravity";
97
+ opencode: "opencode";
98
+ "github-copilot": "github-copilot";
99
+ }>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
100
+ }, z.core.$strip>>>;
101
+ configs: z.ZodDefault<z.ZodArray<z.ZodObject<{
102
+ name: z.ZodString;
103
+ target: z.ZodString;
104
+ patch: z.ZodRecord<z.ZodString, z.ZodUnknown>;
105
+ agents: z.ZodPipe<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodEnum<{
106
+ "claude-code": "claude-code";
107
+ codex: "codex";
108
+ "gemini-cli": "gemini-cli";
109
+ antigravity: "antigravity";
110
+ opencode: "opencode";
111
+ "github-copilot": "github-copilot";
112
+ }>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
113
+ }, z.core.$strip>>>;
114
+ mcpServers: z.ZodDefault<z.ZodArray<z.ZodObject<{
115
+ name: z.ZodString;
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>>>;
126
+ agentRules: z.ZodDefault<z.ZodArray<z.ZodObject<{
127
+ name: z.ZodString;
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>>>;
40
138
  }, z.core.$strip>;
41
139
  export type SkillEntry = z.infer<typeof SkillEntrySchema>;
140
+ export type FileEntry = z.infer<typeof FileEntrySchema>;
141
+ export type ConfigEntry = z.infer<typeof ConfigEntrySchema>;
142
+ export type McpServerEntry = z.infer<typeof McpServerEntrySchema>;
143
+ export type AgentRuleEntry = z.infer<typeof AgentRuleEntrySchema>;
42
144
  export type Manifest = z.infer<typeof ManifestSchema>;
43
145
  export declare const AgentListSchema: z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<string[], string>>, z.ZodArray<z.ZodPipe<z.ZodString, z.ZodEnum<{
44
146
  "claude-code": "claude-code";
@@ -10,6 +10,9 @@ const AGENT_IDS = [
10
10
  ];
11
11
  export { AGENT_IDS };
12
12
  const SAFE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
13
+ // Target templates must start with a known placeholder to prevent raw absolute
14
+ // paths or directory traversal. e.g. "{home}/.claude/settings.json" is valid.
15
+ const TARGET_TEMPLATE_RE = /^\{(home|appdata|xdg_config)\}/;
13
16
  // Standalone schema used for type derivation and single-ID validation (e.g. index.ts).
14
17
  export const AgentIdSchema = z.enum(AGENT_IDS);
15
18
  // Used inside SkillEntrySchema.agents so that enum failures embed the received
@@ -26,26 +29,60 @@ const agentIdElement = z
26
29
  }
27
30
  })
28
31
  .pipe(AgentIdSchema);
32
+ const nameField = z
33
+ .string({ message: "name must be a non-empty string" })
34
+ .min(1, { message: "name must be a non-empty string" })
35
+ .regex(SAFE_NAME_RE, {
36
+ message: "name must contain only letters, digits, hyphens, underscores, and dots, and must not start with a dot",
37
+ });
38
+ const agentsField = z
39
+ .array(agentIdElement, { message: "agents must be a non-empty array" })
40
+ .min(1, { message: "agents must be a non-empty array" })
41
+ .transform((arr) => [...new Set(arr)]);
42
+ const sourcePathField = z
43
+ .string({ message: "path must be a non-empty string" })
44
+ .min(1, { message: "path must be a non-empty string" })
45
+ .refine((p) => !nodePath.isAbsolute(p), {
46
+ message: "path must be a relative path",
47
+ })
48
+ .refine((p) => !nodePath.normalize(p).startsWith(".."), {
49
+ message: "path must not escape the repository root",
50
+ });
51
+ const targetTemplateField = z
52
+ .string({ message: "target must be a non-empty string" })
53
+ .min(1, { message: "target must be a non-empty string" })
54
+ .refine((t) => TARGET_TEMPLATE_RE.test(t), {
55
+ message: "target must start with a known placeholder: {home}, {appdata}, or {xdg_config}",
56
+ });
29
57
  export const SkillEntrySchema = z.object({
30
- name: z
31
- .string({ message: "name must be a non-empty string" })
32
- .min(1, { message: "name must be a non-empty string" })
33
- .regex(SAFE_NAME_RE, {
34
- message: "name must contain only letters, digits, hyphens, underscores, and dots, and must not start with a dot",
35
- }),
36
- path: z
37
- .string({ message: "path must be a non-empty string" })
38
- .min(1, { message: "path must be a non-empty string" })
39
- .refine((p) => !nodePath.isAbsolute(p), {
40
- message: "path must be a relative path",
41
- })
42
- .refine((p) => !nodePath.normalize(p).startsWith(".."), {
43
- message: "path must not escape the repository root",
44
- }),
45
- agents: z
46
- .array(agentIdElement, { message: "agents must be a non-empty array" })
47
- .min(1, { message: "agents must be a non-empty array" })
48
- .transform((arr) => [...new Set(arr)]),
58
+ name: nameField,
59
+ path: sourcePathField,
60
+ agents: agentsField,
61
+ });
62
+ export const FileEntrySchema = z.object({
63
+ name: nameField,
64
+ path: sourcePathField,
65
+ target: targetTemplateField,
66
+ agents: agentsField,
67
+ });
68
+ export const ConfigEntrySchema = z.object({
69
+ name: nameField,
70
+ target: targetTemplateField,
71
+ patch: z.record(z.string(), z.unknown()),
72
+ agents: agentsField,
73
+ });
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,
49
86
  });
50
87
  export const ManifestSchema = z.object({
51
88
  skills: z.array(SkillEntrySchema).superRefine((skills, ctx) => {
@@ -61,8 +98,10 @@ export const ManifestSchema = z.object({
61
98
  seen.add(skill.name);
62
99
  }
63
100
  }),
64
- mcpServers: z.array(z.unknown()).default([]),
65
- agentRules: z.array(z.unknown()).default([]),
101
+ files: z.array(FileEntrySchema).default([]),
102
+ configs: z.array(ConfigEntrySchema).default([]),
103
+ mcpServers: z.array(McpServerEntrySchema).default([]),
104
+ agentRules: z.array(AgentRuleEntrySchema).default([]),
66
105
  });
67
106
  // Parses the --agents CLI flag: comma-separated agent IDs → AgentId[]
68
107
  export const AgentListSchema = z
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { AgentId } from "./schemas/manifest.ts";
2
- export type { AgentId, Manifest, SkillEntry } from "./schemas/manifest.ts";
2
+ export type { AgentId, ConfigEntry, FileEntry, Manifest, SkillEntry, } from "./schemas/manifest.ts";
3
3
  export interface AgentPaths {
4
4
  posix: string[];
5
5
  windows: string[];
@@ -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,9 @@ export interface AgentConfig {
17
19
  detectPaths: AgentPaths;
18
20
  detectBinary: string | null;
19
21
  provenance: AgentProvenance;
22
+ mcpConfigPath?: AgentPaths;
23
+ agentRulesPath?: AgentPaths;
24
+ policyNote?: string;
20
25
  }
21
26
  export interface PlanWarning {
22
27
  kind: "confidence" | "collision" | "ambiguity";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kuznai/inception-engine",
3
- "version": "0.7.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",