agent-skillboard 0.2.8 → 0.2.9

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/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.2.9 — 2026-07-03
6
+
7
+ ### Changed
8
+
9
+ - Lowered the published runtime engine requirement from Node.js `>=20` to
10
+ `>=14.21` after validating CLI smoke on Node 14.21 and 16.20 plus the full
11
+ test suite on Node 18.19.
12
+
13
+ ### Fixed
14
+
15
+ - Replaced runtime use of `node:readline/promises` and
16
+ `String.prototype.replaceAll` so the CLI can load and run on older Node.js
17
+ versions without npm `EBADENGINE` warnings on common system Node installs.
18
+
5
19
  ## 0.2.8 — 2026-07-03
6
20
 
7
21
  ### Added
package/docs/install.md CHANGED
@@ -19,6 +19,10 @@ Codex, Claude, OpenCode, and Hermes user skill roots. The setup is best-effort:
19
19
  it never fails the package install, does not edit agent config files, and does
20
20
  not create project policy files.
21
21
 
22
+ The published CLI supports Node.js 14.21 or newer. Node 12 and older are not
23
+ supported without a transpiled bundle because the source uses modern ESM and
24
+ syntax such as nullish coalescing.
25
+
22
26
  AI/automation/operator details:
23
27
 
24
28
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-skillboard",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Let AI agents pick and use allowed skills in each workflow.",
5
5
  "keywords": [
6
6
  "ai-agent",
@@ -52,7 +52,7 @@
52
52
  "yaml": "^2.8.1"
53
53
  },
54
54
  "engines": {
55
- "node": ">=20"
55
+ "node": ">=14.21"
56
56
  },
57
57
  "license": "MIT",
