@kuznai/inception-engine 0.12.0 → 0.13.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 +9 -4
- package/dist/config/agents.js +25 -12
- package/dist/core/adapters/frontmatter.d.ts +26 -0
- package/dist/core/adapters/frontmatter.js +120 -0
- package/dist/core/adapters/index.d.ts +3 -3
- package/dist/core/adapters/index.js +3 -3
- package/dist/core/adapters/mcp.d.ts +4 -4
- package/dist/core/adapters/mcp.js +78 -21
- package/dist/core/adapters/rules.d.ts +2 -2
- package/dist/core/adapters/rules.js +5 -5
- package/dist/core/adapters/toml.d.ts +19 -0
- package/dist/core/adapters/toml.js +96 -0
- package/dist/core/deploy.js +126 -46
- package/dist/core/resolve.d.ts +1 -1
- package/dist/core/resolve.js +3 -2
- package/dist/core/revert.d.ts +2 -2
- package/dist/core/revert.js +68 -7
- package/dist/core/runtime-paths.d.ts +1 -1
- package/dist/core/runtime-paths.js +16 -3
- package/dist/schemas/manifest.js +4 -2
- package/dist/types.d.ts +34 -4
- package/package.json +3 -1
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;
|
|
52
|
-
| Global Rules Files |
|
|
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
|
|
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
|
-
|
|
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
|
|
package/dist/config/agents.js
CHANGED
|
@@ -54,9 +54,12 @@ export const AGENT_REGISTRY = [
|
|
|
54
54
|
agentRules: "documented",
|
|
55
55
|
},
|
|
56
56
|
mcpSupport: {
|
|
57
|
-
status: "
|
|
58
|
-
schemaLabel: "
|
|
59
|
-
|
|
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: "
|
|
125
|
-
schemaLabel: "frontmatter-
|
|
126
|
-
|
|
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: "
|
|
130
|
-
schemaLabel: "repo-
|
|
131
|
-
|
|
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: "
|
|
154
|
-
schemaLabel: "opencode.json MCP
|
|
155
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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],
|
|
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],
|
|
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
|
+
}
|
package/dist/core/deploy.js
CHANGED
|
@@ -4,7 +4,9 @@ import path from "node:path";
|
|
|
4
4
|
import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
|
|
5
5
|
import { UserError } from "../errors.js";
|
|
6
6
|
import { logger } from "../logger.js";
|
|
7
|
+
import { writeFrontmatterFile } from "./adapters/frontmatter.js";
|
|
7
8
|
import { compileAdapterActions } from "./adapters/index.js";
|
|
9
|
+
import { applyTomlMcpPatch } from "./adapters/toml.js";
|
|
8
10
|
import { lookupDeployment, registerDeployment, verifyDeployment, } from "./ownership.js";
|
|
9
11
|
import { getDeployMethod, resolveAgentSkillPath } from "./resolve.js";
|
|
10
12
|
import { resolveTargetTemplate } from "./runtime-paths.js";
|
|
@@ -80,14 +82,20 @@ function detectCollisions(actions) {
|
|
|
80
82
|
}
|
|
81
83
|
return warnings;
|
|
82
84
|
}
|
|
83
|
-
function detectAmbiguities(detectedAgents) {
|
|
85
|
+
function detectAmbiguities(detectedAgents, actions, home) {
|
|
84
86
|
const warnings = [];
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
const hasGeminiCli = detectedAgents.includes("gemini-cli");
|
|
88
|
+
const hasAntigravity = detectedAgents.includes("antigravity");
|
|
89
|
+
if (hasGeminiCli && hasAntigravity) {
|
|
90
|
+
const sharedGeminiMd = path.resolve(home, ".gemini", "GEMINI.md");
|
|
91
|
+
const sharedSettings = path.resolve(home, ".gemini", "settings.json");
|
|
92
|
+
const targetsShared = actions.some((a) => a.target === sharedGeminiMd || a.target === sharedSettings);
|
|
93
|
+
if (targetsShared) {
|
|
94
|
+
warnings.push({
|
|
95
|
+
kind: "ambiguity",
|
|
96
|
+
message: "Both 'gemini-cli' and 'antigravity' are active, and a deployment targets a shared surface (~/.gemini/GEMINI.md or settings.json). Because Antigravity treats GEMINI.md as an Agent Blueprint, changes intended for one runtime may unexpectedly affect the other.",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
91
99
|
}
|
|
92
100
|
return warnings;
|
|
93
101
|
}
|
|
@@ -143,7 +151,7 @@ async function planFileWriteActions(manifest, sourceDir, resolvedSourceDir, real
|
|
|
143
151
|
}
|
|
144
152
|
return actions;
|
|
145
153
|
}
|
|
146
|
-
function planConfigPatchActions(manifest, detectedAgents, home) {
|
|
154
|
+
function planConfigPatchActions(manifest, detectedAgents, home, repo) {
|
|
147
155
|
const actions = [];
|
|
148
156
|
for (const configEntry of manifest.configs ?? []) {
|
|
149
157
|
for (const agentId of configEntry.agents) {
|
|
@@ -156,7 +164,7 @@ function planConfigPatchActions(manifest, detectedAgents, home) {
|
|
|
156
164
|
kind: "config-patch",
|
|
157
165
|
skill: configEntry.name,
|
|
158
166
|
agent: agentId,
|
|
159
|
-
target: resolveTargetTemplate(configEntry.target, home),
|
|
167
|
+
target: resolveTargetTemplate(configEntry.target, home, repo),
|
|
160
168
|
patch: configEntry.patch,
|
|
161
169
|
confidence: agent.provenance.skills,
|
|
162
170
|
});
|
|
@@ -176,56 +184,44 @@ export async function planDeploy(manifest, sourceDir, detectedAgents, home) {
|
|
|
176
184
|
const actions = [
|
|
177
185
|
...(await planSkillDirActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home)),
|
|
178
186
|
...(await planFileWriteActions(manifest, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home)),
|
|
179
|
-
...planConfigPatchActions(manifest, detectedAgents, home),
|
|
187
|
+
...planConfigPatchActions(manifest, detectedAgents, home, resolvedSourceDir),
|
|
180
188
|
];
|
|
181
|
-
const adapterResult = await compileAdapterActions(manifest.mcpServers, manifest.agentRules, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home);
|
|
189
|
+
const adapterResult = await compileAdapterActions(manifest.mcpServers, manifest.agentRules, sourceDir, resolvedSourceDir, realRoot, detectedAgents, home, resolvedSourceDir);
|
|
182
190
|
actions.push(...adapterResult.actions);
|
|
183
191
|
const warnings = [
|
|
184
|
-
...detectAmbiguities(detectedAgents),
|
|
192
|
+
...detectAmbiguities(detectedAgents, actions, home),
|
|
185
193
|
...detectCollisions(actions),
|
|
186
194
|
...adapterResult.warnings,
|
|
187
195
|
];
|
|
188
196
|
return { actions, warnings };
|
|
189
197
|
}
|
|
198
|
+
async function dispatchDeployAction(action, dryRun, verbose, home, planned, deps) {
|
|
199
|
+
switch (action.kind) {
|
|
200
|
+
case "skill-dir":
|
|
201
|
+
return deploySkillDir(action, dryRun, verbose, home, planned, deps);
|
|
202
|
+
case "file-write":
|
|
203
|
+
return deployFileWrite(action, dryRun, verbose, home, planned, deps);
|
|
204
|
+
case "config-patch":
|
|
205
|
+
return deployConfigPatch(action, dryRun, verbose, home, planned, deps);
|
|
206
|
+
case "toml-patch":
|
|
207
|
+
return deployTomlPatch(action, dryRun, verbose, home, planned, deps);
|
|
208
|
+
case "frontmatter-emit":
|
|
209
|
+
return deployFrontmatterEmit(action, dryRun, verbose, home, planned, deps);
|
|
210
|
+
default:
|
|
211
|
+
throw new Error(`Unhandled deploy action kind: ${action.kind}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
190
214
|
export async function executeDeploy(actions, dryRun, verbose, home, deps = {}) {
|
|
191
215
|
let succeeded = 0;
|
|
192
216
|
const failed = [];
|
|
193
217
|
const planned = [];
|
|
194
218
|
for (const action of actions) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
else {
|
|
202
|
-
failed.push({ action, error: result.error });
|
|
203
|
-
}
|
|
204
|
-
break;
|
|
205
|
-
}
|
|
206
|
-
case "file-write": {
|
|
207
|
-
const result = await deployFileWrite(action, dryRun, verbose, home, planned, deps);
|
|
208
|
-
if (result.error === null) {
|
|
209
|
-
succeeded++;
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
failed.push({ action, error: result.error });
|
|
213
|
-
}
|
|
214
|
-
break;
|
|
215
|
-
}
|
|
216
|
-
case "config-patch": {
|
|
217
|
-
const result = await deployConfigPatch(action, dryRun, verbose, home, planned, deps);
|
|
218
|
-
if (result.error === null) {
|
|
219
|
-
succeeded++;
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
failed.push({ action, error: result.error });
|
|
223
|
-
}
|
|
224
|
-
break;
|
|
225
|
-
}
|
|
226
|
-
default: {
|
|
227
|
-
throw new Error(`Unhandled deploy action kind: ${action}`);
|
|
228
|
-
}
|
|
219
|
+
const result = await dispatchDeployAction(action, dryRun, verbose, home, planned, deps);
|
|
220
|
+
if (result.error === null) {
|
|
221
|
+
succeeded++;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
failed.push({ action, error: result.error });
|
|
229
225
|
}
|
|
230
226
|
}
|
|
231
227
|
return { succeeded, failed, planned };
|
|
@@ -593,3 +589,87 @@ async function backupExisting(targetPath, verbose, home, expected, deps) {
|
|
|
593
589
|
await rename(targetPath, backupPath);
|
|
594
590
|
return backupPath;
|
|
595
591
|
}
|
|
592
|
+
async function deployTomlPatch(action, dryRun, verbose, home, planned, deps) {
|
|
593
|
+
const label = `${action.skill} -> ${action.agent}`;
|
|
594
|
+
if (dryRun) {
|
|
595
|
+
planned.push({
|
|
596
|
+
verb: "patch-toml",
|
|
597
|
+
kind: "toml-patch",
|
|
598
|
+
skill: action.skill,
|
|
599
|
+
agent: action.agent,
|
|
600
|
+
target: action.target,
|
|
601
|
+
confidence: action.confidence,
|
|
602
|
+
});
|
|
603
|
+
return { error: null };
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
// Guard against double-patching by a different skill/agent.
|
|
607
|
+
const existingEntry = await lookupDeployment(home, action.target, deps.registry);
|
|
608
|
+
if (existingEntry &&
|
|
609
|
+
(existingEntry.skill !== action.skill ||
|
|
610
|
+
existingEntry.agent !== action.agent)) {
|
|
611
|
+
throw new Error(`Config "${action.target}" is already patched by skill "${existingEntry.skill}" for agent "${existingEntry.agent}" — refusing to double-patch`);
|
|
612
|
+
}
|
|
613
|
+
await applyTomlMcpPatch(action.target, action.skill, action.config);
|
|
614
|
+
await registerDeployment(home, action.target, {
|
|
615
|
+
kind: "config-patch",
|
|
616
|
+
patch: { mcpServers: { [action.skill]: action.config } },
|
|
617
|
+
undoPatch: { mcpServers: { [action.skill]: null } },
|
|
618
|
+
skill: action.skill,
|
|
619
|
+
agent: action.agent,
|
|
620
|
+
}, deps.registry);
|
|
621
|
+
logger.ok(label);
|
|
622
|
+
if (verbose) {
|
|
623
|
+
logger.detail(`patch-toml: wrote [mcpServers.${action.skill}] to ${action.target}`);
|
|
624
|
+
}
|
|
625
|
+
return { error: null };
|
|
626
|
+
}
|
|
627
|
+
catch (err) {
|
|
628
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
629
|
+
logger.fail(label, msg);
|
|
630
|
+
return { error: msg };
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
async function deployFrontmatterEmit(action, dryRun, verbose, home, planned, deps) {
|
|
634
|
+
const label = `${action.skill} -> ${action.agent}`;
|
|
635
|
+
if (dryRun) {
|
|
636
|
+
planned.push({
|
|
637
|
+
verb: "emit-frontmatter",
|
|
638
|
+
kind: "frontmatter-emit",
|
|
639
|
+
skill: action.skill,
|
|
640
|
+
agent: action.agent,
|
|
641
|
+
target: action.target,
|
|
642
|
+
frontmatter: action.frontmatter,
|
|
643
|
+
confidence: action.confidence,
|
|
644
|
+
});
|
|
645
|
+
return { error: null };
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
// Guard against double-patching by a different skill/agent.
|
|
649
|
+
const existingEntry = await lookupDeployment(home, action.target, deps.registry);
|
|
650
|
+
if (existingEntry &&
|
|
651
|
+
(existingEntry.skill !== action.skill ||
|
|
652
|
+
existingEntry.agent !== action.agent)) {
|
|
653
|
+
throw new Error(`File "${action.target}" is already managed by skill "${existingEntry.skill}" for agent "${existingEntry.agent}" — refusing to overwrite`);
|
|
654
|
+
}
|
|
655
|
+
await writeFrontmatterFile(action.target, action.frontmatter, {
|
|
656
|
+
preserveBody: true,
|
|
657
|
+
});
|
|
658
|
+
await registerDeployment(home, action.target, {
|
|
659
|
+
kind: "file-write",
|
|
660
|
+
source: action.target,
|
|
661
|
+
skill: action.skill,
|
|
662
|
+
agent: action.agent,
|
|
663
|
+
}, deps.registry);
|
|
664
|
+
logger.ok(label);
|
|
665
|
+
if (verbose) {
|
|
666
|
+
logger.detail(`emit-frontmatter: wrote ${action.target}`);
|
|
667
|
+
}
|
|
668
|
+
return { error: null };
|
|
669
|
+
}
|
|
670
|
+
catch (err) {
|
|
671
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
672
|
+
logger.fail(label, msg);
|
|
673
|
+
return { error: msg };
|
|
674
|
+
}
|
|
675
|
+
}
|
package/dist/core/resolve.d.ts
CHANGED
|
@@ -9,4 +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;
|
|
12
|
+
export declare function resolvePlaceholders(segments: string[], skillName: string, home: string, repo?: string): string;
|
package/dist/core/resolve.js
CHANGED
|
@@ -96,13 +96,14 @@ export function resolveAgentSkillPath(agent, skillName, home) {
|
|
|
96
96
|
export function resolveAgentDetectPath(agent, home) {
|
|
97
97
|
return resolveAgentDetectPathFor(agent, home, getPlatformKey());
|
|
98
98
|
}
|
|
99
|
-
export function resolvePlaceholders(segments, skillName, home) {
|
|
99
|
+
export function resolvePlaceholders(segments, skillName, home, repo) {
|
|
100
100
|
const { appdata, xdgConfig } = resolveRuntimePaths(home);
|
|
101
101
|
const resolved = segments.map((seg) => seg
|
|
102
102
|
.replace("{home}", home)
|
|
103
103
|
.replace("{name}", skillName)
|
|
104
104
|
.replace("{appdata}", appdata)
|
|
105
|
-
.replace("{xdg_config}", xdgConfig)
|
|
105
|
+
.replace("{xdg_config}", xdgConfig)
|
|
106
|
+
.replace("{repo}", repo ?? ""));
|
|
106
107
|
const root = resolved.find((segment) => segment.length > 0) ?? home;
|
|
107
108
|
return getPathApi(root).join(...resolved);
|
|
108
109
|
}
|
package/dist/core/revert.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentId, Manifest, PlannedChange, RevertAction } from "../types.ts";
|
|
2
2
|
import { type RegistryPersistence } from "./ownership.ts";
|
|
3
|
-
export declare function planRevert(manifest: Manifest, detectedAgents: AgentId[], home: string): RevertAction[];
|
|
4
|
-
export declare function planRevertAll(manifest: Manifest, home: string): RevertAction[];
|
|
3
|
+
export declare function planRevert(manifest: Manifest, detectedAgents: AgentId[], home: string, repo?: string): RevertAction[];
|
|
4
|
+
export declare function planRevertAll(manifest: Manifest, home: string, repo?: string): RevertAction[];
|
|
5
5
|
export declare function executeRevert(actions: RevertAction[], dryRun: boolean, verbose: boolean, home: string, deps?: RevertDependencies): Promise<{
|
|
6
6
|
succeeded: number;
|
|
7
7
|
skipped: number;
|
package/dist/core/revert.js
CHANGED
|
@@ -2,6 +2,7 @@ import { lstat, readFile, rm, unlink, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { AGENT_REGISTRY_BY_ID } from "../config/agents.js";
|
|
3
3
|
import { logger } from "../logger.js";
|
|
4
4
|
import { compileAgentRuleReverts, compileMcpServerReverts, } from "./adapters/index.js";
|
|
5
|
+
import { revertTomlMcpPatch } from "./adapters/toml.js";
|
|
5
6
|
import { lookupDeployment, unregisterDeployment, } from "./ownership.js";
|
|
6
7
|
import { resolveAgentSkillPath } from "./resolve.js";
|
|
7
8
|
import { resolveTargetTemplate } from "./runtime-paths.js";
|
|
@@ -62,22 +63,22 @@ function buildConfigPatchReverts(manifest, home, agentFilter) {
|
|
|
62
63
|
}
|
|
63
64
|
return actions;
|
|
64
65
|
}
|
|
65
|
-
export function planRevert(manifest, detectedAgents, home) {
|
|
66
|
+
export function planRevert(manifest, detectedAgents, home, repo) {
|
|
66
67
|
return [
|
|
67
68
|
...buildSkillDirReverts(manifest, home, detectedAgents),
|
|
68
69
|
...buildFileWriteReverts(manifest, home, detectedAgents),
|
|
69
70
|
...buildConfigPatchReverts(manifest, home, detectedAgents),
|
|
70
|
-
...(manifest.mcpServers ?? []).flatMap((e) => compileMcpServerReverts(e, detectedAgents, home)),
|
|
71
|
-
...(manifest.agentRules ?? []).flatMap((e) => compileAgentRuleReverts(e, detectedAgents, home)),
|
|
71
|
+
...(manifest.mcpServers ?? []).flatMap((e) => compileMcpServerReverts(e, detectedAgents, home, repo)),
|
|
72
|
+
...(manifest.agentRules ?? []).flatMap((e) => compileAgentRuleReverts(e, detectedAgents, home, repo)),
|
|
72
73
|
];
|
|
73
74
|
}
|
|
74
|
-
export function planRevertAll(manifest, home) {
|
|
75
|
+
export function planRevertAll(manifest, home, repo) {
|
|
75
76
|
return [
|
|
76
77
|
...buildSkillDirReverts(manifest, home, null),
|
|
77
78
|
...buildFileWriteReverts(manifest, home, null),
|
|
78
79
|
...buildConfigPatchReverts(manifest, home, null),
|
|
79
|
-
...(manifest.mcpServers ?? []).flatMap((e) => compileMcpServerReverts(e, null, home)),
|
|
80
|
-
...(manifest.agentRules ?? []).flatMap((e) => compileAgentRuleReverts(e, null, home)),
|
|
80
|
+
...(manifest.mcpServers ?? []).flatMap((e) => compileMcpServerReverts(e, null, home, repo)),
|
|
81
|
+
...(manifest.agentRules ?? []).flatMap((e) => compileAgentRuleReverts(e, null, home, repo)),
|
|
81
82
|
];
|
|
82
83
|
}
|
|
83
84
|
function recordOutcome(result, action, counts, failed) {
|
|
@@ -148,8 +149,21 @@ export async function executeRevert(actions, dryRun, verbose, home, deps = {}) {
|
|
|
148
149
|
case "config-patch":
|
|
149
150
|
result = await revertConfigPatch(action, dryRun, verbose, home, planned, deps);
|
|
150
151
|
break;
|
|
152
|
+
case "toml-patch":
|
|
153
|
+
result = await revertTomlPatch(action, dryRun, verbose, home, planned, deps);
|
|
154
|
+
break;
|
|
155
|
+
case "frontmatter-emit":
|
|
156
|
+
// Frontmatter-emit files are fully managed by inception — revert
|
|
157
|
+
// deletes the file, same as a file-write revert.
|
|
158
|
+
result = await revertFileWrite({
|
|
159
|
+
kind: "file-write",
|
|
160
|
+
skill: action.skill,
|
|
161
|
+
agent: action.agent,
|
|
162
|
+
target: action.target,
|
|
163
|
+
}, dryRun, verbose, home, planned, deps);
|
|
164
|
+
break;
|
|
151
165
|
default:
|
|
152
|
-
throw new Error(`Unhandled revert action kind: ${action}`);
|
|
166
|
+
throw new Error(`Unhandled revert action kind: ${action.kind}`);
|
|
153
167
|
}
|
|
154
168
|
recordOutcome(result, action, counts, failed);
|
|
155
169
|
}
|
|
@@ -316,3 +330,50 @@ async function revertConfigPatch(action, dryRun, verbose, home, planned, deps) {
|
|
|
316
330
|
return { outcome: "fail", error: msg };
|
|
317
331
|
}
|
|
318
332
|
}
|
|
333
|
+
async function revertTomlPatch(action, dryRun, verbose, home, planned, deps) {
|
|
334
|
+
const label = `${action.skill} -> ${action.agent}`;
|
|
335
|
+
try {
|
|
336
|
+
await lstat(action.target);
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
const result = lstatOutcome(err);
|
|
340
|
+
if (result.outcome === "skip") {
|
|
341
|
+
logger.skip(label, "(not found, skipping)");
|
|
342
|
+
return result;
|
|
343
|
+
}
|
|
344
|
+
logger.fail(label, result.error);
|
|
345
|
+
return result;
|
|
346
|
+
}
|
|
347
|
+
const entry = await lookupDeployment(home, action.target, deps.registry);
|
|
348
|
+
if (!entry ||
|
|
349
|
+
entry.kind !== "config-patch" ||
|
|
350
|
+
entry.skill !== action.skill ||
|
|
351
|
+
entry.agent !== action.agent) {
|
|
352
|
+
logger.warn(label, `skipping: ${action.target} is not in the deployment registry — not managed by inception-engine`);
|
|
353
|
+
return { outcome: "skip" };
|
|
354
|
+
}
|
|
355
|
+
if (dryRun) {
|
|
356
|
+
planned.push({
|
|
357
|
+
verb: "unapply-patch",
|
|
358
|
+
kind: "toml-patch",
|
|
359
|
+
skill: action.skill,
|
|
360
|
+
agent: action.agent,
|
|
361
|
+
target: action.target,
|
|
362
|
+
});
|
|
363
|
+
return { outcome: "ok" };
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
await revertTomlMcpPatch(action.target, action.skill);
|
|
367
|
+
await unregisterDeployment(home, action.target, deps.registry);
|
|
368
|
+
logger.ok(label);
|
|
369
|
+
if (verbose) {
|
|
370
|
+
logger.detail(`removed [mcpServers.${action.skill}] from: ${action.target}`);
|
|
371
|
+
}
|
|
372
|
+
return { outcome: "ok" };
|
|
373
|
+
}
|
|
374
|
+
catch (err) {
|
|
375
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
376
|
+
logger.fail(label, msg);
|
|
377
|
+
return { outcome: "fail", error: msg };
|
|
378
|
+
}
|
|
379
|
+
}
|
|
@@ -5,4 +5,4 @@ export interface RuntimePaths {
|
|
|
5
5
|
}
|
|
6
6
|
export declare function getPathApi(root: string): typeof path.posix | typeof path.win32;
|
|
7
7
|
export declare function resolveRuntimePaths(home: string): RuntimePaths;
|
|
8
|
-
export declare function resolveTargetTemplate(template: string, home: string): string;
|
|
8
|
+
export declare function resolveTargetTemplate(template: string, home: string, repo?: string): string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
const TARGET_TEMPLATE_RE = /^\{(home|appdata|xdg_config)\}(?<suffix>(?:[\\/].*)?)$/;
|
|
2
|
+
const TARGET_TEMPLATE_RE = /^\{(home|appdata|xdg_config|repo)\}(?<suffix>(?:[\\/].*)?)$/;
|
|
3
3
|
export function getPathApi(root) {
|
|
4
4
|
if (root.includes("\\") || /^[a-zA-Z]:/.test(root)) {
|
|
5
5
|
return path.win32;
|
|
@@ -32,18 +32,31 @@ export function resolveRuntimePaths(home) {
|
|
|
32
32
|
: homePathApi.join(home, ".config");
|
|
33
33
|
return { appdata, xdgConfig };
|
|
34
34
|
}
|
|
35
|
-
export function resolveTargetTemplate(template, home) {
|
|
35
|
+
export function resolveTargetTemplate(template, home, repo) {
|
|
36
36
|
const { appdata, xdgConfig } = resolveRuntimePaths(home);
|
|
37
37
|
const match = TARGET_TEMPLATE_RE.exec(template);
|
|
38
38
|
if (!match) {
|
|
39
39
|
throw new Error(`Invalid target template: ${template}`);
|
|
40
40
|
}
|
|
41
|
+
const root = match[1];
|
|
42
|
+
if (root === "repo") {
|
|
43
|
+
if (!repo) {
|
|
44
|
+
throw new Error(`Target template uses {repo} but no manifest directory was provided: ${template}`);
|
|
45
|
+
}
|
|
46
|
+
const suffix = match.groups?.suffix ?? "";
|
|
47
|
+
const segments = suffix.split(/[\\/]+/).filter(Boolean);
|
|
48
|
+
const repoPathApi = getPathApi(repo);
|
|
49
|
+
const resolved = segments.length === 0 ? repo : repoPathApi.join(repo, ...segments);
|
|
50
|
+
if (!isSameOrDescendantPath(resolved, repo)) {
|
|
51
|
+
throw new Error(`Target template resolves outside its placeholder root: ${template}`);
|
|
52
|
+
}
|
|
53
|
+
return suffix === "" ? repo : `${repo}${suffix}`;
|
|
54
|
+
}
|
|
41
55
|
const baseByRoot = {
|
|
42
56
|
home,
|
|
43
57
|
appdata,
|
|
44
58
|
xdg_config: xdgConfig,
|
|
45
59
|
};
|
|
46
|
-
const root = match[1];
|
|
47
60
|
const suffix = match.groups?.suffix ?? "";
|
|
48
61
|
const segments = suffix.split(/[\\/]+/).filter(Boolean);
|
|
49
62
|
const pathApi = getPathApi(baseByRoot[root]);
|
package/dist/schemas/manifest.js
CHANGED
|
@@ -13,7 +13,9 @@ const SAFE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
|
13
13
|
// Target templates must be rooted at a known placeholder and may only add
|
|
14
14
|
// descendant path segments beneath that root. e.g. "{home}/.claude/settings.json"
|
|
15
15
|
// is valid, while "{home}/../.ssh/config" is rejected.
|
|
16
|
-
|
|
16
|
+
// {repo} resolves to the manifest directory at deploy time, enabling repo-local
|
|
17
|
+
// targets (e.g. Antigravity's .agents/rules/ surface).
|
|
18
|
+
const TARGET_TEMPLATE_RE = /^\{(home|appdata|xdg_config|repo)\}(?:[\\/].*)?$/;
|
|
17
19
|
// Standalone schema used for type derivation and single-ID validation (e.g. index.ts).
|
|
18
20
|
export const AgentIdSchema = z.enum(AGENT_IDS);
|
|
19
21
|
// Used inside SkillEntrySchema.agents so that enum failures embed the received
|
|
@@ -53,7 +55,7 @@ const targetTemplateField = z
|
|
|
53
55
|
.string({ message: "target must be a non-empty string" })
|
|
54
56
|
.min(1, { message: "target must be a non-empty string" })
|
|
55
57
|
.refine((t) => TARGET_TEMPLATE_RE.test(t), {
|
|
56
|
-
message: "target must start with a known placeholder: {home}, {appdata}, or {
|
|
58
|
+
message: "target must start with a known placeholder: {home}, {appdata}, {xdg_config}, or {repo}",
|
|
57
59
|
})
|
|
58
60
|
.refine((t) => !t.split(/[\\/]+/).includes(".."), {
|
|
59
61
|
message: "target must not escape its placeholder root",
|
package/dist/types.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface SupportedAgentSurface {
|
|
|
9
9
|
status: "supported";
|
|
10
10
|
path: AgentPaths;
|
|
11
11
|
schemaLabel: string;
|
|
12
|
+
mcpPatchKey?: string;
|
|
12
13
|
}
|
|
13
14
|
export interface UnsupportedAgentSurface {
|
|
14
15
|
status: "unsupported";
|
|
@@ -63,7 +64,23 @@ export interface ConfigPatchDeployAction {
|
|
|
63
64
|
patch: unknown;
|
|
64
65
|
confidence?: Confidence;
|
|
65
66
|
}
|
|
66
|
-
export
|
|
67
|
+
export interface TomlPatchDeployAction {
|
|
68
|
+
kind: "toml-patch";
|
|
69
|
+
skill: string;
|
|
70
|
+
agent: AgentId;
|
|
71
|
+
target: string;
|
|
72
|
+
config: Record<string, unknown>;
|
|
73
|
+
confidence?: Confidence;
|
|
74
|
+
}
|
|
75
|
+
export interface FrontmatterEmitDeployAction {
|
|
76
|
+
kind: "frontmatter-emit";
|
|
77
|
+
skill: string;
|
|
78
|
+
agent: AgentId;
|
|
79
|
+
target: string;
|
|
80
|
+
frontmatter: Record<string, unknown>;
|
|
81
|
+
confidence?: Confidence;
|
|
82
|
+
}
|
|
83
|
+
export type DeployAction = SkillDirDeployAction | FileWriteDeployAction | ConfigPatchDeployAction | TomlPatchDeployAction | FrontmatterEmitDeployAction;
|
|
67
84
|
export interface SkillDirRevertAction {
|
|
68
85
|
kind: "skill-dir";
|
|
69
86
|
skill: string;
|
|
@@ -82,17 +99,30 @@ export interface ConfigPatchRevertAction {
|
|
|
82
99
|
agent: AgentId;
|
|
83
100
|
target: string;
|
|
84
101
|
}
|
|
85
|
-
export
|
|
86
|
-
|
|
102
|
+
export interface TomlPatchRevertAction {
|
|
103
|
+
kind: "toml-patch";
|
|
104
|
+
skill: string;
|
|
105
|
+
agent: AgentId;
|
|
106
|
+
target: string;
|
|
107
|
+
}
|
|
108
|
+
export interface FrontmatterEmitRevertAction {
|
|
109
|
+
kind: "frontmatter-emit";
|
|
110
|
+
skill: string;
|
|
111
|
+
agent: AgentId;
|
|
112
|
+
target: string;
|
|
113
|
+
}
|
|
114
|
+
export type RevertAction = SkillDirRevertAction | FileWriteRevertAction | ConfigPatchRevertAction | TomlPatchRevertAction | FrontmatterEmitRevertAction;
|
|
115
|
+
export type PlannedChangeVerb = "create-symlink" | "copy-dir" | "write-file" | "patch-config" | "patch-toml" | "emit-frontmatter" | "remove" | "unapply-patch";
|
|
87
116
|
export interface PlannedChange {
|
|
88
117
|
verb: PlannedChangeVerb;
|
|
89
|
-
kind: "skill-dir" | "file-write" | "config-patch";
|
|
118
|
+
kind: "skill-dir" | "file-write" | "config-patch" | "toml-patch" | "frontmatter-emit";
|
|
90
119
|
skill: string;
|
|
91
120
|
agent: AgentId;
|
|
92
121
|
source?: string;
|
|
93
122
|
target: string;
|
|
94
123
|
method?: "symlink" | "copy";
|
|
95
124
|
patch?: Record<string, unknown>;
|
|
125
|
+
frontmatter?: Record<string, unknown>;
|
|
96
126
|
confidence?: Confidence;
|
|
97
127
|
}
|
|
98
128
|
export interface CliOptions {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kuznai/inception-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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",
|
|
@@ -47,6 +47,8 @@
|
|
|
47
47
|
"test:posix": "node --test --test-isolation=none test/unit/*.test.ts test/os/cross-platform/*.test.ts test/os/posix/*.test.ts"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
+
"front-matter": "^4.0.2",
|
|
51
|
+
"smol-toml": "^1.6.1",
|
|
50
52
|
"zod": "^4.0.0"
|
|
51
53
|
},
|
|
52
54
|
"devDependencies": {
|