@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 +73 -15
- package/dist/config/agents.js +35 -0
- package/dist/config/manifest.js +5 -2
- package/dist/core/adapters/index.d.ts +11 -0
- package/dist/core/adapters/index.js +18 -0
- package/dist/core/adapters/mcp.d.ts +8 -0
- package/dist/core/adapters/mcp.js +51 -0
- package/dist/core/adapters/rules.d.ts +8 -0
- package/dist/core/adapters/rules.js +54 -0
- package/dist/core/deploy.js +109 -53
- package/dist/core/preflight.js +6 -0
- package/dist/core/resolve.d.ts +1 -0
- package/dist/core/resolve.js +1 -1
- package/dist/core/revert.js +82 -20
- package/dist/core/validation.d.ts +3 -0
- package/dist/core/validation.js +41 -0
- package/dist/index.js +38 -8
- package/dist/schemas/manifest.d.ts +104 -2
- package/dist/schemas/manifest.js +60 -21
- package/dist/types.d.ts +6 -1
- package/package.json +1 -1
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
|
|
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 |
|
|
42
|
-
|
|
43
|
-
| Skills (SKILL.md) |
|
|
44
|
-
| File write |
|
|
45
|
-
| Config patch (JSON merge) |
|
|
46
|
-
| MCP Servers |
|
|
47
|
-
| Agent Rules |
|
|
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
|
-
"
|
|
63
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
package/dist/config/agents.js
CHANGED
|
@@ -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]));
|
package/dist/config/manifest.js
CHANGED
|
@@ -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
|
|
41
|
+
// Top-level array fields with wrong type → uniform message
|
|
42
42
|
if (issuePath.length === 1 &&
|
|
43
|
-
(issuePath[0] === "mcpServers" ||
|
|
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
|
+
}
|
package/dist/core/deploy.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
}
|
package/dist/core/preflight.js
CHANGED
|
@@ -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
|
}
|
package/dist/core/resolve.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/resolve.js
CHANGED
|
@@ -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");
|
package/dist/core/revert.js
CHANGED
|
@@ -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
|
-
|
|
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 (!
|
|
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
|
-
|
|
36
|
+
function buildFileWriteReverts(manifest, home, agentFilter) {
|
|
27
37
|
const actions = [];
|
|
28
|
-
for (const
|
|
29
|
-
for (const agentId of
|
|
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: "
|
|
36
|
-
skill:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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}
|
|
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}
|
|
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(
|
|
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(
|
|
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
|
-
|
|
39
|
-
|
|
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";
|
package/dist/schemas/manifest.js
CHANGED
|
@@ -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:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
path:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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";
|