@ruvnet/open-claude-code 2.1.1 → 2.1.2-nightly.20261011.203

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/README.md CHANGED
@@ -17,7 +17,7 @@
17
17
  <img alt="Nightly" src="https://img.shields.io/badge/nightly-verified_releases-brightgreen?style=flat-square" />
18
18
  </p>
19
19
 
20
- > **Verified nightly releases:** Detection, source validation, tests, AI analysis and npm publication run automatically. Capability proposals require review before implementation. See the release workflow and ADR-004.
20
+ > **Release automation:** The nightly workflow gates publication on source validation, tests, AI analysis, and registry checks. A successful run is the evidence of completion; failed runs do not advance checkpoints. Capability proposals require review before implementation. See the release workflow and ADR-004.
21
21
 
22
22
  ---
23
23
 
@@ -44,6 +44,53 @@ npx @ruvnet/open-claude-code "what files are in this directory?"
44
44
 
45
45
  ---
46
46
 
47
+
48
+ ## Skills and Claude Code plugin
49
+
50
+ Three shared skills cover CLI usage, diagnosis, and release verification. Choose one installation method to avoid duplicate skills.
51
+
52
+ ### Install with npx skills
53
+
54
+ The tested skills installer requires Node 22.20 or newer (Node 24 recommended). OCC itself requires Node 20 or newer.
55
+
56
+ ```bash
57
+ npx skills@1.7.2 add ruvnet/open-claude-code --agent claude-code --skill occ-use occ-diagnose occ-verify-release
58
+ ```
59
+
60
+ OCC 2.1.2 also loads these project skills from `.claude/skills`, including the installer's symlinks. Project skills override user skills. Invoke them with OCC's Skill tool or slash commands.
61
+
62
+ ### Install the native Claude Code plugin
63
+
64
+ In Claude Code:
65
+
66
+ ```text
67
+ /plugin marketplace add ruvnet/open-claude-code
68
+ /plugin install occ@ruvnet-occ
69
+ /occ:occ-use
70
+ /occ:occ-diagnose
71
+ /occ:occ-verify-release
72
+ ```
73
+
74
+ Restart Claude Code after installation if the current session has not loaded the new skills.
75
+
76
+ ```bash
77
+ claude plugin update occ@ruvnet-occ
78
+ claude plugin uninstall occ@ruvnet-occ
79
+ ```
80
+
81
+ The `occ` plugin uses the same `skills/` sources. It installs no hooks, MCP servers, credentials, or background processes. It guides use of the separately published CLI. This native Claude Code package does not imply compatibility between OCC's custom PluginLoader and all third party Claude plugins.
82
+
83
+ ### Reproduce installation tests
84
+
85
+ ```bash
86
+ # From a repository checkout, with Node 24 and Claude Code installed:
87
+ node scripts/plugin-e2e.mjs
88
+ # Test public GitHub distribution after publishing:
89
+ node scripts/plugin-e2e.mjs ruvnet/open-claude-code
90
+ ```
91
+
92
+ The suite installs and removes real skills and plugins in disposable profiles, checks all three Claude skill expansions with a local model fixture, and verifies OCC discovery. Live provider behavior is tested separately by `scripts/live-package-e2e.mjs`. Tested installer: skills 1.7.2; Claude Code: 2.1.283.
93
+
47
94
  ## 🧠 What Is This?
48
95
 
