wicked-brain 0.12.0 → 0.12.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/install.mjs CHANGED
@@ -11,8 +11,44 @@ const __dirname = fileURLToPath(new URL(".", import.meta.url));
11
11
  const skillsSource = join(__dirname, "skills");
12
12
  const home = homedir();
13
13
 
14
+ // Claude-root candidate builder. Claude Code's config root is redirectable
15
+ // via $CLAUDE_CONFIG_DIR (multi-tenant setups, alt-config layouts, corporate
16
+ // home-dir overrides). Mirrors the 0.3.3 wicked-testing fix: env var is
17
+ // authoritative when set; otherwise we probe ~/.claude + common alt-config
18
+ // paths and install into each that carries Claude identity markers.
19
+ function buildClaudeTarget(rootDir, source, { trusted = false } = {}) {
20
+ return {
21
+ name: "claude",
22
+ rootDir,
23
+ dir: join(rootDir, "skills"),
24
+ agentDir: join(rootDir, "agents"),
25
+ agentSubdir: "agents",
26
+ platform: "claude",
27
+ identityMarkers: ["settings.json", "plugins", "projects"],
28
+ source,
29
+ trusted,
30
+ };
31
+ }
32
+
33
+ function resolveClaudeCandidates() {
34
+ const envDir = process.env.CLAUDE_CONFIG_DIR;
35
+ if (envDir && typeof envDir === "string" && envDir.trim()) {
36
+ // Function replacement avoids `$&` etc. being interpreted as regex
37
+ // back-references if $HOME contains those literals (pathological but
38
+ // cheap to defend against — flagged by gemini on the sibling PRs).
39
+ const root = resolve(envDir.trim().replace(/^~/, () => home));
40
+ return [buildClaudeTarget(root, "env:CLAUDE_CONFIG_DIR", { trusted: true })];
41
+ }
42
+ return [
43
+ buildClaudeTarget(join(home, ".claude"), "default"),
44
+ buildClaudeTarget(join(home, "alt-configs", ".claude"), "alt-configs"),
45
+ buildClaudeTarget(join(home, ".config", "claude"), "xdg"),
46
+ ];
47
+ }
48
+
49
+ // Canonical non-claude targets. Claude is expanded dynamically via
50
+ // resolveClaudeCandidates() below so CLI_TARGETS stays a flat spec.
14
51
  const CLI_TARGETS = [
15
- { name: "claude", dir: join(home, ".claude", "skills"), agentDir: join(home, ".claude", "agents"), agentSubdir: "agents", platform: "claude" },
16
52
  { name: "gemini", dir: join(home, ".gemini", "skills"), agentDir: join(home, ".gemini", "agents"), agentSubdir: "agents", platform: "gemini" },
17
53
  { name: "copilot", dir: join(home, ".github", "skills"), agentDir: join(home, ".github", "agents"), agentSubdir: "agents", platform: "copilot" },
18
54
  { name: "codex", dir: join(home, ".codex", "skills"), agentDir: join(home, ".codex", "agents"), agentSubdir: "agents", platform: "codex" },
@@ -21,22 +57,61 @@ const CLI_TARGETS = [
21
57
  { name: "antigravity", dir: join(home, ".antigravity", "skills"), agentDir: join(home, ".antigravity", "rules"), agentSubdir: "rules", platform: "antigravity" },
22
58
  ];
23
59
 
60
+ // Identity-marker gate for claude candidates. Without this, probing
61
+ // ~/.claude, ~/alt-configs/.claude, and ~/.config/claude would install
62
+ // into every path that happens to exist — risky if one was created by a
63
+ // different tool. Env-var / --path targets are `trusted` and skip this.
64
+ function claudeHasIdentityMarker(target) {
65
+ if (target.trusted) return true;
66
+ if (!existsSync(target.rootDir)) return false;
67
+ return (target.identityMarkers || []).some(m => existsSync(join(target.rootDir, m)));
68
+ }
69
+
24
70
  console.log("wicked-brain installer\n");
25
71
 
26
72
  const args = argv.slice(2);
27
- const argValue = (a) => a.split("=")[1];
28
- const cliArg = args.find((a) => a.startsWith("--cli="));
29
- const pathArg = args.find((a) => a.startsWith("--path="));
73
+
74
+ // Flag parser supporting both forms:
75
+ // --flag=value (canonical)
76
+ // --flag value (common shell muscle-memory; previously silently
77
+ // dropped the value and fell through to default
78
+ // detection — same bug that hit wicked-testing 0.3.2).
79
+ // Narrow string-boolean coercion: literal "true" / "false" become
80
+ // booleans so `--hooks=false` doesn't install hooks.
81
+ const flagValue = (name) => {
82
+ const f = args.find(a => a === `--${name}` || a.startsWith(`--${name}=`));
83
+ if (!f) return null;
84
+ let val;
85
+ if (f.includes("=")) {
86
+ // slice from the first '=' forward — split("=")[1] would truncate at
87
+ // the second '=' (e.g. --path=/volumes/build=artifacts would silently
88
+ // drop "=artifacts").
89
+ val = f.slice(f.indexOf("=") + 1);
90
+ } else {
91
+ const idx = args.indexOf(f);
92
+ const next = args[idx + 1];
93
+ val = (next && !next.startsWith("-")) ? next : true;
94
+ }
95
+ if (val === "false") return false;
96
+ if (val === "true") return true;
97
+ return val;
98
+ };
99
+
100
+ const cliArg = flagValue("cli");
101
+ const pathArg = flagValue("path");
102
+
103
+ // Validate --cli upfront — if the user passed a bare --cli or --cli=,
104
+ // they misspoke and we should not silently fall through to "install
105
+ // everywhere". Applies regardless of whether --path is also set.
106
+ if (cliArg === true || cliArg === "") {
107
+ console.error("Error: --cli requires a value (e.g. --cli=claude or --cli claude)");
108
+ process.exit(1);
109
+ }
30
110
 
31
111
  let targets;
32
112
 
33
- if (pathArg) {
34
- const rawPath = argValue(pathArg);
35
- if (!rawPath) {
36
- console.error("Error: --path requires a value (e.g. --path=~/.claude)");
37
- process.exit(1);
38
- }
39
- const customPath = resolve(rawPath.replace(/^~/, home));
113
+ if (pathArg && typeof pathArg === "string" && pathArg !== "") {
114
+ const customPath = resolve(pathArg.replace(/^~/, () => home));
40
115
  // Strip leading dot to match CLI_TARGETS names (e.g. ".claude" → "claude")
41
116
  const dirName = basename(customPath).replace(/^\./, "");
42
117
  const knownPlatform = CLI_TARGETS.find((t) => t.name === dirName);
@@ -48,19 +123,33 @@ if (pathArg) {
48
123
  platform: knownPlatform?.platform ?? dirName,
49
124
  }];
50
125
  console.log(`Custom path: ${customPath}\n`);
126
+ } else if (pathArg === true || pathArg === "") {
127
+ console.error("Error: --path requires a value (e.g. --path=~/.claude or --path ~/.claude)");
128
+ process.exit(1);
51
129
  } else {
52
- // Detect which CLIs are installed by checking if parent dir exists
53
- const detected = CLI_TARGETS.filter((t) => existsSync(resolve(t.dir, "..")));
130
+ // Build the detection set: expanded claude candidates (env var OR alt-config
131
+ // probes) + all non-claude targets. Claude candidates pass an identity-marker
132
+ // check so we don't install into a bare ~/.claude that belongs to some other
133
+ // tool. Non-claude targets keep the original parent-dir-exists heuristic.
134
+ const claudeDetected = resolveClaudeCandidates().filter(claudeHasIdentityMarker);
135
+ const otherDetected = CLI_TARGETS.filter((t) => existsSync(resolve(t.dir, "..")));
136
+ const detected = [...claudeDetected, ...otherDetected];
54
137
 
55
138
  if (detected.length === 0) {
56
139
  console.log("No supported AI CLIs detected. Supported: claude, gemini, copilot, codex, cursor, kiro, antigravity");
57
- console.log("Install skills manually by copying the skills/ directory.");
140
+ console.log("Install skills manually by copying the skills/ directory, or set CLAUDE_CONFIG_DIR.");
58
141
  process.exit(1);
59
142
  }
60
143
 
61
- console.log(`Detected CLIs: ${detected.map((d) => d.name).join(", ")}\n`);
144
+ // Annotate claude candidates with their source when more than one was
145
+ // detected, so the log is not ambiguous.
146
+ const claudeCount = claudeDetected.length;
147
+ const label = (d) => d.name === "claude" && claudeCount > 1 && d.source
148
+ ? `${d.name}[${d.source}]`
149
+ : d.name;
150
+ console.log(`Detected CLIs: ${detected.map(label).join(", ")}\n`);
62
151
 
63
- const cliFilter = cliArg ? argValue(cliArg).split(",") : null;
152
+ const cliFilter = (typeof cliArg === "string" && cliArg !== "") ? cliArg.split(",") : null;
64
153
  targets = cliFilter ? detected.filter((d) => cliFilter.includes(d.name)) : detected;
65
154
  }
66
155
 
@@ -106,8 +195,10 @@ for (const target of targets) {
106
195
  console.log(` Installed ${agentCount} agents to ${target.agentDir}`);
107
196
  }
108
197
 
109
- // Optional hook installation (--hooks flag)
110
- const installHooks = args.includes("--hooks");
198
+ // Optional hook installation (--hooks flag). Goes through flagValue so
199
+ // `--hooks=false` correctly disables; bare `--hooks` and `--hooks=true`
200
+ // both enable (flagValue coerces "true"/"false" literals to booleans).
201
+ const installHooks = flagValue("hooks") === true;
111
202
 
112
203
  if (installHooks) {
113
204
  console.log("\nInstalling hooks...");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wicked-brain",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "type": "module",
5
5
  "description": "Digital brain as skills for AI coding CLIs — no vector DB, no embeddings, no infrastructure",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wicked-brain-server",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "type": "module",
5
5
  "description": "SQLite FTS5 search server for wicked-brain digital knowledge bases",
6
6
  "keywords": [