58
58
  "devDependencies": {
@@ -70,5 +70,5 @@ function emptyApplication(blockedReason) {
70
70
  }
71
71
 
72
72
  function shellQuote(value) {
73
- return /^[A-Za-z0-9_./:=@-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
73
+ return /^[A-Za-z0-9_./:=@-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
74
74
  }
@@ -43,7 +43,7 @@ export function userHermesUnit(path, home) {
43
43
  }
44
44
 
45
45
  export function hermesProfileUnit(path, home) {
46
- const profile = safeSegment(basename(dirname(path))).replaceAll(".", "-");
46
+ const profile = safeSegment(basename(dirname(path))).replace(/\./g, "-");
47
47
  return userSkillUnit(`hermes.profile.${profile}.skills`, path, home);
48
48
  }
49
49
 
@@ -55,7 +55,7 @@ export function customUserUnit(path, home) {
55
55
  }
56
56
 
57
57
  export function isHermesProfileSkillsPath(path) {
58
- const normalized = path.replaceAll("\\", "/");
58
+ const normalized = path.replace(/\\/g, "/");
59
59
  return /\/\.hermes\/profiles\/[^/]+\/skills$/u.test(normalized);
60
60
  }
61
61
 
@@ -72,7 +72,7 @@ export function displayPath(path, home) {
72
72
  return "~";
73
73
  }
74
74
  if (!rel.startsWith("..") && !isAbsolute(rel)) {
75
- return `~/${rel.replaceAll("\\", "/")}`;
75
+ return `~/${rel.replace(/\\/g, "/")}`;
76
76
  }
77
77
  return resolvedPath;
78
78
  }
@@ -242,7 +242,7 @@ function localWorkflowTarget(unit) {
242
242
  const profile = unit.id.slice("hermes.profile.".length, -".skills".length);
243
243
  return { harness: "hermes", workflow: `hermes-${profile}-local-manual` };
244
244
  }
245
- const base = safeSegment(unit?.id ?? "local").replaceAll(".", "-");
245
+ const base = safeSegment(unit?.id ?? "local").replace(/\./g, "-");
246
246
  return { harness: "local", workflow: `${base}-local-manual` };
247
247
  }
248
248
 
@@ -495,7 +495,7 @@ async function findSkillFiles(root, base, options, seen = new Set()) {
495
495
  const entries = await readdir(root, { withFileTypes: true }).catch(() => []);
496
496
  for (const entry of entries) {
497
497
  const path = join(root, entry.name);
498
- const rel = relative(base, path).replaceAll("\\", "/");
498
+ const rel = relative(base, path).replace(/\\/g, "/");
499
499
  if (options.excludeSystem === true && (rel === ".system" || rel.startsWith(".system/"))) {
500
500
  continue;
501
501
  }
@@ -539,7 +539,7 @@ function skillIdFor(unit, root, file, frontmatter) {
539
539
  }
540
540
 
541
541
  function skillPath(root, file) {
542
- return relative(root, dirname(file)).replaceAll("\\", "/");
542
+ return relative(root, dirname(file)).replace(/\\/g, "/");
543
543
  }
544
544
 
545
545
  function skillNode(skill, defaults = {}) {
@@ -266,7 +266,7 @@ function digest(content) {
266
266
  }
267
267
 
268
268
  function resolveRelativeTarget(root, value) {
269
- const relativePath = String(value).replaceAll("\\", "/");
269
+ const relativePath = String(value).replace(/\\/g, "/");
270
270
  if (relativePath.trim() === "" || relativePath.includes("\0")) {
271
271
  throw new Error("skill path must be a non-empty relative path");
272
272
  }
@@ -565,8 +565,8 @@ function workflowSummary(workflow) {
565
565
  }
566
566
 
567
567
  function safeText(value, maxLength = 180) {
568
- const compact = String(value).replaceAll(/\s+/g, " ").trim();
569
- const withoutTicks = compact.replaceAll("`", "'");
568
+ const compact = String(value).replace(/\s+/g, " ").trim();
569
+ const withoutTicks = compact.replace(/`/g, "'");
570
570
  return withoutTicks.length > maxLength ? `${withoutTicks.slice(0, maxLength - 3)}...` : withoutTicks;
571
571
  }
572
572
 
package/src/cli.mjs CHANGED
@@ -1097,7 +1097,7 @@ function formatList(values) {
1097
1097
  }
1098
1098
 
1099
1099
  function shellQuote(value) {
1100
- return /^[A-Za-z0-9_./:=@-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
1100
+ return /^[A-Za-z0-9_./:=@-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
1101
1101
  }
1102
1102
 
1103
1103
  function readCsv(value) {
@@ -35,7 +35,7 @@ export function resolveVariantSnapshotFile({ configPath, snapshotPath }) {
35
35
  throw new Error(`snapshot path must stay under ${SNAPSHOT_ROOT}`);
36
36
  }
37
37
  return {
38
- storedPath: relative(configDir, absolutePath).replaceAll("\\", "/"),
38
+ storedPath: relative(configDir, absolutePath).replace(/\\/g, "/"),
39
39
  absolutePath,
40
40
  rootPath: root
41
41
  };
@@ -213,7 +213,7 @@ function normalizeStoredSnapshotPath(value) {
213
213
  if (value.includes("\0")) {
214
214
  throw new Error("snapshot path must not contain null bytes");
215
215
  }
216
- const normalized = value.replaceAll("\\", "/");
216
+ const normalized = value.replace(/\\/g, "/");
217
217
  if (normalized.startsWith("/") || normalized.startsWith("//") || /^[A-Za-z]:\//.test(normalized)) {
218
218
  throw new Error("snapshot path must be relative to the config directory");
219
219
  }
package/src/hook-plan.mjs CHANGED
@@ -143,7 +143,7 @@ function modeToPermissions(mode) {
143
143
  }
144
144
 
145
145
  function shellQuote(value) {
146
- return `'${value.replaceAll("'", "'\\''")}'`;
146
+ return `'${value.replace(/'/g, "'\\''")}'`;
147
147
  }
148
148
 
149
149
  function safeHookFilePart(value) {
@@ -315,9 +315,9 @@ function normalizePath(value, root) {
315
315
 
316
316
  function displayPath(path, root) {
317
317
  if (!isAbsolute(path)) {
318
- return path.replaceAll("\\", "/");
318
+ return path.replace(/\\/g, "/");
319
319
  }
320
- const rel = relative(root, path).replaceAll("\\", "/");
320
+ const rel = relative(root, path).replace(/\\/g, "/");
321
321
  return rel.startsWith("..") ? path : rel;
322
322
  }
323
323
 
@@ -1,6 +1,6 @@
1
1
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
3
- import { createInterface } from "node:readline/promises";
3
+ import { createInterface } from "node:readline";
4
4
  import { setupAgentSkillTargets, supportedAgentNames } from "./agent-skill-roots.mjs";
5
5
  import { initProject } from "./init.mjs";
6
6
  import { uninstallProject } from "./uninstall.mjs";
@@ -180,13 +180,19 @@ async function promptForSetup(runtime) {
180
180
  output: runtime.stdout
181
181
  });
182
182
  try {
183
- const answer = await rl.question("Install SkillBoard agent integration now? [y/N] ");
183
+ const answer = await question(rl, "Install SkillBoard agent integration now? [y/N] ");
184
184
  return /^(y|yes)$/i.test(answer.trim());
185
185
  } finally {
186
186
  rl.close();
187
187
  }
188
188
  }
189
189
 
190
+ function question(rl, prompt) {
191
+ return new Promise((resolve) => {
192
+ rl.question(prompt, resolve);
193
+ });
194
+ }
195
+
190
196
  function formatList(values) {
191
197
  return values.length === 0 ? "none" : values.map((value) => `\`${value}\``).join(", ");
192
198
  }
@@ -299,7 +305,7 @@ ${AGENT_INTEGRATION_END}
299
305
 
300
306
  function commandPrefix(runtime) {
301
307
  const entrypoint = runtime.entrypointPath ?? "";
302
- const normalized = entrypoint.replaceAll("\\", "/");
308
+ const normalized = entrypoint.replace(/\\/g, "/");
303
309
  if (normalized.includes("/_npx/")) {
304
310
  const packageSpec = runtime.packageSpec ?? "agent-skillboard";
305
311
  return `npx --yes --package ${shellQuote(packageSpec)} skillboard`;
@@ -314,7 +320,7 @@ function isSourceTreeEntrypoint(entrypoint) {
314
320
  if (entrypoint === "") {
315
321
  return false;
316
322
  }
317
- const normalized = entrypoint.replaceAll("\\", "/");
323
+ const normalized = entrypoint.replace(/\\/g, "/");
318
324
  return (normalized === "bin/skillboard.mjs" || normalized.endsWith("/bin/skillboard.mjs"))
319
325
  && !normalized.includes("/node_modules/")
320
326
  && !normalized.includes("/_npx/")
@@ -323,7 +329,7 @@ function isSourceTreeEntrypoint(entrypoint) {
323
329
 
324
330
  function sourceTreeEntrypoint(entrypoint, cwd) {
325
331
  const absoluteEntrypoint = isAbsolute(entrypoint) ? entrypoint : resolve(cwd, entrypoint);
326
- const relativeEntrypoint = relative(cwd, absoluteEntrypoint).replaceAll("\\", "/");
332
+ const relativeEntrypoint = relative(cwd, absoluteEntrypoint).replace(/\\/g, "/");
327
333
  if (!relativeEntrypoint.startsWith("../") && relativeEntrypoint !== ".." && !isAbsolute(relativeEntrypoint)) {
328
334
  return shellQuote(relativeEntrypoint);
329
335
  }
@@ -334,5 +340,5 @@ function shellQuote(value) {
334
340
  if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) {
335
341
  return value;
336
342
  }
337
- return `'${value.replaceAll("'", "'\\''")}'`;
343
+ return `'${value.replace(/'/g, "'\\''")}'`;
338
344
  }
@@ -5,7 +5,7 @@ export function normalizeSkillPath(value, label = "skill path") {
5
5
  if (value.includes("\0")) {
6
6
  throw new Error(`${label} must not contain null bytes`);
7
7
  }
8
- const normalized = value.replaceAll("\\", "/");
8
+ const normalized = value.replace(/\\/g, "/");
9
9
  if (normalized.startsWith("/") || normalized.startsWith("//") || /^[A-Za-z]:\//.test(normalized)) {
10
10
  throw new Error(`${label} must be relative to the skills root`);
11
11
  }
@@ -164,7 +164,7 @@ function resolveUnderRoot(root, path) {
164
164
  }
165
165
 
166
166
  function relativePath(root, path) {
167
- const rel = relative(root, path).replaceAll("\\", "/");
167
+ const rel = relative(root, path).replace(/\\/g, "/");
168
168
  return rel.startsWith("..") ? path : rel;
169
169
  }
170
170
 
@@ -14,7 +14,7 @@ export async function importSource(options) {
14
14
  const matchedFiles = files
15
15
  .map((file) => ({
16
16
  file,
17
- relativeFile: relative(sourceRoot, file).replaceAll("\\", "/")
17
+ relativeFile: relative(sourceRoot, file).replace(/\\/g, "/")
18
18
  }))
19
19
  .filter((entry) => matchesAnyProfilePattern(entry.relativeFile, profile.skillPaths))
20
20
  .sort((left, right) => left.relativeFile.localeCompare(right.relativeFile));
@@ -238,9 +238,9 @@ function matchingPathRule(file, profile) {
238
238
  function patternToRegExp(pattern) {
239
239
  const escaped = pattern
240
240
  .replace(/[.+^${}()|[\]\\]/g, "\\$&")
241
- .replaceAll("**", "\0")
242
- .replaceAll("*", "[^/]*")
243
- .replaceAll("\0", ".*");
241
+ .split("**").join("\0")
242
+ .split("*").join("[^/]*")
243
+ .split("\0").join(".*");
244
244
  return new RegExp(`^${escaped}$`);
245
245
  }
246
246
 
@@ -165,7 +165,7 @@ export async function sourceDigest(path) {
165
165
 
166
166
  async function addPathDigest(hash, root, path) {
167
167
  const stats = await lstat(path);
168
- const rel = relative(root, path).replaceAll("\\", "/") || ".";
168
+ const rel = relative(root, path).replace(/\\/g, "/") || ".";
169
169
  if (stats.isSymbolicLink()) {
170
170
  hash.update(`symlink\0${rel}\0${await readlink(path)}\n`);
171
171
  return;
package/src/workspace.mjs CHANGED
@@ -51,7 +51,7 @@ async function discoverInstalledSkills(skillsRoot, declaredSkills, options = {})
51
51
  const skillFiles = await findSkillFiles(skillsRoot);
52
52
  for (const file of skillFiles) {
53
53
  const frontmatter = parseSkillFrontmatter(await readFile(file, "utf8"));
54
- const path = relative(skillsRoot, file).replaceAll("\\", "/").replace(/\/SKILL\.md$/, "");
54
+ const path = relative(skillsRoot, file).replace(/\\/g, "/").replace(/\/SKILL\.md$/, "");
55
55
  const declared = declaredSkills.find((skill) => skill.path === path);
56
56
  appendInstalledSkill(installed, installedKeys, {
57
57
  id: declared?.id ?? frontmatter.name ?? path,