haac-aikit 0.14.1 → 0.15.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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: decide
3
- description: Use when the user types "/decide <topic>", "use the decide skill", or "make me a decision page on X" — a non-trivial choice with 2-4 viable options that needs tradeoffs visualized as one rich HTML page. Generates a self-contained file at `docs/decisions/YYYY-MM-DD-<slug>.html`. Opt-in: do not invoke proactively.
3
+ description: "Use when the user types \"/decide <topic>\", \"use the decide skill\", or \"make me a decision page on X\" — a non-trivial choice with 2-4 viable options that needs tradeoffs visualized as one rich HTML page. Generates a self-contained file at `docs/decisions/YYYY-MM-DD-<slug>.html`. Opt-in: do not invoke proactively."
4
4
  version: "1.0.0"
5
5
  source: haac-aikit
6
6
  license: MIT
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: directions
3
- description: Use when the user types "/directions <surface>", "show me design directions for X", or "explore visual options for the empty state / hero / dashboard card" — 2-4 rendered visual takes side-by-side on one self-contained HTML page with a light/dark toggle. Output: `docs/directions/YYYY-MM-DD-<slug>.html`. Opt-in only.
3
+ description: "Use when the user types \"/directions <surface>\", \"show me design directions for X\", or \"explore visual options for the empty state / hero / dashboard card\" — 2-4 rendered visual takes side-by-side on one self-contained HTML page with a light/dark toggle. Output: `docs/directions/YYYY-MM-DD-<slug>.html`. Opt-in only."
4
4
  version: "1.0.0"
5
5
  source: haac-aikit
6
6
  license: MIT
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: roadmap
3
- description: Use when the user types "/roadmap <feature>", "draw up a roadmap for X", or "give me a one-page implementation doc" — the approach is already settled and they want milestones + data-flow diagram + mockups + key code + risks + open questions on one committed HTML page. Output: `docs/roadmaps/YYYY-MM-DD-<slug>.html`. Opt-in only.
3
+ description: "Use when the user types \"/roadmap <feature>\", \"draw up a roadmap for X\", or \"give me a one-page implementation doc\" — the approach is already settled and they want milestones + data-flow diagram + mockups + key code + risks + open questions on one committed HTML page. Output: `docs/roadmaps/YYYY-MM-DD-<slug>.html`. Opt-in only."
4
4
  version: "1.0.0"
5
5
  source: haac-aikit
6
6
  license: MIT
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: test-driven-development
3
- description: Use when implementing new behaviour, fixing a confirmed bug, or when the user says "TDD", "test-first", "write a test for X", or "add tests". Enforces red-green-refactor: every behaviour gets a failing test first, then the minimal code to pass, then refactor — so untested code never enters the codebase.
3
+ description: "Use when implementing new behaviour, fixing a confirmed bug, or when the user says \"TDD\", \"test-first\", \"write a test for X\", or \"add tests\". Enforces red-green-refactor: every behaviour gets a failing test first, then the minimal code to pass, then refactor — so untested code never enters the codebase."
4
4
  version: "1.0.0"
5
5
  source: obra/superpowers
6
6
  license: MIT
package/dist/cli.mjs CHANGED
@@ -1422,12 +1422,33 @@ function parseFrontmatter(content) {
1422
1422
  inToolsList = true;
1423
1423
  continue;
1424
1424
  }
1425
- frontmatter[key] = val;
1425
+ frontmatter[key] = unquoteYamlScalar(val);
1426
1426
  }
1427
1427
  const doc = { frontmatter, body: body.trimStart() };
1428
1428
  if (toolsList) doc.toolsList = toolsList;
1429
1429
  return doc;
1430
1430
  }