49
96
  **Open Claude Code** is a ground-up open source rebuild of Anthropic's [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code), informed by [ruDevolution's](https://github.com/ruvnet/rudevolution) AI-powered decompilation of the published npm package.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ruvnet/open-claude-code",
3
- "version": "2.1.1",
3
+ "version": "2.1.2-nightly.20261011.203",
4
4
  "description": "Open source agentic coding CLI with verified nightly upstream analysis",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,11 +12,22 @@ const MAX_TOOL_RECURSION_DEPTH = 50;
12
12
 
13
13
  export function createAgentLoop({ model, tools, permissions, settings, hooks, cascade = null }) {
14
14
  const contextManager = new ContextManager(settings.maxContextTokens || 180000);
15
+ for (const key of ['allowedTools', 'disallowedTools']) {
16
+ if (settings[key] != null && (!Array.isArray(settings[key]) ||
17
+ settings[key].some(name => typeof name !== 'string' || !name.trim()))) {
18
+ throw new TypeError(key + ' must be an array of nonempty tool names');
19
+ }
20
+ }
21
+ const allowedNames = new Set(settings.allowedTools || []);
22
+ const deniedNames = new Set(settings.disallowedTools || []);
23
+ const toolAllowed = name => !deniedNames.has(name) &&
24
+ (allowedNames.size === 0 || allowedNames.has(name));
25
+ const availableTools = () => (tools.list?.() || []).filter(tool => toolAllowed(tool.name));
15
26
 
16
27
  // Build system prompt using the new builder
17
28
  const promptResult = buildSystemPrompt({
18
29
  cwd: process.cwd(),
19
- tools: tools.list?.() || [],
30
+ tools: availableTools(),
20
31
  override: settings.systemPromptOverride,
21
32
  addDirs: settings.addDirs,
22
33
  });
@@ -113,7 +124,7 @@ export function createAgentLoop({ model, tools, permissions, settings, hooks, ca
113
124
  try {
114
125
  if (settings.stream !== false && provider === 'anthropic') {
115
126
  // Streaming mode
116
- response = await callApiStreaming(provider, activeModel, state, tools.list(), settings);
127
+ response = await callApiStreaming(provider, activeModel, state, availableTools(), settings);
117
128
  const collectedContent = [];
118
129
  let currentText = '';
119
130
  let currentThinking = '';
@@ -140,7 +151,7 @@ export function createAgentLoop({ model, tools, permissions, settings, hooks, ca
140
151
  response = response.accumulated;
141
152
  } else {
142
153
  // Non-streaming mode
143
- response = await callApi(provider, activeModel, state, tools.list(), settings);
154
+ response = await callApi(provider, activeModel, state, availableTools(), settings);
144
155
  }
145
156
  } catch (err) {
146
157
  recordOptOutcome(false);
@@ -180,6 +191,14 @@ export function createAgentLoop({ model, tools, permissions, settings, hooks, ca
180
191
  const toolResults = [];
181
192
 
182
193
  for (const block of toolUseBlocks) {
194
+ // Enforce before hooks and dispatch, even if the model invents an
195
+ // unadvertised call. Deny rules take precedence over allow rules.
196
+ if (!toolAllowed(block.name)) {
197
+ yield { type: 'hookPermissionResult', tool: block.name, allowed: false };
198
+ toolResults.push({ type: 'tool_result', tool_use_id: block.id,
199
+ is_error: true, content: 'Tool blocked by configured allow/deny list' });
200
+ continue;
201
+ }
183
202
  // Run pre-tool hooks
184
203
  if (hooks) {
185
204
  const hookResult = await hooks.runPreToolUse(block.name, block.input);
@@ -29,12 +29,13 @@ export class SkillsLoader {
29
29
  */
30
30
  load(cwd = process.cwd()) {
31
31
  const isDefaultCwd = cwd === process.cwd();
32
+ this.skills.clear();
32
33
  this.searchPaths = [path.join(cwd, '.claude', 'skills')];
33
34
 
34
35
  if (isDefaultCwd) {
35
36
  const globalSkills = path.join(process.env.HOME || '', '.claude', 'skills');
36
37
  if (globalSkills !== this.searchPaths[0]) {
37
- this.searchPaths.push(globalSkills);
38
+ this.searchPaths.unshift(globalSkills);
38
39
  }
39
40
  }
40
41
 
@@ -49,7 +50,8 @@ export class SkillsLoader {
49
50
  try {
50
51
  const entries = fs.readdirSync(dir, { withFileTypes: true });
51
52
  for (const entry of entries) {
52
- if (!entry.isDirectory()) continue;
53
+ // skills CLI installs directory symlinks by default.
54
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
53
55
 
54
56
  const skillFile = path.join(dir, entry.name, 'SKILL.md');
55
57
  try {