@xmarts/genius-setup 1.8.0 → 1.10.0

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.
@@ -123,16 +123,17 @@ function runSetup(token) {
123
123
  if (secret) { try { fs.chmodSync(p, 0o600); } catch (_) {} }
124
124
  }
125
125
 
126
- // 1) ~/.genius/config.json + hook
126
+ // 1) ~/.genius/config.json + hook. MERGE into any existing config so state we
127
+ // track across runs (e.g. managed_mcp_keys for MCP de-provisioning) survives.
127
128
  ensureDir(HOOKS_DIR);
128
- const cfg = {
129
- base_url: BASE,
130
- token: token,
131
- capture_endpoint: BASE + '/xma/genius/v1/capture/transcript',
132
- inject_endpoint: BASE + '/xma/genius/v1/inject',
133
- };
129
+ const cfgPath0 = path.join(GENIUS_DIR, 'config.json');
130
+ const cfg = readJson(cfgPath0);
131
+ cfg.base_url = BASE;
132
+ cfg.token = token;
133
+ cfg.capture_endpoint = BASE + '/xma/genius/v1/capture/transcript';
134
+ cfg.inject_endpoint = BASE + '/xma/genius/v1/inject';
134
135
  if (CLIENT) cfg.target_client = CLIENT;
135
- writeJson(path.join(GENIUS_DIR, 'config.json'), cfg, true); // secret: 0600
136
+ writeJson(cfgPath0, cfg, true); // secret: 0600
136
137
 
137
138
  // Copy the hooks shipped alongside this script (capture + per-turn inject).