1431
+ function unquoteYamlScalar(value) {
1432
+ if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
1433
+ const inner = value.slice(1, -1);
1434
+ return inner.replace(/\\(.)/g, (_2, ch) => {
1435
+ switch (ch) {
1436
+ case "n":
1437
+ return "\n";
1438
+ case "t":
1439
+ return " ";
1440
+ case "r":
1441
+ return "\r";
1442
+ default:
1443
+ return ch;
1444
+ }
1445
+ });
1446
+ }
1447
+ if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
1448
+ return value.slice(1, -1).replace(/''/g, "'");
1449
+ }
1450
+ return value;
1451
+ }
1431
1452
  function skillToCursorMdc(skill) {
1432
1453
  const description = skill.frontmatter["description"] ?? "";
1433
1454
  const escaped = description.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
@@ -1442,9 +1463,10 @@ ${skill.body}
1442
1463
  function skillToWindsurfRule(skill) {
1443
1464
  const description = skill.frontmatter["description"] ?? "";
1444
1465
  const name = skill.frontmatter["name"] ?? "(unnamed)";
1466
+ const escapedDesc = description.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1445
1467
  const header = `---
1446
1468
  trigger: model_decision
1447
- description: ${description}
1469
+ description: "${escapedDesc}"
1448
1470
  ---
1449
1471
 
1450
1472
  `;
@@ -1464,9 +1486,10 @@ _Truncated to fit Windsurf's 12k-char rule limit. Full skill: \`.claude/skills/$
1464
1486
  }
1465
1487
  function skillToCopilotInstruction(skill) {
1466
1488
  const description = skill.frontmatter["description"] ?? "";
1489
+ const escapedDesc = description.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1467
1490
  return `---
1468
1491
  applyTo: '**'
1469
- description: ${description}
1492
+ description: "${escapedDesc}"
1470
1493
  ---
1471
1494
 
1472
1495
  ${skill.body}
@@ -1492,8 +1515,9 @@ ${escaped}
1492
1515
  function agentToCopilotAgent(agent) {
1493
1516
  const name = agent.frontmatter["name"] ?? "";
1494
1517
  const description = agent.frontmatter["description"] ?? "";
1518
+ const escapedDesc = description.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1495
1519
  const model = agent.frontmatter["model"] ?? "";
1496
- const fm = [`name: ${name}`, `description: ${description}`];
1520
+ const fm = [`name: ${name}`, `description: "${escapedDesc}"`];
1497
1521
  if (model) fm.push(`model: ${model}`);
1498
1522
  if (agent.toolsList && agent.toolsList.length > 0) {
1499
1523
  fm.push(`tools:`);
@@ -1503,6 +1527,19 @@ function agentToCopilotAgent(agent) {
1503
1527
  ${fm.join("\n")}
1504
1528
  ---
1505
1529
 
1530
+ ${agent.body}
1531
+ `;
1532
+ }
1533
+ function agentToCursorAgent(agent) {
1534
+ const name = agent.frontmatter["name"] ?? "";
1535
+ const description = agent.frontmatter["description"] ?? "";
1536
+ const escapedDesc = description.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1537
+ return `---
1538
+ name: ${name}
1539
+ description: "${escapedDesc}"
1540
+ model: inherit
1541
+ ---
1542
+
1506
1543
  ${agent.body}
1507
1544
  `;
1508
1545
  }
@@ -1741,6 +1778,15 @@ async function runSync(argv) {
1741
1778
  if (!name) continue;
1742
1779
  results.push(safeWrite(`.cursor/rules/skill-${name}.mdc`, skillToCursorMdc(skill), { ...opts, useMarkers: false }));
1743
1780
  }
1781
+ results.push(...syncSkills("tier1", opts, ".cursor/skills"));
1782
+ results.push(...syncSkills("tier2", opts, ".cursor/skills"));
1783
+ if (config.integrations.subagents) {
1784
+ for (const agent of allAgents) {
1785
+ const name = agent.frontmatter["name"];
1786
+ if (!name) continue;
1787
+ results.push(safeWrite(`.cursor/agents/${name}.md`, agentToCursorAgent(agent), { ...opts, useMarkers: false }));
1788
+ }
1789
+ }
1744
1790
  if (config.integrations.hooks) {
1745
1791
  const srcDir = join2(CATALOG_ROOT, "hooks");
1746
1792
  const destDir = ".cursor/hooks";
@@ -1758,6 +1804,8 @@ async function runSync(argv) {
1758
1804
  }
1759
1805
  if (config.tools.includes("windsurf")) {
1760
1806
  results.push(safeWrite(".windsurf/rules/project.md", catalog.windsurfRules(), { ...opts, useMarkers: false }));
1807
+ results.push(...syncSkills("tier1", opts, ".windsurf/skills"));
1808
+ results.push(...syncSkills("tier2", opts, ".windsurf/skills"));
1761
1809
  for (const skill of allSkills) {
1762
1810
  const name = skill.frontmatter["name"];
1763
1811
  if (!name) continue;
@@ -1771,6 +1819,8 @@ async function runSync(argv) {
1771
1819
  if (!name) continue;
1772
1820
  results.push(safeWrite(`.github/instructions/${name}.instructions.md`, skillToCopilotInstruction(skill), { ...opts, useMarkers: false }));
1773
1821
  }
1822
+ results.push(...syncSkills("tier1", opts, ".github/skills"));
1823
+ results.push(...syncSkills("tier2", opts, ".github/skills"));
1774
1824
  if (config.integrations.subagents) {
1775
1825
  for (const agent of allAgents) {
1776
1826
  const name = agent.frontmatter["name"];
@@ -1802,6 +1852,8 @@ async function runSync(argv) {
1802
1852
  const subdir = htmlSkillNames.has(name) ? "html/" : "";
1803
1853
  results.push(safeWrite(`.gemini/commands/${subdir}${name}.toml`, skillToGeminiCommand(skill), { ...opts, useMarkers: false }));
1804
1854
  }
1855
+ results.push(...syncSkills("tier1", opts, ".gemini/skills"));
1856
+ results.push(...syncSkills("tier2", opts, ".gemini/skills"));
1805
1857
  if (config.integrations.mcp) {
1806
1858
  results.push(safeWrite(".gemini/settings.json", catalog.mcpJson(), { ...opts, useMarkers: false }));
1807
1859
  }
@@ -1923,12 +1975,22 @@ function copyAction(srcPath, destPath, opts) {
1923
1975
  }
1924
1976
  return { path: destPath, action: existed ? "updated" : "created", src: srcPath };
1925
1977
  }
1978
+ function assertSafeName(name, source) {
1979
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) {
1980
+ throw new Error(
1981
+ `aikit: refusing to sync '${source}' \u2014 name '${name}' contains characters outside [a-z0-9-]. This would either break the target tool's loader or (if it contains '/' or '..') escape the target directory. Rename the skill/agent so its frontmatter 'name:' matches /^[a-z0-9][a-z0-9-]*$/.`
1982
+ );
1983
+ }
1984
+ }
1926
1985
  function loadSkillsFromCatalog() {
1927
1986
  const out = [];
1928
1987
  for (const tier of ["tier1", "tier2"]) {
1929
- for (const name of listSkillFolders(tier)) {
1930
- const content = readFileSync5(join2(skillFolder(tier, name), "SKILL.md"), "utf8");
1931
- out.push(parseFrontmatter(content));
1988
+ for (const folder of listSkillFolders(tier)) {
1989
+ const content = readFileSync5(join2(skillFolder(tier, folder), "SKILL.md"), "utf8");
1990
+ const parsed = parseFrontmatter(content);
1991
+ const name = parsed.frontmatter["name"];
1992
+ if (name) assertSafeName(name, `catalog/skills/${tier}/${folder}/SKILL.md`);
1993
+ out.push(parsed);
1932
1994
  }
1933
1995
  }
1934
1996
  return out;
@@ -1944,7 +2006,10 @@ function loadAgentsFromCatalog(config) {
1944
2006
  const include = selection === "all" ? allNames : allNames.filter((a2) => selection.includes(a2));
1945
2007
  for (const name of include) {
1946
2008
  const content = readFileSync5(join2(dir, `${name}.md`), "utf8");
1947
- out.push(parseFrontmatter(content));
2009
+ const parsed = parseFrontmatter(content);
2010
+ const fmName = parsed.frontmatter["name"];
2011
+ if (fmName) assertSafeName(fmName, `catalog/agents/${tier}/${name}.md`);
2012
+ out.push(parsed);
1948
2013
  }
1949
2014
  }
1950
2015
  return out;
@@ -1954,8 +2019,7 @@ function listHookFiles() {
1954
2019
  if (!existsSync5(dir)) return [];
1955
2020
  return readdirSync2(dir).filter((f2) => f2.endsWith(".sh"));
1956
2021
  }
1957
- function syncSkills(tier, opts) {
1958
- const destRoot = `.claude/skills`;
2022
+ function syncSkills(tier, opts, destRoot = ".claude/skills") {
1959
2023
  const results = [];
1960
2024
  for (const name of listSkillFolders(tier)) {
1961
2025
  const srcRoot = skillFolder(tier, name);
@@ -3516,7 +3580,7 @@ function isInteractive() {
3516
3580
  }
3517
3581
 
3518
3582
  // src/cli.ts
3519
- var VERSION = "0.14.1";
3583
+ var VERSION = "0.15.0";
3520
3584
  var HELP = `
3521
3585
  haac-aikit \u2014 the batteries-included AI-coding kit
3522
3586