kodelyth-ecc 2.24.8 → 2.24.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 +68 -0
- package/CLAUDE.md +1 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/scripts/mcp/register-self.js +55 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,74 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Kodelyth ECC are documented here.
|
|
4
4
|
|
|
5
|
+
## v2.24.9 — a malformed Claude config was silently replaced on install (October 2026)
|
|
6
|
+
|
|
7
|
+
**Data-loss fix. Upgrade before running an install on a machine whose
|
|
8
|
+
`~/.claude.json` may have been hand-edited.**
|
|
9
|
+
|
|
10
|
+
MCP self-registration read the user's Claude config, merged ECC's server entry in,
|
|
11
|
+
and wrote it back:
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
const existing = readJson(file) || {};
|
|
15
|
+
const next = { ...existing, mcpServers: servers };
|
|
16
|
+
writeJson(file, next);
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`readJson` returned `null` for **every** failure. So a config that merely failed to
|
|
20
|
+
parse — one trailing comma from a hand-edit, a half-written file, a bad mount —
|
|
21
|
+
was indistinguishable from a config that did not exist, and `|| {}` turned it into
|
|
22
|
+
an empty object. The merge then wrote that back.
|
|
23
|
+
|
|
24
|
+
Measured on a realistic config with two other MCP servers, a `projects` block with
|
|
25
|
+
history, and user settings: **366 bytes in, 140 bytes out**. Every other MCP
|
|
26
|
+
server, every project entry, the theme and `autoUpdates` — gone. No error, no
|
|
27
|
+
warning, no backup.
|
|
28
|
+
|
|
29
|
+
This runs automatically during install. So `npx kodelyth-ecc` on a machine whose
|
|
30
|
+
`.claude.json` had any syntax error destroyed the user's Claude configuration.
|
|
31
|
+
|
|
32
|
+
### What changed
|
|
33
|
+
|
|
34
|
+
`readConfig` now returns one of three states instead of a nullable value:
|
|
35
|
+
|
|
36
|
+
| state | meaning | action |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `absent` | missing, empty, or whitespace-only | safe to treat as `{}` and create |
|
|
39
|
+
| `present` | parsed to an object | merge normally |
|
|
40
|
+
| `unreadable` | exists but will not parse, or is not an object | **refuse** |
|
|
41
|
+
|
|
42
|
+
Refusing is the point: there is no safe way to merge into a file whose contents
|
|
43
|
+
cannot be reconstructed. The file is left byte-identical and the error names it
|
|
44
|
+
along with the parse failure. `unregisterInFile` applies the same rule on the way
|
|
45
|
+
out — removing an entry from a file we cannot read means writing a file we cannot
|
|
46
|
+
reconstruct.
|
|
47
|
+
|
|
48
|
+
Writes now go through `safeFs.replaceFilePreservingMode`: atomic via
|
|
49
|
+
temp-and-rename, so a crash or a full disk partway through cannot truncate the
|
|
50
|
+
config — the same data loss by a different route — and the file keeps its existing
|
|
51
|
+
permissions rather than having them widened.
|
|
52
|
+
|
|
53
|
+
### Two mistakes caught while fixing it
|
|
54
|
+
|
|
55
|
+
The first version of the fix called `replaceFileAtomic` without its `mode`
|
|
56
|
+
argument, which threw on **every** path including valid configs. Testing the happy
|
|
57
|
+
path, not just the bug, is what surfaced it; verifying only that the refusal
|
|
58
|
+
worked would have shipped an installer that never registered anything.
|
|
59
|
+
|
|
60
|
+
The mode-preservation test then asserted `mode === 0o600` and failed Windows CI
|
|
61
|
+
with `438 !== 384`. Windows has no POSIX permission bits, so `chmod` there is close
|
|
62
|
+
to a no-op and node reports `0666` regardless. The property worth testing was never
|
|
63
|
+
the literal value but that the write does not *change* the mode, which is true on
|
|
64
|
+
every platform.
|
|
65
|
+
|
|
66
|
+
Eight tests, where this file previously had none: the three read states,
|
|
67
|
+
preservation of unrelated config on a valid merge, byte-identical refusal across
|
|
68
|
+
four kinds of malformed input, creation from absent and empty, idempotence,
|
|
69
|
+
unregister's refusal, mode preservation, and no stray temp files.
|
|
70
|
+
|
|
71
|
+
**690 tests across 44 files.**
|
|
72
|
+
|
|
5
73
|
## v2.24.8 — adversarial pass: swarm, dashboard, supply-chain (October 2026)
|
|
6
74
|
|
|
7
75
|
Three defects found by attacking the remaining unprobed surfaces rather than
|
package/CLAUDE.md
CHANGED
|
@@ -26,7 +26,7 @@ scripts/ → Node.js utilities: MCP server, dashboard, swarm, replay, router
|
|
|
26
26
|
bundles/ → 3 power bundles (indie-hacker, red-team, enterprise)
|
|
27
27
|
actions/ → GitHub Action (CI/CD integration for PR review)
|
|
28
28
|
docs/ → Feature docs (arena.md, mcp.md, dashboard.md, swarm.md, replay.md, evolve.md, supply-chain.md)
|
|
29
|
-
tests/ →
|
|
29
|
+
tests/ → 690 passing tests across 44 test files
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
## Running Tests
|
package/README.md
CHANGED
|
@@ -118,7 +118,7 @@ Most "AI agent kits" are folders of markdown files you have to remember the name
|
|
|
118
118
|
| Quality hooks | ✓ 44 | Some | ✗ | ✗ |
|
|
119
119
|
| IDE platforms | **11** (Claude Code, Windsurf, Cursor, Codex, Antigravity, OpenCode, Cline, Roo Code, Aider, Kimi, Gemini CLI) | 1-2 | 1 | Varies |
|
|
120
120
|
| Telemetry | **None** | Varies | None | Varies |
|
|
121
|
-
| Test coverage | ✓
|
|
121
|
+
| Test coverage | ✓ 690 tests | ✗ | ✗ | ✗ |
|
|
122
122
|
| Distributed via | `npx`, curl, clone | Manual | Manual | Manual |
|
|
123
123
|
|
|
124
124
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kodelyth-ecc",
|
|
3
|
-
"version": "2.24.
|
|
3
|
+
"version": "2.24.9",
|
|
4
4
|
"description": "Production-grade AI coding toolkit — 70 agents (incl. devil-mode adversarial crew), 196 skills, 103 commands, parallel multi-agent commands, semantic intent routing, self-learning memory, and a built-in MCP server (16 tools / 6 prompts / 377 resources) that bridges to Claude Desktop, LangGraph, AutoGen, CrewAI, and OpenAI Agents SDK. Works on macOS, Linux and Windows with Claude Code, Windsurf, Cursor, Codex, Antigravity, OpenCode, Cline, RooCode, Aider, Kimi, and Gemini CLI.",
|
|
5
5
|
"author": "Kodelyth <github.com/sifxprime>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
'use strict';
|
|
8
8
|
|
|
9
9
|
const fs = require('fs');
|
|
10
|
+
const safeFs = require('../lib/safe-fs');
|
|
10
11
|
const os = require('os');
|
|
11
12
|
const path = require('path');
|
|
12
13
|
|
|
@@ -24,13 +25,48 @@ function claudeDesktopConfigPath() {
|
|
|
24
25
|
return path.join(home, '.config', 'Claude', 'claude_desktop_config.json');
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
// Reading this file has THREE outcomes, and collapsing them is destructive.
|
|
29
|
+
//
|
|
30
|
+
// readJson used to return null for every failure, and the caller did
|
|
31
|
+
// `readJson(file) || {}`. So a config that merely failed to parse — one trailing
|
|
32
|
+
// comma from a hand-edit, a half-written file, a bad mount — was treated as an
|
|
33
|
+
// empty config, and the merge wrote it back containing only ECC's own entry.
|
|
34
|
+
// Measured on a realistic config: 366 bytes in, 140 bytes out, with every other
|
|
35
|
+
// MCP server, every project entry and every setting gone. Silently, during an
|
|
36
|
+
// install the user did not think was risky.
|
|
37
|
+
//
|
|
38
|
+
// Absent is safe to treat as empty. Unreadable is not.
|
|
39
|
+
function readConfig(file) {
|
|
40
|
+
let raw;
|
|
41
|
+
try {
|
|
42
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (err.code === 'ENOENT') return { state: 'absent', data: {} };
|
|
45
|
+
return { state: 'unreadable', reason: err.message };
|
|
46
|
+
}
|
|
47
|
+
// An empty or whitespace-only file is effectively absent; JSON.parse would
|
|
48
|
+
// throw on it and we would otherwise refuse to ever register.
|
|
49
|
+
if (raw.trim() === '') return { state: 'absent', data: {} };
|
|
50
|
+
try {
|
|
51
|
+
const data = JSON.parse(raw);
|
|
52
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
53
|
+
return { state: 'unreadable', reason: 'top-level value is not an object' };
|
|
54
|
+
}
|
|
55
|
+
return { state: 'present', data };
|
|
56
|
+
} catch (err) {
|
|
57
|
+
return { state: 'unreadable', reason: err.message };
|
|
58
|
+
}
|
|
29
59
|
}
|
|
30
60
|
|
|
31
61
|
function writeJson(file, obj) {
|
|
32
62
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
33
|
-
|
|
63
|
+
// Atomic: write a temp file and rename. A crash or a full disk partway through
|
|
64
|
+
// a plain writeFileSync leaves the user with a truncated config, which is the
|
|
65
|
+
// same data loss by a different route.
|
|
66
|
+
// PreservingMode rather than Atomic: this is the user's own config file and it
|
|
67
|
+
// may carry permissions they chose. It keeps the existing mode when the file is
|
|
68
|
+
// already there and falls back to 0644 when creating it.
|
|
69
|
+
safeFs.replaceFilePreservingMode(file, JSON.stringify(obj, null, 2) + '\n');
|
|
34
70
|
}
|
|
35
71
|
|
|
36
72
|
function serverEntry() {
|
|
@@ -42,7 +78,15 @@ function serverEntry() {
|
|
|
42
78
|
}
|
|
43
79
|
|
|
44
80
|
function registerInFile(file, opts = {}) {
|
|
45
|
-
const
|
|
81
|
+
const cfg = readConfig(file);
|
|
82
|
+
if (cfg.state === 'unreadable') {
|
|
83
|
+
// Refusing is the whole point. Overwriting would destroy whatever is in there.
|
|
84
|
+
throw new Error(
|
|
85
|
+
`refusing to modify ${file}: it exists but could not be parsed (${cfg.reason}). ` +
|
|
86
|
+
`Fix or move the file, then re-run. Nothing was written.`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
const existing = cfg.data;
|
|
46
90
|
const servers = existing.mcpServers || {};
|
|
47
91
|
const already = servers[SERVER_NAME];
|
|
48
92
|
const desired = serverEntry();
|
|
@@ -56,7 +100,10 @@ function registerInFile(file, opts = {}) {
|
|
|
56
100
|
}
|
|
57
101
|
|
|
58
102
|
function unregisterInFile(file) {
|
|
59
|
-
const
|
|
103
|
+
const cfg = readConfig(file);
|
|
104
|
+
// Same rule on the way out: an unparseable config is left exactly as it is.
|
|
105
|
+
if (cfg.state === 'unreadable') return { file, action: 'skipped-unreadable', reason: cfg.reason };
|
|
106
|
+
const existing = cfg.data;
|
|
60
107
|
if (!existing || !existing.mcpServers || !existing.mcpServers[SERVER_NAME]) {
|
|
61
108
|
return { file, action: 'not-present' };
|
|
62
109
|
}
|
|
@@ -113,4 +160,7 @@ module.exports = {
|
|
|
113
160
|
registerAll,
|
|
114
161
|
unregisterAll,
|
|
115
162
|
statusAll,
|
|
163
|
+
// Exported for tests: these must be exercisable without writing to a real
|
|
164
|
+
// ~/.claude.json, and the whole point of the fix is what they refuse to do.
|
|
165
|
+
_internals: { readConfig, registerInFile, unregisterInFile },
|
|
116
166
|
};
|