@kuznai/inception-engine 0.13.0 → 0.14.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.
@@ -9,7 +9,7 @@ import { compileAdapterActions } from "./adapters/index.js";
9
9
  import { applyTomlMcpPatch } from "./adapters/toml.js";
10
10
  import { lookupDeployment, registerDeployment, verifyDeployment, } from "./ownership.js";
11
11
  import { getDeployMethod, resolveAgentSkillPath } from "./resolve.js";
12
- import { resolveTargetTemplate } from "./runtime-paths.js";
12
+ import { getPathApi, resolveTargetTemplate } from "./runtime-paths.js";
13
13
  import { sourceAccessError, validateSkillDefinitionFile, validateSourceFile, validateSourcePath, } from "./validation.js";
14
14
  function isPlainObject(v) {
15
15
  return typeof v === "object" && v !== null && !Array.isArray(v);
@@ -87,9 +87,19 @@ function detectAmbiguities(detectedAgents, actions, home) {
87
87
  const hasGeminiCli = detectedAgents.includes("gemini-cli");
88
88
  const hasAntigravity = detectedAgents.includes("antigravity");
89
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);
90
+ const homePathApi = getPathApi(home);
91
+ const sharedGeminiMd = homePathApi.join(home, ".gemini", "GEMINI.md");
92
+ const sharedSettings = homePathApi.join(home, ".gemini", "settings.json");
93
+ // Normalize paths for comparison to handle case-insensitivity on Windows
94
+ const normalize = (pathStr) => homePathApi === path.win32
95
+ ? pathStr.toLowerCase()
96
+ : pathStr;
97
+ const normalizedGeminiMd = normalize(sharedGeminiMd);
98
+ const normalizedSettings = normalize(sharedSettings);
99
+ const targetsShared = actions.some((a) => {
100
+ const normalizedTarget = normalize(a.target);
101
+ return normalizedTarget === normalizedGeminiMd || normalizedTarget === normalizedSettings;
102
+ });
93
103
  if (targetsShared) {
94
104
  warnings.push({
95
105
  kind: "ambiguity",
package/dist/core/init.js CHANGED
@@ -3,6 +3,29 @@ import path from "node:path";
3
3
  import { dryRunPrefix, logger } from "../logger.js";
4
4
  import { AGENT_IDS } from "../schemas/manifest.js";
5
5
  const SAFE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
6
+ // Ordered list: first match wins. Catch-all is applied at call site.
7
+ const AGENT_RULES_FILE_PATTERNS = [
8
+ {
9
+ fileNames: ["claude.md", "claude-instructions.md"],
10
+ agents: ["claude-code"],
11
+ },
12
+ {
13
+ fileNames: ["agents.md", "agents-instructions.md"],
14
+ agents: ["codex", "opencode"],
15
+ },
16
+ {
17
+ fileNames: ["gemini.md", "gemini-instructions.md"],
18
+ agents: ["gemini-cli", "antigravity"],
19
+ },
20
+ { fileNames: ["copilot-instructions.md"], agents: ["github-copilot"] },
21
+ ];
22
+ // Conventional subdirectory names to scan one level deep for .md files.
23
+ const AGENT_RULES_SUBDIRS = [
24
+ "rules",
25
+ "instructions",
26
+ ".github",
27
+ ".agents/rules",
28
+ ];
6
29
  async function findSkillDirs(baseDir, dir, found) {
7
30
  let entries;
8
31
  try {
@@ -52,6 +75,91 @@ function buildSkills(found, agents) {
52
75
  }
53
76
  return skills;
54
77
  }
78
+ function defaultAgentsForFile(fileName, fallback) {
79
+ const lower = fileName.toLowerCase();
80
+ for (const { fileNames, agents } of AGENT_RULES_FILE_PATTERNS) {
81
+ if (fileNames.includes(lower))
82
+ return agents;
83
+ }
84
+ return fallback;
85
+ }
86
+ function isInsideSkillDir(relPath, skillDirRelPaths) {
87
+ const parentRelPath = path.dirname(relPath).split(path.sep).join("/");
88
+ if (skillDirRelPaths.has(parentRelPath))
89
+ return true;
90
+ return [...skillDirRelPaths].some((sp) => parentRelPath === sp || parentRelPath.startsWith(`${sp}/`));
91
+ }
92
+ function deriveAgentRulesName(relPath, fileName) {
93
+ const ext = path.extname(fileName).toLowerCase();
94
+ const baseName = path.basename(fileName, ext);
95
+ const rawName = baseName.toLowerCase().replace(/[^a-zA-Z0-9._-]/g, "-");
96
+ if (!SAFE_NAME_RE.test(rawName)) {
97
+ logger.warn("init", `Skipping "${relPath}": could not derive a valid agentRules name`);
98
+ return null;
99
+ }
100
+ return rawName;
101
+ }
102
+ async function scanDirForMarkdown(dir, baseDir, skillDirRelPaths, seen, candidates) {
103
+ let entries;
104
+ try {
105
+ entries = await readdir(dir, { withFileTypes: true, encoding: "utf-8" });
106
+ }
107
+ catch {
108
+ return;
109
+ }
110
+ for (const entry of entries) {
111
+ if (!entry.isFile())
112
+ continue;
113
+ const ext = path.extname(entry.name).toLowerCase();
114
+ if (ext !== ".md" && ext !== ".markdown")
115
+ continue;
116
+ const absPath = path.join(dir, entry.name);
117
+ const relPath = path.relative(baseDir, absPath).split(path.sep).join("/");
118
+ if (seen.has(relPath) || isInsideSkillDir(relPath, skillDirRelPaths))
119
+ continue;
120
+ const name = deriveAgentRulesName(relPath, entry.name);
121
+ if (name === null)
122
+ continue;
123
+ seen.add(relPath);
124
+ candidates.push({ relPath, name, defaultAgents: [] });
125
+ }
126
+ }
127
+ async function findAgentRulesCandidates(baseDir, skillDirRelPaths) {
128
+ const candidates = [];
129
+ const seen = new Set();
130
+ await scanDirForMarkdown(baseDir, baseDir, skillDirRelPaths, seen, candidates);
131
+ for (const subdir of AGENT_RULES_SUBDIRS) {
132
+ await scanDirForMarkdown(path.join(baseDir, subdir), baseDir, skillDirRelPaths, seen, candidates);
133
+ }
134
+ return candidates;
135
+ }
136
+ function buildAgentRules(candidates, activeAgents, skillNamesSeen) {
137
+ const rules = [];
138
+ const namesSeen = new Set(skillNamesSeen);
139
+ for (const { relPath, name: rawName } of candidates) {
140
+ const fileName = path.basename(relPath);
141
+ const defaultAgents = defaultAgentsForFile(fileName, activeAgents);
142
+ // Intersect with active agents; fall back to full active list if empty
143
+ const intersection = defaultAgents.filter((a) => activeAgents.includes(a));
144
+ const agents = intersection.length > 0 ? intersection : activeAgents;
145
+ // Resolve name collision with skill names
146
+ let name = rawName;
147
+ if (namesSeen.has(name)) {
148
+ const candidate = `${name}-rules`;
149
+ if (SAFE_NAME_RE.test(candidate)) {
150
+ logger.warn("init", `agentRules name "${name}" collides with a skill name; using "${candidate}"`);
151
+ name = candidate;
152
+ }
153
+ else {
154
+ logger.warn("init", `Skipping "${relPath}": name "${name}" collides with a skill name and fallback is invalid`);
155
+ continue;
156
+ }
157
+ }
158
+ namesSeen.add(name);
159
+ rules.push({ name, path: relPath, agents });
160
+ }
161
+ return rules;
162
+ }
55
163
  async function manifestExists(manifestPath) {
56
164
  try {
57
165
  await access(manifestPath);
@@ -73,27 +181,38 @@ export async function runInit(options) {
73
181
  await findSkillDirs(directory, directory, found);
74
182
  if (found.length === 0) {
75
183
  logger.info("No skill directories found (looking for directories containing SKILL.md).");
76
- return 0;
77
184
  }
78
185
  const skills = buildSkills(found, agents);
79
- if (skills.length === 0) {
80
- logger.info("No skills could be added to the manifest.");
81
- return 0;
82
- }
83
- const manifest = { skills, mcpServers: [], agentRules: [] };
186
+ const skillNamesSeen = new Set(skills.map((s) => s.name));
187
+ const skillDirRelPaths = new Set(found.map((f) => f.relPath));
188
+ const agentRulesCandidates = await findAgentRulesCandidates(directory, skillDirRelPaths);
189
+ const agentRules = buildAgentRules(agentRulesCandidates, agents, skillNamesSeen);
190
+ const manifest = {
191
+ skills,
192
+ files: [],
193
+ configs: [],
194
+ mcpServers: [],
195
+ agentRules,
196
+ };
84
197
  const json = `${JSON.stringify(manifest, null, 2)}\n`;
85
198
  if (dryRun) {
86
- logger.info(`${dryRunPrefix(true)}Would write ${manifestPath} with ${skills.length} skill(s):`);
199
+ logger.info(`${dryRunPrefix(true)}Would write ${manifestPath} with ${skills.length} skill(s) and ${agentRules.length} agentRule(s):`);
87
200
  logger.info("");
88
201
  logger.info(json);
89
202
  return 0;
90
203
  }
91
204
  await writeFile(manifestPath, json, "utf-8");
92
- logger.info(`Generated ${manifestPath} with ${skills.length} skill(s).`);
205
+ logger.info(`Generated ${manifestPath} with ${skills.length} skill(s) and ${agentRules.length} agentRule(s).`);
93
206
  if (verbose) {
94
207
  for (const s of skills) {
95
208
  logger.detail(`${s.name} → ${s.path}`);
96
209
  }
210
+ if (agentRules.length > 0) {
211
+ logger.detail("agentRules:");
212
+ for (const r of agentRules) {
213
+ logger.detail(` ${r.name} → ${r.path} [${r.agents.join(", ")}]`);
214
+ }
215
+ }
97
216
  }
98
217
  return 0;
99
218
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kuznai/inception-engine",
3
- "version": "0.13.0",
3
+ "version": "0.14.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",