@kuznai/inception-engine 0.6.0 → 0.6.2

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.
@@ -38,6 +38,11 @@ function validateManifest(data, filePath) {
38
38
  if (issuePath.length === 1 && issuePath[0] === "skills") {
39
39
  throw new UserError("MANIFEST_INVALID", `${filePath}: "skills" must be an array`);
40
40
  }
41
+ // Top-level "mcpServers" or "agentRules": wrong type → uniform message
42
+ if (issuePath.length === 1 &&
43
+ (issuePath[0] === "mcpServers" || issuePath[0] === "agentRules")) {
44
+ throw new UserError("MANIFEST_INVALID", `${filePath}: "${issuePath[0]}" must be an array`);
45
+ }
41
46
  throw new UserError("MANIFEST_INVALID", `${filePath}: ${formatZodPath(issuePath)}${issue.message}`);
42
47
  }
43
48
  return result.data;
@@ -28,6 +28,7 @@ export async function planDeploy(manifest, sourceDir, detectedAgents, home) {
28
28
  for (const skill of manifest.skills) {
29
29
  const source = path.resolve(sourceDir, skill.path);
30
30
  await validateSourcePath(source, skill.path, resolvedSourceDir, realRoot);
31
+ await validateSkillContract(source, skill.path);
31
32
  for (const agentId of skill.agents) {
32
33
  if (!detectedAgents.includes(agentId))
33
34
  continue;
@@ -36,6 +37,7 @@ export async function planDeploy(manifest, sourceDir, detectedAgents, home) {
36
37
  continue;
37
38
  const target = resolveAgentSkillPath(agent, skill.name, home);
38
39
  actions.push({
40
+ kind: "skill-dir",
39
41
  skill: skill.name,
40
42
  agent: agentId,
41
43
  source,
@@ -50,36 +52,52 @@ export async function executeDeploy(actions, dryRun, verbose, home) {
50
52
  let succeeded = 0;
51
53
  const failed = [];
52
54
  for (const action of actions) {
53
- const label = `${action.skill} -> ${action.agent}`;
54
- try {
55
- await access(action.source);
56
- }
57
- catch (err) {
58
- const msg = sourceAccessError(err, action.source);
59
- failed.push({ action, error: msg });
60
- logger.fail(label, msg);
61
- continue;
62
- }
63
- if (dryRun) {
64
- logger.plan(label);
65
- if (verbose) {
66
- logger.detail(`${action.method}: ${action.source} -> ${action.target}`);
55
+ switch (action.kind) {
56
+ case "skill-dir": {
57
+ const result = await deploySkillDir(action, dryRun, verbose, home);
58
+ if (result.error === null) {
59
+ succeeded++;
60
+ }
61
+ else {
62
+ failed.push({ action, error: result.error });
63
+ }
64
+ break;
65
+ }
66
+ default: {
67
+ const _ = action.kind;
68
+ throw new Error(`Unhandled deploy action kind: ${_}`);
67
69
  }
68
- succeeded++;
69
- continue;
70
- }
71
- try {
72
- await executeDeployAction(action, verbose, home);
73
- succeeded++;
74
- }
75
- catch (err) {
76
- const msg = err instanceof Error ? err.message : String(err);
77
- failed.push({ action, error: msg });
78
- logger.fail(label, msg);
79
70
  }
80
71
  }
81
72
  return { succeeded, failed };
82
73
  }
74
+ async function deploySkillDir(action, dryRun, verbose, home) {
75
+ const label = `${action.skill} -> ${action.agent}`;
76
+ try {
77
+ await access(action.source);
78
+ }
79
+ catch (err) {
80
+ const msg = sourceAccessError(err, action.source);
81
+ logger.fail(label, msg);
82
+ return { error: msg };
83
+ }
84
+ if (dryRun) {
85
+ logger.plan(label);
86
+ if (verbose) {
87
+ logger.detail(`${action.method}: ${action.source} -> ${action.target}`);
88
+ }
89
+ return { error: null };
90
+ }
91
+ try {
92
+ await executeDeployAction(action, verbose, home);
93
+ return { error: null };
94
+ }
95
+ catch (err) {
96
+ const msg = err instanceof Error ? err.message : String(err);
97
+ logger.fail(label, msg);
98
+ return { error: msg };
99
+ }
100
+ }
83
101
  async function validateSourcePath(source, skillPath, resolvedSourceDir, realRoot) {
84
102
  if (!source.startsWith(resolvedSourceDir + path.sep)) {
85
103
  throw new UserError("DEPLOY_FAILED", `Skill path "${skillPath}" resolves outside the repository root: ${source}`);
@@ -97,6 +115,31 @@ async function validateSourcePath(source, skillPath, resolvedSourceDir, realRoot
97
115
  // Source doesn't exist yet — will be caught during execute
98
116
  }
99
117
  }
118
+ async function validateSkillContract(source, skillPath) {
119
+ let stat;
120
+ try {
121
+ stat = await lstat(source);
122
+ }
123
+ catch (err) {
124
+ const code = err.code;
125
+ if (code === "ENOENT") {
126
+ throw new UserError("DEPLOY_FAILED", `Skill "${skillPath}" source not found: ${source}`);
127
+ }
128
+ if (code === "EACCES" || code === "EPERM") {
129
+ throw new UserError("DEPLOY_FAILED", `Permission denied accessing skill "${skillPath}" source: ${source}`);
130
+ }
131
+ throw new UserError("DEPLOY_FAILED", `Cannot access skill "${skillPath}" source: ${source}`);
132
+ }
133
+ if (!stat.isDirectory()) {
134
+ throw new UserError("DEPLOY_FAILED", `Skill "${skillPath}" source is not a directory: ${source}`);
135
+ }
136
+ try {
137
+ await access(path.join(source, "SKILL.md"));
138
+ }
139
+ catch {
140
+ throw new UserError("DEPLOY_FAILED", `Skill "${skillPath}" source is missing SKILL.md: ${source}`);
141
+ }
142
+ }
100
143
  async function assertTargetAbsent(targetPath) {
101
144
  try {
102
145
  await lstat(targetPath);
@@ -1,2 +1,7 @@
1
1
  import type { AgentId } from "../types.ts";
2
+ export type ExecFn = (cmd: string, args: readonly string[]) => Promise<void>;
2
3
  export declare function detectInstalledAgents(home: string): Promise<AgentId[]>;
4
+ export declare function isBinaryInPath(binary: string, execFn?: ExecFn): Promise<boolean>;
5
+ export declare function isBinaryViaWhereExe(binary: string, execFn?: ExecFn): Promise<boolean>;
6
+ export declare function isBinaryViaCommandV(binary: string): Promise<boolean>;
7
+ export declare function isBinaryViaWhich(binary: string, execFn?: ExecFn): Promise<boolean>;
@@ -4,6 +4,9 @@ import { promisify } from "node:util";
4
4
  import { AGENT_REGISTRY } from "../config/agents.js";
5
5
  import { resolveAgentDetectPath } from "./resolve.js";
6
6
  const execFileAsync = promisify(execFile);
7
+ const defaultExecFn = async (cmd, args) => {
8
+ await execFileAsync(cmd, args);
9
+ };
7
10
  export async function detectInstalledAgents(home) {
8
11
  const detected = [];
9
12
  for (const agent of AGENT_REGISTRY) {
@@ -27,18 +30,12 @@ async function isAgentInstalled(agent, home) {
27
30
  }
28
31
  return false;
29
32
  }
30
- async function isBinaryInPath(binary) {
33
+ export async function isBinaryInPath(binary, execFn = defaultExecFn) {
31
34
  if (process.platform === "win32") {
32
- try {
33
- await execFileAsync("where.exe", [binary]);
34
- return true;
35
- }
36
- catch {
37
- return false;
38
- }
35
+ return isBinaryViaWhereExe(binary, execFn);
39
36
  }
40
37
  try {
41
- await execFileAsync("which", [binary]);
38
+ await execFn("which", [binary]);
42
39
  return true;
43
40
  }
44
41
  catch (err) {
@@ -49,9 +46,19 @@ async function isBinaryInPath(binary) {
49
46
  return false;
50
47
  }
51
48
  }
49
+ // Windows-only: use where.exe to check if a binary is in PATH.
50
+ export async function isBinaryViaWhereExe(binary, execFn = defaultExecFn) {
51
+ try {
52
+ await execFn("where.exe", [binary]);
53
+ return true;
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
52
59
  // Used only when `which` is absent (e.g. minimal Alpine containers).
53
60
  // `command -v` is a POSIX shell built-in available wherever /bin/sh is.
54
- async function isBinaryViaCommandV(binary) {
61
+ export async function isBinaryViaCommandV(binary) {
55
62
  try {
56
63
  // Pass binary as a positional arg ($1) to avoid any shell-injection risk.
57
64
  await execFileAsync("sh", ["-c", 'command -v "$1"', "--", binary]);
@@ -61,6 +68,16 @@ async function isBinaryViaCommandV(binary) {
61
68
  return false;
62
69
  }
63
70
  }
71
+ // POSIX: use `which` to check if a binary is in PATH.
72
+ export async function isBinaryViaWhich(binary, execFn = defaultExecFn) {
73
+ try {
74
+ await execFn("which", [binary]);
75
+ return true;
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }
64
81
  function isENOENT(err) {
65
82
  return (typeof err === "object" &&
66
83
  err !== null &&
@@ -13,7 +13,12 @@ export function planRevert(manifest, detectedAgents, home) {
13
13
  if (!agent)
14
14
  continue;
15
15
  const target = resolveAgentSkillPath(agent, skill.name, home);
16
- actions.push({ skill: skill.name, agent: agentId, target });
16
+ actions.push({
17
+ kind: "skill-dir",
18
+ skill: skill.name,
19
+ agent: agentId,
20
+ target,
21
+ });
17
22
  }
18
23
  }
19
24
  return actions;
@@ -26,7 +31,12 @@ export function planRevertAll(manifest, home) {
26
31
  if (!agent)
27
32
  continue;
28
33
  const target = resolveAgentSkillPath(agent, skill.name, home);
29
- actions.push({ skill: skill.name, agent: agentId, target });
34
+ actions.push({
35
+ kind: "skill-dir",
36
+ skill: skill.name,
37
+ agent: agentId,
38
+ target,
39
+ });
30
40
  }
31
41
  }
32
42
  return actions;
@@ -43,15 +53,24 @@ export async function executeRevert(actions, dryRun, verbose, home) {
43
53
  let skipped = 0;
44
54
  const failed = [];
45
55
  for (const action of actions) {
46
- const result = await executeRevertAction(action, dryRun, verbose, home);
47
- if (result.outcome === "fail") {
48
- failed.push({ action, error: result.error });
49
- }
50
- else if (result.outcome === "skip") {
51
- skipped++;
52
- }
53
- else {
54
- succeeded++;
56
+ switch (action.kind) {
57
+ case "skill-dir": {
58
+ const result = await executeRevertAction(action, dryRun, verbose, home);
59
+ if (result.outcome === "fail") {
60
+ failed.push({ action, error: result.error });
61
+ }
62
+ else if (result.outcome === "skip") {
63
+ skipped++;
64
+ }
65
+ else {
66
+ succeeded++;
67
+ }
68
+ break;
69
+ }
70
+ default: {
71
+ const _ = action.kind;
72
+ throw new Error(`Unhandled revert action kind: ${_}`);
73
+ }
55
74
  }
56
75
  }
57
76
  return { succeeded, skipped, failed };
@@ -13,30 +13,30 @@ export type AgentId = z.output<typeof AgentIdSchema>;
13
13
  export declare const SkillEntrySchema: z.ZodObject<{
14
14
  name: z.ZodString;
15
15
  path: z.ZodString;
16
- agents: z.ZodArray<z.ZodPipe<z.ZodString, z.ZodEnum<{
16
+ agents: z.ZodPipe<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodEnum<{
17
17
  "claude-code": "claude-code";
18
18
  codex: "codex";
19
19
  "gemini-cli": "gemini-cli";
20
20
  antigravity: "antigravity";
21
21
  opencode: "opencode";
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
25
  export declare const ManifestSchema: z.ZodObject<{
26
26
  skills: z.ZodArray<z.ZodObject<{
27
27
  name: z.ZodString;
28
28
  path: z.ZodString;
29
- agents: z.ZodArray<z.ZodPipe<z.ZodString, z.ZodEnum<{
29
+ agents: z.ZodPipe<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodEnum<{
30
30
  "claude-code": "claude-code";
31
31
  codex: "codex";
32
32
  "gemini-cli": "gemini-cli";
33
33
  antigravity: "antigravity";
34
34
  opencode: "opencode";
35
35
  "github-copilot": "github-copilot";
36
- }>>>;
36
+ }>>>, z.ZodTransform<("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[], ("claude-code" | "codex" | "gemini-cli" | "antigravity" | "opencode" | "github-copilot")[]>>;
37
37
  }, z.core.$strip>>;
38
- mcpServers: z.ZodCatch<z.ZodArray<z.ZodUnknown>>;
39
- agentRules: z.ZodCatch<z.ZodArray<z.ZodUnknown>>;
38
+ mcpServers: z.ZodDefault<z.ZodArray<z.ZodUnknown>>;
39
+ agentRules: z.ZodDefault<z.ZodArray<z.ZodUnknown>>;
40
40
  }, z.core.$strip>;
41
41
  export type SkillEntry = z.infer<typeof SkillEntrySchema>;
42
42
  export type Manifest = z.infer<typeof ManifestSchema>;
@@ -44,12 +44,25 @@ export const SkillEntrySchema = z.object({
44
44
  }),
45
45
  agents: z
46
46
  .array(agentIdElement, { message: "agents must be a non-empty array" })
47
- .min(1, { message: "agents must be a non-empty array" }),
47
+ .min(1, { message: "agents must be a non-empty array" })
48
+ .transform((arr) => [...new Set(arr)]),
48
49
  });
49
50
  export const ManifestSchema = z.object({
50
- skills: z.array(SkillEntrySchema),
51
- mcpServers: z.array(z.unknown()).catch([]),
52
- agentRules: z.array(z.unknown()).catch([]),
51
+ skills: z.array(SkillEntrySchema).superRefine((skills, ctx) => {
52
+ const seen = new Set();
53
+ for (const [i, skill] of skills.entries()) {
54
+ if (seen.has(skill.name)) {
55
+ ctx.addIssue({
56
+ code: "custom",
57
+ path: [i, "name"],
58
+ message: `duplicate skill name "${skill.name}"`,
59
+ });
60
+ }
61
+ seen.add(skill.name);
62
+ }
63
+ }),
64
+ mcpServers: z.array(z.unknown()).default([]),
65
+ agentRules: z.array(z.unknown()).default([]),
53
66
  });
54
67
  // Parses the --agents CLI flag: comma-separated agent IDs → AgentId[]
55
68
  export const AgentListSchema = z
package/dist/types.d.ts CHANGED
@@ -18,18 +18,22 @@ export interface AgentConfig {
18
18
  detectBinary: string | null;
19
19
  provenance: AgentProvenance;
20
20
  }
21
- export interface DeployAction {
21
+ export interface SkillDirDeployAction {
22
+ kind: "skill-dir";
22
23
  skill: string;
23
24
  agent: AgentId;
24
25
  source: string;
25
26
  target: string;
26
27
  method: "symlink" | "copy";
27
28
  }
28
- export interface RevertAction {
29
+ export type DeployAction = SkillDirDeployAction;
30
+ export interface SkillDirRevertAction {
31
+ kind: "skill-dir";
29
32
  skill: string;
30
33
  agent: AgentId;
31
34
  target: string;
32
35
  }
36
+ export type RevertAction = SkillDirRevertAction;
33
37
  export interface CliOptions {
34
38
  command: "deploy" | "revert" | "help";
35
39
  directory: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kuznai/inception-engine",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
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",