138
139
  function copyHook(fname) {
@@ -149,6 +150,7 @@ function runSetup(token) {
149
150
  const hookDst = copyHook('genius_capture.py');
150
151
  const injectDst = copyHook('genius_inject.py');
151
152
  const guardDst = copyHook('plan_committee_guard.py');
153
+ const presenceDst = copyHook('genius_presence.py');
152
154
 
153
155
  // 2) ~/.claude.json mcpServers (merge, no clobber)
154
156
  const claudeJsonPath = path.join(HOME, '.claude.json');
@@ -186,6 +188,18 @@ function runSetup(token) {
186
188
  // (XMA_PLAN_GUARD_MODE=block|warn|off). Unlike the fire-and-forget hooks above, this
187
189
  // one can return {"decision":"block"} — by design — but never traps a session.
188
190
  addHook('Stop', guardDst);
191
+ // Presence heartbeat (time-tracking "amarre"): binds worked time to THIS machine's
192
+ // clock — counts VSCode edits + executing loops/workflows, excludes idle-open VSCode.
193
+ // Unlike every hook above, this is a LONG-LIVED daemon (loops up to ~18h), so it MUST be
194
+ // launched DETACHED (nohup ... & disown) — never as a blocking `python3 X || true`, which
195
+ // would freeze every SessionStart. A single-instance lock + self-terminate make repeated
196
+ // SessionStart launches safe. POSIX form; mirrors the proven admin-bootstrap installer.
197
+ settings.hooks.SessionStart = settings.hooks.SessionStart || [];
198
+ if (!JSON.stringify(settings.hooks.SessionStart).includes('genius_presence.py')) {
199
+ const presenceCmd = 'nohup python3 ' + JSON.stringify(presenceDst) +
200
+ ' --cwd "${CLAUDE_PROJECT_DIR:-$PWD}" >/dev/null 2>&1 & disown 2>/dev/null; true';
201
+ settings.hooks.SessionStart.push({ hooks: [{ type: 'command', command: presenceCmd }] });
202
+ }
189
203
  writeJson(settingsPath, settings);
190
204
 
191
205
  // 4) ~/.claude/CLAUDE.md — install/refresh the Genius Protocol between auto-managed
@@ -211,12 +225,104 @@ function runSetup(token) {
211
225
  log('WARN: could not write CLAUDE.md protocol (' + e.message + ')');
212
226
  }
213
227
 
228
+ // 5) Fan out the consultant's OTHER MCP servers (asana, clickup, google, ...).
229
+ // Their non-secret shape (manifest) + per-device secret VALUES come from the
230
+ // Bearer-authed /xma/genius/v1/mcp/secrets endpoint. We write Claude Code
231
+ // always and Claude Desktop only if it is installed. Fail-open: a fetch
232
+ // error never breaks onboarding. Re-runs (self-update / epoch bump) converge.
233
+ function desktopConfigPath() {
234
+ try {
235
+ if (process.platform === 'darwin')
236
+ return path.join(HOME, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
237
+ if (process.platform === 'win32')
238
+ return path.join(process.env.APPDATA || path.join(HOME, 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json');
239
+ return path.join(HOME, '.config', 'Claude', 'claude_desktop_config.json');
240
+ } catch (_) { return ''; }
241
+ }
242
+ function mcpEntry(srv, secrets, host) {
243
+ // One mcpServers entry, or null if a required secret is missing (never write a
244
+ // half-broken server) or it is not portable to this host.
245
+ const env = {};
246
+ let ok = true;
247
+ (srv.env_keys || []).forEach((k) => { if (secrets[k]) env[k] = secrets[k]; else ok = false; });
248
+ if (!ok) return null;
249
+ const isHttp = srv.transport === 'http' || srv.transport === 'sse';
250
+ if (host === 'desktop') {
251
+ if (srv.host_targets !== 'both' || srv.desktop_strategy === 'skip') return null;
252
+ if (isHttp) return { command: 'npx', args: ['-y', 'mcp-remote', srv.url] }; // stdio-only Desktop
253
+ const e = { command: srv.command, args: srv.args || [] };
254
+ if (Object.keys(env).length) e.env = env;
255
+ return e;
256
+ }
257
+ if (isHttp) return { type: srv.transport, url: srv.url };
258
+ const e = { type: 'stdio', command: srv.command, args: srv.args || [] };
259
+ if (Object.keys(env).length) e.env = env;
260
+ return e;
261
+ }
262
+ function writeHostMcps(cfgPath, manifest, secrets, host, prevKeys) {
263
+ let obj = readJson(cfgPath);
264
+ if (!obj || typeof obj !== 'object') obj = {};
265
+ backup(cfgPath);
266
+ obj.mcpServers = obj.mcpServers || {};
267
+ // De-provision: drop servers WE previously managed that are no longer granted.
268
+ (prevKeys || []).forEach((k) => {
269
+ if (obj.mcpServers[k] && !manifest.find((s) => s.key === k)) delete obj.mcpServers[k];
270
+ });
271
+ const nowKeys = [];
272
+ manifest.forEach((srv) => {
273
+ const entry = mcpEntry(srv, secrets, host);
274
+ if (entry) { obj.mcpServers[srv.key] = entry; nowKeys.push(srv.key); }
275
+ else if (obj.mcpServers[srv.key] && (prevKeys || []).includes(srv.key)) {
276
+ delete obj.mcpServers[srv.key]; // lost its secret / portability -> remove
277
+ }
278
+ });
279
+ writeJson(cfgPath, obj, true); // may carry env secrets -> 0600
280
+ return nowKeys;
281
+ }
282
+ function provisionMcps(token) {
283
+ let url;
284
+ try { url = new URL(BASE + '/xma/genius/v1/mcp/secrets'); } catch (_) { return; }
285
+ const mod = url.protocol === 'http:' ? require('http') : require('https');
286
+ const payload = JSON.stringify({ jsonrpc: '2.0', method: 'call', id: 1, params: {} });
287
+ const req = mod.request({
288
+ hostname: url.hostname, port: url.port || (url.protocol === 'http:' ? 80 : 443),
289
+ path: url.pathname, method: 'POST',
290
+ headers: {
291
+ 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload),
292
+ 'Authorization': 'Bearer ' + token, 'User-Agent': 'XmartsGenius-Setup/1.6.0',
293
+ },
294
+ timeout: 15000,
295
+ }, (res) => {
296
+ let data = ''; res.on('data', (c) => { data += c; });
297
+ res.on('end', () => {
298
+ try {
299
+ const parsed = JSON.parse(data); const r = parsed.result || parsed;
300
+ if (!r || r.status !== 'ok' || !Array.isArray(r.manifest)) return;
301
+ const secrets = r.secrets || {};
302
+ const cfgPath = path.join(GENIUS_DIR, 'config.json');
303
+ const prev = (readJson(cfgPath).managed_mcp_keys) || [];
304
+ const managed = writeHostMcps(path.join(HOME, '.claude.json'), r.manifest, secrets, 'code', prev);
305
+ const dp = desktopConfigPath();
306
+ if (dp && (fs.existsSync(dp) || fs.existsSync(path.dirname(dp)))) {
307
+ try { writeHostMcps(dp, r.manifest, secrets, 'desktop', prev); } catch (_) {}
308
+ }
309
+ try { const c = readJson(cfgPath); c.managed_mcp_keys = managed; writeJson(cfgPath, c, true); } catch (_) {}
310
+ if (managed.length) log(' - MCP fan-out : ' + managed.join(', '));
311
+ } catch (_) {}
312
+ });
313
+ });
314
+ req.on('error', () => {}); req.on('timeout', () => { try { req.destroy(); } catch (_) {} });
315
+ req.write(payload); req.end();
316
+ }
317
+ provisionMcps(token);
318
+
214
319
  log('');
215
320
  log(' Xmarts Genius is set up.');
216
321
  log(' - MCP server : beesmart_genius -> ' + BASE + '/mcp/consultant');
217
322
  log(' - Capture hook : ' + hookDst + ' (SessionEnd / PreCompact / Stop)');
218
323
  log(' - Inject hook : ' + injectDst + ' (UserPromptSubmit / SessionStart)');
219
324
  log(' - Plan/committee : ' + guardDst + ' (Stop — enforces plan/comité → Brain; XMA_PLAN_GUARD_MODE=block|warn|off)');
325
+ log(' - Presence : ' + presenceDst + ' (SessionStart, detached — honest time-tracking amarre)');
220
326
  log(' - Config : ' + path.join(GENIUS_DIR, 'config.json'));
221
327
  log(' - Protocol : ' + path.join(CLAUDE_DIR, 'CLAUDE.md') + ' (auto-managed)');
222
328
  if (CLIENT) log(' - Default client : ' + CLIENT);
@@ -25,6 +25,7 @@ TIMEOUT = 3.5
25
25
  SELF_UPDATE_TTL = 86400 # at most once/day
26
26
  SELF_UPDATE_STAMP = os.path.join(HOME, ".genius", ".last_selfupdate")
27
27
  FORCE_STAMP = os.path.join(HOME, ".genius", ".last_forced_epoch")
28
+ MCP_EPOCH_STAMP = os.path.join(HOME, ".genius", ".last_mcp_epoch")
28
29
 
29
30
  SESSION_PRIMER = (
30
31
  "[Xmarts Genius — Brain connected]\n"
@@ -77,9 +78,10 @@ def _inject(cfg, prompt):
77
78
  result = data.get("result", data) or {}
78
79
  return ((result.get("context", "") or ""),
79
80
  result.get("hook_force_epoch"),
80
- result.get("hook_pin_version"))
81
+ result.get("hook_pin_version"),
82
+ result.get("mcp_manifest_epoch"))
81
83
  except Exception:
82
- return "", None, None
84
+ return "", None, None, None
83
85
 
84
86
 
85
87
  # M6 (Phase-0 integrity gate): NEVER resolve `@latest`. An unsigned `npx @latest`
@@ -101,7 +103,7 @@ def _pkg_spec(pin_version):
101
103
  return "@xmarts/genius-setup@%s" % PINNED_FALLBACK
102
104
 
103
105
 
104
- def _maybe_self_update(cfg, force_epoch=None, pin_version=None):
106
+ def _maybe_self_update(cfg, force_epoch=None, pin_version=None, mcp_epoch=None):
105
107
  """Keep the consultant on the SERVER-PINNED Genius package with ZERO action: at most
106
108
  once/day, silently re-run the installer in the background. New hooks, MCP
107
109
  tools, settings and the CLAUDE.md protocol then flow automatically — the
@@ -129,6 +131,18 @@ def _maybe_self_update(cfg, force_epoch=None, pin_version=None):
129
131
  forced = fe > last
130
132
  except Exception:
131
133
  forced = False
134
+ # MCP fan-out reconvergence: a newer manifest epoch forces an immediate
135
+ # re-run (re-provisions ~/.claude.json + Desktop), bypassing the daily TTL.
136
+ if not forced and mcp_epoch:
137
+ try:
138
+ me = int(mcp_epoch)
139
+ lastm = 0
140
+ if os.path.exists(MCP_EPOCH_STAMP):
141
+ with open(MCP_EPOCH_STAMP) as fh:
142
+ lastm = int((fh.read() or "0").strip() or 0)
143
+ forced = me > lastm
144
+ except Exception:
145
+ pass
132
146
  if not forced and os.path.exists(SELF_UPDATE_STAMP) and (
133
147
  time.time() - os.path.getmtime(SELF_UPDATE_STAMP)) < SELF_UPDATE_TTL:
134
148
  return
@@ -140,6 +154,9 @@ def _maybe_self_update(cfg, force_epoch=None, pin_version=None):
140
154
  if force_epoch:
141
155
  with open(FORCE_STAMP, "w") as fh:
142
156
  fh.write(str(int(force_epoch)))
157
+ if mcp_epoch:
158
+ with open(MCP_EPOCH_STAMP, "w") as fh:
159
+ fh.write(str(int(mcp_epoch)))
143
160
  except Exception:
144
161
  return
145
162
  env = dict(os.environ)
@@ -174,11 +191,12 @@ def main():
174
191
  _maybe_self_update(cfg) # TTL-only on priming
175
192
  _emit("SessionStart", SESSION_PRIMER)
176
193
  else:
177
- context, force_epoch, pin_version = _inject(cfg, event.get("prompt") or "")
194
+ context, force_epoch, pin_version, mcp_epoch = _inject(cfg, event.get("prompt") or "")
178
195
  # The inject call already round-tripped the server, so honour any
179
196
  # admin-pushed force-update epoch here (bypasses the daily TTL) and install
180
- # the exact server-pinned version (never @latest).
181
- _maybe_self_update(cfg, force_epoch, pin_version)
197
+ # the exact server-pinned version (never @latest). A newer MCP manifest
198
+ # epoch likewise forces an immediate re-provision of the fan-out servers.
199
+ _maybe_self_update(cfg, force_epoch, pin_version, mcp_epoch)
182
200
  _emit("UserPromptSubmit", context)
183
201
 
184
202
 
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env python3
2
+ """Genius presence heartbeat (amarre v2, Phase 2) — CONSERVATIVE.
3
+
4
+ Emits one presence ping (~every PING_INTERVAL) to the Genius capture/presence endpoint
5
+ ONLY when there is REAL recent activity, so it binds worked-time to the consultant's
6
+ machine clock without counting an idle-open VSCode:
7
+
8
+ ACTIVE := a workspace SOURCE file was edited in the last IDLE_WINDOW (human work)
9
+ OR the Claude transcript / agent files grew in the last IDLE_WINDOW
10
+ (a /loop or Workflow is executing — counts even with no human interaction)
11
+ IDLE := neither -> NO ping (VSCode open but nothing running is NOT counted)
12
+
13
+ Metadata only: workspace path + git branch (for project classification + retroactive
14
+ attribution) + machine-clock timestamp + tz. No transcript content. Never blocks, always
15
+ exit 0. Single-instance per machine (lockfile). Self-terminates when the Claude session
16
+ is gone (transcript dir idle > STOP_IDLE) or after MAX_LIFETIME.
17
+ """
18
+ import json
19
+ import os
20
+ import re
21
+ import subprocess
22
+ import sys
23
+ import time
24
+ import urllib.request
25
+
26
+ HOME = os.path.expanduser("~")
27
+ CFG = os.path.join(HOME, ".genius", "config.json")
28
+ LOCK = os.path.join(HOME, ".genius", "presence.lock")
29
+ STOP_FLAG = os.path.join(HOME, ".genius", "presence.stop")
30
+ PROJECTS = os.path.join(HOME, ".claude", "projects")
31
+
32
+ PING_INTERVAL = 300 # 5 min between ticks
33
+ IDLE_WINDOW = 600 # a file touched in the last 10 min => active this tick
34
+ STOP_IDLE = 2700 # Claude dir quiet 45 min => session over, exit
35
+ MAX_LIFETIME = 18 * 3600 # hard stop
36
+ SKIP_DIRS = {".git", "node_modules", "__pycache__", ".next", "dist", "build",
37
+ ".venv", "venv", ".mypy_cache", ".pytest_cache", "outbox"}
38
+
39
+
40
+ def _cfg():
41
+ try:
42
+ with open(CFG) as fh:
43
+ return json.load(fh)
44
+ except Exception:
45
+ return {}
46
+
47
+
48
+ def _recent(path, window_s, budget_s=2.0):
49
+ """True if any non-skipped file under path has mtime within window_s. Bounded by a
50
+ small wall-clock budget so a huge tree never stalls a tick. Early-returns."""
51
+ cutoff = time.time() - window_s
52
+ deadline = time.time() + budget_s
53
+ try:
54
+ for root, dirs, files in os.walk(path):
55
+ dirs[:] = [d for d in dirs if d not in SKIP_DIRS and not d.startswith(".")]
56
+ for f in files:
57
+ try:
58
+ if os.path.getmtime(os.path.join(root, f)) >= cutoff:
59
+ return True
60
+ except OSError:
61
+ pass
62
+ if time.time() > deadline:
63
+ break
64
+ except OSError:
65
+ pass
66
+ return False
67
+
68
+
69
+ def _project_dir(cwd):
70
+ """Best-effort: the ~/.claude/projects/<slug> dir for this cwd, else the most
71
+ recently-modified project dir (the active session)."""
72
+ slug = re.sub(r"[^A-Za-z0-9]+", "-", cwd)
73
+ cand = os.path.join(PROJECTS, slug)
74
+ if os.path.isdir(cand):
75
+ return cand
76
+ # fallback: most-recently-touched project dir
77
+ try:
78
+ best, best_m = None, 0
79
+ for d in os.listdir(PROJECTS):
80
+ p = os.path.join(PROJECTS, d)
81
+ if os.path.isdir(p):
82
+ m = os.path.getmtime(p)
83
+ if m > best_m:
84
+ best, best_m = p, m
85
+ return best
86
+ except OSError:
87
+ return None
88
+
89
+
90
+ def _git_branch(cwd):
91
+ try:
92
+ out = subprocess.run(["git", "-C", cwd, "rev-parse", "--abbrev-ref", "HEAD"],
93
+ capture_output=True, text=True, timeout=3)
94
+ b = (out.stdout or "").strip()
95
+ return b if b and b != "HEAD" else ""
96
+ except Exception:
97
+ return ""
98
+
99
+
100
+ def _post(cfg, payload):
101
+ base = (cfg.get("base_url") or "").rstrip("/")
102
+ token = cfg.get("token") or ""
103
+ if not base or not token:
104
+ return False
105
+ url = base + "/xma/genius/v1/capture/presence"
106
+ body = json.dumps({"jsonrpc": "2.0", "method": "call",
107
+ "params": payload, "id": 1}).encode()
108
+ req = urllib.request.Request(url, data=body, method="POST")
109
+ req.add_header("Content-Type", "application/json")
110
+ req.add_header("Authorization", "Bearer " + token)
111
+ req.add_header("User-Agent", "GeniusPresence/1.0") # WAF blocks default Python-urllib UA
112
+ try:
113
+ with urllib.request.urlopen(req, timeout=15) as resp:
114
+ json.loads(resp.read().decode("utf-8", "replace") or "{}")
115
+ return True
116
+ except Exception:
117
+ return False
118
+
119
+
120
+ def _work_mode(cwd, claude_dir):
121
+ """Label HOW the work is happening (owner: loop/workflow vs short interactions).
122
+ A workspace edit = the human is present -> interactive. Otherwise, if a Workflow/
123
+ subagent run is active -> autonomous (Claude working alone, e.g. overnight). Default
124
+ interactive (a normal Claude turn)."""
125
+ if _recent(cwd, IDLE_WINDOW):
126
+ return "interactive"
127
+ if claude_dir:
128
+ for sub in ("subagents", os.path.join("subagents", "workflows")):
129
+ if _recent(os.path.join(claude_dir, sub), IDLE_WINDOW, budget_s=1.5):
130
+ return "autonomous"
131
+ return "interactive"
132
+
133
+
134
+ def _ping(cfg, cwd, session_ref, claude_dir):
135
+ payload = {
136
+ "session_ref": session_ref,
137
+ "occurred_at_epoch": time.time(),
138
+ "utc_offset_minutes": -int(time.timezone // 60) if not time.daylight
139
+ else -int(time.altzone // 60),
140
+ "workspace": cwd[:256],
141
+ "git_branch": _git_branch(cwd)[:128],
142
+ "surface": "vscode",
143
+ "work_mode": _work_mode(cwd, claude_dir),
144
+ }
145
+ return _post(cfg, payload)
146
+
147
+
148
+ def _is_active(cwd, claude_dir):
149
+ """Conservative: real work signal in the last IDLE_WINDOW — human edit OR Claude
150
+ execution (loop/workflow). Idle-open VSCode touches no file -> not active."""
151
+ if _recent(cwd, IDLE_WINDOW):
152
+ return True
153
+ if claude_dir and _recent(claude_dir, IDLE_WINDOW):
154
+ return True
155
+ return False
156
+
157
+
158
+ def _claude_idle_too_long(claude_dir):
159
+ return not (claude_dir and _recent(claude_dir, STOP_IDLE, budget_s=2.0))
160
+
161
+
162
+ def main():
163
+ args = sys.argv[1:]
164
+ once = "--once" in args
165
+ cwd = os.getcwd()
166
+ session_ref = "vscode:%s" % time.strftime("%Y-%m-%d")
167
+ for i, a in enumerate(args):
168
+ if a == "--cwd" and i + 1 < len(args):
169
+ cwd = args[i + 1]
170
+ if a == "--session" and i + 1 < len(args):
171
+ session_ref = args[i + 1]
172
+ cfg = _cfg()
173
+ claude_dir = _project_dir(cwd)
174
+
175
+ if once:
176
+ active = _is_active(cwd, claude_dir)
177
+ ok = _ping(cfg, cwd, session_ref, claude_dir) if active else False
178
+ print(json.dumps({"active": active, "posted": ok, "cwd": cwd,
179
+ "claude_dir": claude_dir, "session_ref": session_ref}))
180
+ return
181
+
182
+ # single-instance lock
183
+ try:
184
+ if os.path.exists(LOCK):
185
+ with open(LOCK) as fh:
186
+ pid = int((fh.read() or "0").strip() or 0)
187
+ if pid:
188
+ try:
189
+ os.kill(pid, 0) # alive?
190
+ return # another daemon owns it
191
+ except OSError:
192
+ pass
193
+ with open(LOCK, "w") as fh:
194
+ fh.write(str(os.getpid()))
195
+ except Exception:
196
+ pass
197
+
198
+ start = time.time()
199
+ try:
200
+ while True:
201
+ if os.path.exists(STOP_FLAG):
202
+ break
203
+ if time.time() - start > MAX_LIFETIME:
204
+ break
205
+ claude_dir = _project_dir(cwd) # refresh (slug may resolve later)
206
+ if _is_active(cwd, claude_dir):
207
+ _ping(cfg, cwd, session_ref, claude_dir)
208
+ elif _claude_idle_too_long(claude_dir):
209
+ break # session truly over
210
+ time.sleep(PING_INTERVAL)
211
+ finally:
212
+ try:
213
+ if os.path.exists(LOCK):
214
+ os.remove(LOCK)
215
+ except OSError:
216
+ pass
217
+
218
+
219
+ if __name__ == "__main__":
220
+ try:
221
+ main()
222
+ except Exception:
223
+ pass
224
+ sys.exit(0)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xmarts/genius-setup",
3
- "version": "1.8.0",
4
- "description": "One-command, self-updating onboarding for Xmarts Genius (Brain MCP + per-turn injection + automatic session/time capture + plan/committee→Brain enforcement + CLAUDE.md protocol). Secret-free onboarding via single-use setup-token exchange (no Bearer in the GET-able bootstrap). Installs once; self-updates daily so new tools/skills/features arrive with zero action.",
3
+ "version": "1.10.0",
4
+ "description": "One-command, self-updating onboarding for Xmarts Genius (Brain MCP + per-turn injection + automatic session/time capture + presence heartbeat for honest time-tracking 'amarre' + plan/committee→Brain enforcement + CLAUDE.md protocol + MCP fan-out: provisions the consultant's granted MCP servers into Claude Code & Claude Desktop). Secret-free onboarding via single-use setup-token exchange. Installs once; self-updates daily so new tools/skills/features arrive with zero action.",
5
5
  "bin": {
6
6
  "genius-setup": "bin/genius-setup.js"
7
7
  },
@@ -10,6 +10,7 @@
10
10
  "hooks/genius_inject.py",
11
11
  "hooks/genius_capture.py",
12
12
  "hooks/plan_committee_guard.py",
13
+ "hooks/genius_presence.py",
13
14
  "CLAUDE_PROTOCOL.md",
14
15
  "README.md"
15
16
  ],