worclaude 2.9.3 → 2.10.1

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
@@ -4,6 +4,47 @@ All notable changes to worclaude are documented in this file. Format loosely fol
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.10.1] — 2026-04-29
8
+
9
+ Adds opt-in scaffolding for Claude Code 2.1.113's `sandbox.network` deny/allow lists. Worclaude is a scaffolder, so the new `templates/settings/base.json` ships empty `deniedDomains` and `allowedDomains` stubs rather than an opinionated default list — project owners decide their own network policy. The merge paths for both fresh init (Scenario A) and existing-project init/upgrade (Scenarios B/C) union-merge the new arrays preserving any user-added domains, and a new `worclaude doctor` check warns when the block is missing or malformed (with `worclaude upgrade` as the remediation hint). Also bundles a Dependabot major bump to `commander` 14, which is now Node-20+-only and was unblocked by the v2.10.0 Node 18 drop.
10
+
11
+ ### Added
12
+
13
+ - **Sandbox network scaffolding** (PR #172) — `templates/settings/base.json` now scaffolds `sandbox.network.deniedDomains: []` and `allowedDomains: []` between the existing `permissions` and `hooks` blocks. New `mergeSettings` helper `unionStringList(inputs, accessor)` in `src/core/scaffolder.js` handles both `permissions.allow` and the new sandbox arrays uniformly. Backward compatible: a base without `sandbox` produces output without `sandbox`, so legacy callers in tests or downstream consumers don't surface the key spuriously.
14
+ - **`appendUnique(target, key, source)` helper** in `src/core/merger.js` (PR #172) — folds three previously-duplicated union-merge call sites in `mergeSettingsPermissionsAndHooks` (allow / deny / sandbox-arrays) into one-liners. Extracted during a `/simplify` pass after three parallel review agents flagged the duplication.
15
+ - **`checkSandboxBlock` doctor check** (PR #172) — warns when `settings.json` is missing the `sandbox` block (with a `worclaude upgrade` remediation pointer for legacy installs), when `sandbox.network` is malformed, or when either array isn't actually an array.
16
+
17
+ ### Changed
18
+
19
+ - **`commander` 13.1.0 → 14.0.3** (PR #171) — Dependabot major bump. Commander 14 requires Node 20+ (already satisfied after v2.10.0's Node 18 drop) and adds `helpGroup`/`optionsGroup`/`commandsGroup` APIs plus unescaped negative-number support. Worclaude's CLI surface is unaffected.
20
+ - ⚠ **PR #171 shipped without a `Version bump:` declaration** — Dependabot-generated body, no manual annotation. Treated as `none` per `/sync`'s "missing → none" rule and surfaced here permanently. PR #172's `patch` declaration drove the release.
21
+
22
+ ### Tests
23
+
24
+ - 967 → 992 (+25 net). Per-stack sandbox-array assertions across all 16 supported language templates (replaced one all-stacks loop test for individual failure attribution); 3 scaffolder unit tests covering union-merge, dedup, and legacy-passthrough; 2 Scenario B regressions for legacy-install upgrade and user-domain preservation through subsequent merges; 2 doctor checks for missing-block (legacy install) and malformed-block scenarios.
25
+
26
+ Release group: 2 PRs (1 patch, 1 missing-declaration treated as none). v2.10.0 → v2.10.1.
27
+
28
+ ## [2.10.0] — 2026-04-29
29
+
30
+ Drops support for Node 18, which reached LTS end-of-life on 2025-04-30 (12 months before this release). The drop unblocks two Dependabot PRs stuck on Node-20-only features (`inquirer 13`'s `util.styleText` and `ora 9`'s regex `v` flag) and ships those bumps in the same release. Also recovers from a Dependabot routing misconfiguration: `.github/dependabot.yml` now declares `target-branch: develop` for both ecosystems, fixing a config gap that caused 5 PRs in the v2.9.3 → v2.10.0 window to be opened against main instead of develop. Their content is preserved across both branches via a recovery sync.
31
+
32
+ ### Breaking
33
+
34
+ - **Node 18 no longer supported** (PR #167) — `engines.node` is now `>=20.0.0`. Running `npm install -g worclaude` on Node 18 will print an `EBADENGINE` warning (npm doesn't block by default but the warning is visible). CI test matrix dropped from `[18, 20, 22]` to `[20, 22]`. Required-status-checks on the `develop-protection` and `main-protection` rulesets updated accordingly. Tech-stack mentions refreshed in CLAUDE.md, AGENTS.md, README.md, `docs/guide/getting-started.md`, and `templates/specs/spec-md-library.md`.
35
+
36
+ ### Changed
37
+
38
+ - **`ora` 8.2.0 → 9.4.0** (PR #169) — major bump. ora 9 uses regex `v` flag (Node 20+); previously blocked by the v2.9.x Node 18 matrix.
39
+ - **`inquirer` 12.11.1 → 13.4.2** (PR #169) — major bump. inquirer 13 uses `util.styleText` (Node 20.12+); previously blocked by the v2.9.x Node 18 matrix.
40
+ - **Dependabot routing fixed** (PR #168) — added `target-branch: develop` to both `npm` and `github-actions` ecosystems in `.github/dependabot.yml`. Previously, Dependabot defaulted to the repo's default branch (main), causing PRs to misroute. Future Dependabot Monday runs will correctly target develop.
41
+
42
+ ### Internal
43
+
44
+ - **Recovery sync develop ← main** (PR #168) — brings 5 misrouted Dependabot squash commits from main onto develop (prettier 3.8.3, claude-code-action 1.0.109, actions/cache 5, vitest 4, eslint 10). All updates were legitimate; merge made via `git merge origin/main --no-ff` with auto-resolution.
45
+
46
+ Release group: 3 PRs (1 minor, 1 patch, 1 none). No missing Version bump declarations.
47
+
7
48
  ## [2.9.3] — 2026-04-29
8
49
 
9
50
  Security tooling refresh shipped as a paired group: a CI-tooling migration from Snyk (whose free-tier scan limit had blocked the v2.9.2 release PR) to a GitHub-native open-source SCA stack (Dependabot + OSV-Scanner), and the cleanup of the inaugural CodeQL scan after enabling the default setup. CodeQL surfaced 5 findings — 2× High "Incomplete multi-character sanitization" on the project-scanner README detector's HTML-stripping helpers, and 3× Medium "Workflow does not contain permissions" on `ci.yml`'s three jobs — all closed in this release. The sanitization fix extracts a `stripUntilStable(text, regex)` helper for the do-while-until-stable pattern; the permissions fix adds a top-level `permissions: contents: read` block matching the rest of the repo's workflows. SECURITY.md's AI-detected typosquat section also refined with the actual chain context: the `claude` npm package is `bcherny/redirect-claude`, an intentional Boris-Cherny-maintained typosquat-warning redirect, not an abandoned package as previously documented.
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  <a href="https://www.npmjs.com/package/worclaude"><img src="https://img.shields.io/npm/dm/worclaude" alt="downloads" /></a>
8
8
  <a href="https://github.com/sefaertunc/Worclaude/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/sefaertunc/Worclaude/ci.yml?label=tests" alt="tests" /></a>
9
9
  <a href="LICENSE"><img src="https://img.shields.io/github/license/sefaertunc/Worclaude" alt="license" /></a>
10
- <img src="https://img.shields.io/badge/node-%3E%3D18-brightgreen" alt="node >= 18" />
10
+ <img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen" alt="node >= 20" />
11
11
  <img src="https://img.shields.io/badge/built%20for-Claude%20Code-cc785c" alt="Built for Claude Code" />
12
12
  </p>
13
13
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worclaude",
3
- "version": "2.9.3",
3
+ "version": "2.10.1",
4
4
  "description": "The Workflow Layer for Claude Code — scaffold agents, commands, skills, hooks, and memory into any project",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,7 +28,7 @@
28
28
  "funding": "https://github.com/sponsors/sefaertunc",
29
29
  "author": "Sefa Ertunç",
30
30
  "engines": {
31
- "node": ">=18.0.0"
31
+ "node": ">=20.0.0"
32
32
  },
33
33
  "scripts": {
34
34
  "test": "vitest run",
@@ -66,18 +66,18 @@
66
66
  "dependencies": {
67
67
  "@sefaertunc/anthropic-watch-client": "^1.0.2",
68
68
  "chalk": "^5.4.1",
69
- "commander": "^13.1.0",
69
+ "commander": "^14.0.3",
70
70
  "fs-extra": "^11.3.0",
71
- "inquirer": "^12.5.0",
72
- "ora": "^8.2.0",
71
+ "inquirer": "^13.4.2",
72
+ "ora": "^9.4.0",
73
73
  "smol-toml": "^1.6.1",
74
74
  "yaml": "^2.8.3"
75
75
  },
76
76
  "devDependencies": {
77
- "eslint": "^9.22.0",
77
+ "eslint": "^10.2.1",
78
78
  "prettier": "^3.5.3",
79
79
  "vitepress": "^1.6.4",
80
- "vitest": "^3.0.9"
80
+ "vitest": "^4.1.5"
81
81
  },
82
82
  "overrides": {
83
83
  "brace-expansion": "^1.1.13",
@@ -286,6 +286,36 @@ async function readSettingsJson(projectRoot) {
286
286
  }
287
287
  }
288
288
 
289
+ async function checkSandboxBlock(projectRoot) {
290
+ const settings = await readSettingsJson(projectRoot);
291
+ if (!settings) return null;
292
+
293
+ if (!settings.sandbox) {
294
+ return result(
295
+ WARN,
296
+ 'Sandbox block',
297
+ 'settings.json missing `sandbox` block. Run `worclaude upgrade` to scaffold network deny/allow lists.'
298
+ );
299
+ }
300
+
301
+ const network = settings.sandbox.network;
302
+ if (!network || typeof network !== 'object') {
303
+ return result(WARN, 'Sandbox block', '`sandbox.network` block missing or malformed');
304
+ }
305
+
306
+ const issues = [];
307
+ if (!Array.isArray(network.deniedDomains)) {
308
+ issues.push('`deniedDomains` not an array');
309
+ }
310
+ if (!Array.isArray(network.allowedDomains)) {
311
+ issues.push('`allowedDomains` not an array');
312
+ }
313
+ if (issues.length > 0) {
314
+ return result(WARN, 'Sandbox block', issues.join('; '));
315
+ }
316
+ return result(PASS, 'Sandbox block', null);
317
+ }
318
+
289
319
  async function checkHookEventNames(projectRoot) {
290
320
  const settings = await readSettingsJson(projectRoot);
291
321
  if (!settings) {
@@ -1047,6 +1077,7 @@ export async function doctorCommand(options = {}) {
1047
1077
  record('core', await checkClaudeMdMemoryGuidance(projectRoot));
1048
1078
  record('core', await checkAgentsMd(projectRoot));
1049
1079
  record('core', await checkSettingsJson(projectRoot));
1080
+ record('core', await checkSandboxBlock(projectRoot));
1050
1081
  record('core', await checkSessions(projectRoot));
1051
1082
  spacer();
1052
1083
 
@@ -241,20 +241,28 @@ export async function mergeSettingsPermissionsAndHooks(
241
241
  const existing = parseUserJson(existingRaw, '.claude/settings.json');
242
242
 
243
243
  // Merge permissions (Tier 1) — union-merge both allow and deny
244
- const existingAllow = existing.permissions?.allow || [];
245
- const workflowAllow = workflowSettings.permissions?.allow || [];
246
- const newAllow = workflowAllow.filter((p) => !existingAllow.includes(p));
247
244
  if (!existing.permissions) existing.permissions = {};
248
- existing.permissions.allow = [...existingAllow, ...newAllow];
245
+ const newAllow = appendUnique(existing.permissions, 'allow', workflowSettings.permissions?.allow);
249
246
 
250
- const existingDeny = existing.permissions?.deny || [];
251
- const workflowDeny = workflowSettings.permissions?.deny || [];
252
- const newDeny = workflowDeny.filter((p) => !existingDeny.includes(p));
253
- if (newDeny.length > 0 || existingDeny.length > 0) {
254
- existing.permissions.deny = [...existingDeny, ...newDeny];
247
+ const existingDenyLen = (existing.permissions.deny ?? []).length;
248
+ const newDeny = appendUnique(existing.permissions, 'deny', workflowSettings.permissions?.deny);
249
+ if (existingDenyLen === 0 && newDeny === 0) {
250
+ delete existing.permissions.deny;
255
251
  }
256
252
 
257
- report.added.permissions = newAllow.length + newDeny.length;
253
+ report.added.permissions = newAllow + newDeny;
254
+
255
+ // Merge sandbox block (Tier 1 — additive, preserves user customizations)
256
+ const workflowSandbox = workflowSettings.sandbox?.network;
257
+ if (workflowSandbox) {
258
+ if (!existing.sandbox) existing.sandbox = {};
259
+ if (!existing.sandbox.network || typeof existing.sandbox.network !== 'object') {
260
+ existing.sandbox.network = {};
261
+ }
262
+ for (const key of ['deniedDomains', 'allowedDomains']) {
263
+ appendUnique(existing.sandbox.network, key, workflowSandbox[key]);
264
+ }
265
+ }
258
266
 
259
267
  // Merge hooks (Tier 1 + Tier 3)
260
268
  if (!existing.hooks) existing.hooks = {};
@@ -360,6 +368,13 @@ async function mergeSettingsJson(projectRoot, existingScan, selections, report)
360
368
  }
361
369
  }
362
370
 
371
+ function appendUnique(target, key, source) {
372
+ const existing = Array.isArray(target[key]) ? target[key] : [];
373
+ const additions = Array.isArray(source) ? source.filter((d) => !existing.includes(d)) : [];
374
+ target[key] = [...existing, ...additions];
375
+ return additions.length;
376
+ }
377
+
363
378
  function countHooks(hooks) {
364
379
  if (!hooks) return 0;
365
380
  return Object.values(hooks).reduce((sum, entries) => sum + entries.length, 0);
@@ -204,16 +204,31 @@ export async function scaffoldMemoryDocs(projectRoot) {
204
204
 
205
205
  export function mergeSettings(base, ...stacks) {
206
206
  const merged = JSON.parse(JSON.stringify(base));
207
- const baseAllow = merged.permissions?.allow || [];
207
+ const inputs = [base, ...stacks].filter(Boolean);
208
208
 
209
- for (const stack of stacks) {
210
- if (!stack) continue;
211
- const stackAllow = stack.permissions?.allow || [];
212
- if (stackAllow.length > 0) {
213
- baseAllow.push(...stackAllow);
214
- }
209
+ merged.permissions.allow = unionStringList(inputs, (i) => i.permissions?.allow);
210
+
211
+ if (merged.sandbox?.network) {
212
+ merged.sandbox.network.deniedDomains = unionStringList(
213
+ inputs,
214
+ (i) => i.sandbox?.network?.deniedDomains
215
+ );
216
+ merged.sandbox.network.allowedDomains = unionStringList(
217
+ inputs,
218
+ (i) => i.sandbox?.network?.allowedDomains
219
+ );
215
220
  }
216
221
 
217
- merged.permissions.allow = [...new Set(baseAllow)];
218
222
  return merged;
219
223
  }
224
+
225
+ function unionStringList(inputs, accessor) {
226
+ const set = new Set();
227
+ for (const input of inputs) {
228
+ const list = accessor(input);
229
+ if (Array.isArray(list)) {
230
+ for (const item of list) set.add(item);
231
+ }
232
+ }
233
+ return [...set];
234
+ }
@@ -81,6 +81,12 @@
81
81
  "Bash(rm -rf $HOME)", "Bash(rm -rf $HOME/*)"
82
82
  ]
83
83
  },
84
+ "sandbox": {
85
+ "network": {
86
+ "deniedDomains": [],
87
+ "allowedDomains": []
88
+ }
89
+ },
84
90
  "hooks": {
85
91
  "PostToolUse": [
86
92
  {
@@ -52,7 +52,7 @@ catch (error) { /* [ErrorType]: [message] */ }
52
52
  ## Compatibility Matrix
53
53
  | Runtime/Version | Supported | Notes |
54
54
  |------------------------|-----------|----------------------------|
55
- | [Node.js >= 18] | Yes | [ESM and CJS] |
55
+ | [Node.js >= 20] | Yes | [ESM and CJS] |
56
56
  | [Node.js 16] | No | [Reason] |
57
57
  | [Python >= 3.10] | Yes | [Type hints required] |
58
58
  | [Browser (ESM)] | Yes | [Bundle size: ~N kB gzip] |