@xmarts/genius-setup 1.2.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.
- package/README.md +62 -0
- package/bin/genius-setup.js +129 -0
- package/hooks/genius_capture.py +216 -0
- package/hooks/genius_inject.py +98 -0
- package/package.json +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Xmarts Genius — Frictionless Setup
|
|
2
|
+
|
|
3
|
+
Three ways to get a consultant onto the Genius Brain with **automatic learning
|
|
4
|
+
capture**. Pick one; they all converge on the same `~/.genius/config.json` +
|
|
5
|
+
Claude Code hooks, and all are idempotent.
|
|
6
|
+
|
|
7
|
+
The capture guarantee is **defense-in-depth** and does NOT depend on any single
|
|
8
|
+
piece:
|
|
9
|
+
|
|
10
|
+
- **L0/L2 — server-side (always on):** every MCP tool call is recorded, and the
|
|
11
|
+
Brain miner (`gemini-2.5-flash` LLM-as-judge, every 2h) extracts the learnings
|
|
12
|
+
the agent forgot — even if the consultant never installs anything beyond the
|
|
13
|
+
MCP connection.
|
|
14
|
+
- **L3 — deterministic hook (richer):** the `SessionEnd` / `PreCompact` / `Stop`
|
|
15
|
+
hook POSTs the transcript to `/xma/genius/v1/capture/transcript`. Hooks
|
|
16
|
+
guarantee *command execution* where directives don't.
|
|
17
|
+
- **L4 — durable spool:** the hook writes to `~/.genius/outbox/` first, then
|
|
18
|
+
delivers; a central outage never loses a capture. The server side mirrors this
|
|
19
|
+
with `xma.capture.inbox` (persist-then-process + bounded retry).
|
|
20
|
+
|
|
21
|
+
Everything collapses to **one** learning row via the content-hash idempotency
|
|
22
|
+
sink, so all layers can fire at-least-once with exactly-once effect.
|
|
23
|
+
|
|
24
|
+
## Option A — npx (any OS)
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx -y @xmarts/genius-setup --token <YOUR_BEARER> --base-url https://beesmart.digital
|
|
28
|
+
# optional: --client "Acme Corp" (default scope for this machine)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Restart Claude Code. Done.
|
|
32
|
+
|
|
33
|
+
## Option B — Claude Code plugin
|
|
34
|
+
|
|
35
|
+
Install the `xmarts-genius` plugin from the Genius marketplace. It bundles the
|
|
36
|
+
hooks + slash commands (`/genius-status`, `/genius-capture`) and auto-updates.
|
|
37
|
+
You still set the MCP Bearer once (the plugin reads `~/.genius/config.json` for
|
|
38
|
+
the capture endpoint + token — run Option A once, or `/genius-status` guides you).
|
|
39
|
+
|
|
40
|
+
## Option C — VS Code panel
|
|
41
|
+
|
|
42
|
+
Install the **Xmarts Genius** extension. Click **Genius: Set up**, paste your
|
|
43
|
+
Bearer token once (stored in SecretStorage), and it runs the same bootstrap. The
|
|
44
|
+
panel shows Brain connection, capture endpoint, default client, and outbox
|
|
45
|
+
backlog. It is **UI only** — never the capture mechanism.
|
|
46
|
+
|
|
47
|
+
## Where the token comes from
|
|
48
|
+
|
|
49
|
+
Each consultant's Bearer is their MCP API key, generated from their
|
|
50
|
+
`xma.consultant` ficha in Genius ("Generate Claude Code Setup"). No new secret.
|
|
51
|
+
|
|
52
|
+
## Files this writes
|
|
53
|
+
|
|
54
|
+
| Path | Purpose |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `~/.genius/config.json` | endpoint + token (+ optional default client) |
|
|
57
|
+
| `~/.genius/hooks/genius_capture.py` | the deterministic capture hook |
|
|
58
|
+
| `~/.genius/outbox/` | durable local spool (auto-flushed) |
|
|
59
|
+
| `~/.claude.json` → `mcpServers.beesmart_genius` | the Brain MCP |
|
|
60
|
+
| `~/.claude/settings.json` → `hooks` | SessionEnd / PreCompact / Stop |
|
|
61
|
+
|
|
62
|
+
All edits are merged (never clobbered) with a timestamped backup.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @xmarts/genius-setup — one-command frictionless onboarding for Xmarts Genius.
|
|
4
|
+
*
|
|
5
|
+
* npx @xmarts/genius-setup --token <bearer> [--base-url https://beesmart.digital] [--client "Acme"]
|
|
6
|
+
*
|
|
7
|
+
* Idempotent + cross-platform (incl. Windows). It:
|
|
8
|
+
* 1. writes ~/.genius/config.json (endpoints + token) and installs the per-turn
|
|
9
|
+
* inject hook (hooks/genius_inject.py) + the session-capture hook
|
|
10
|
+
* (hooks/genius_capture.py);
|
|
11
|
+
* 2. registers the beesmart_genius MCP server in ~/.claude.json;
|
|
12
|
+
* 3. wires per-turn injection (UserPromptSubmit) + SessionStart priming +
|
|
13
|
+
* SessionEnd/PreCompact/Stop capture into ~/.claude/settings.json — merged,
|
|
14
|
+
* never duplicated, with a timestamped backup first.
|
|
15
|
+
* After this, the consultant has Brain + capture + feedback flowing with zero
|
|
16
|
+
* further prompts. The token is the consultant's MCP Bearer (no new secret).
|
|
17
|
+
*/
|
|
18
|
+
'use strict';
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
|
|
23
|
+
function arg(name, def) {
|
|
24
|
+
const i = process.argv.indexOf('--' + name);
|
|
25
|
+
return i > -1 && process.argv[i + 1] ? process.argv[i + 1] : def;
|
|
26
|
+
}
|
|
27
|
+
function log(msg) { process.stdout.write(msg + '\n'); }
|
|
28
|
+
|
|
29
|
+
// (#pkg-4) Prefer GENIUS_TOKEN env over --token argv: argv leaks into the
|
|
30
|
+
// process list and shell history. Warn if a token is passed on the CLI.
|
|
31
|
+
let TOKEN = process.env.GENIUS_TOKEN || '';
|
|
32
|
+
if (!TOKEN) {
|
|
33
|
+
TOKEN = arg('token', '');
|
|
34
|
+
if (TOKEN) log('WARN: passing --token on the CLI leaks it to the process list / shell history. Prefer GENIUS_TOKEN=... npx @xmarts/genius-setup');
|
|
35
|
+
}
|
|
36
|
+
const BASE = (arg('base-url', 'https://beesmart.digital')).replace(/\/+$/, '');
|
|
37
|
+
const CLIENT = arg('client', '');
|
|
38
|
+
if (!TOKEN) {
|
|
39
|
+
log('ERROR: a consultant Bearer token is required. Set GENIUS_TOKEN=<token> (preferred) or pass --token <token>.');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const HOME = os.homedir();
|
|
44
|
+
const GENIUS_DIR = path.join(HOME, '.genius');
|
|
45
|
+
const HOOKS_DIR = path.join(GENIUS_DIR, 'hooks');
|
|
46
|
+
const CLAUDE_DIR = path.join(HOME, '.claude');
|
|
47
|
+
|
|
48
|
+
function ensureDir(d) { fs.mkdirSync(d, { recursive: true }); }
|
|
49
|
+
function readJson(p) { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return {}; } }
|
|
50
|
+
function backup(p) {
|
|
51
|
+
if (fs.existsSync(p)) {
|
|
52
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
53
|
+
fs.copyFileSync(p, p + '.genius-bak-' + stamp);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function writeJson(p, obj, secret) {
|
|
57
|
+
// (#pkg-3) secret files (token-bearing) are written 0600, never world-readable.
|
|
58
|
+
ensureDir(path.dirname(p));
|
|
59
|
+
fs.writeFileSync(p, JSON.stringify(obj, null, 2), secret ? { mode: 0o600 } : undefined);
|
|
60
|
+
if (secret) { try { fs.chmodSync(p, 0o600); } catch (_) {} }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 1) ~/.genius/config.json + hook
|
|
64
|
+
ensureDir(HOOKS_DIR);
|
|
65
|
+
const cfg = {
|
|
66
|
+
base_url: BASE,
|
|
67
|
+
token: TOKEN,
|
|
68
|
+
capture_endpoint: BASE + '/xma/genius/v1/capture/transcript',
|
|
69
|
+
inject_endpoint: BASE + '/xma/genius/v1/inject',
|
|
70
|
+
};
|
|
71
|
+
if (CLIENT) cfg.target_client = CLIENT;
|
|
72
|
+
writeJson(path.join(GENIUS_DIR, 'config.json'), cfg, true); // secret: 0600
|
|
73
|
+
|
|
74
|
+
// Copy the hooks shipped alongside this script (capture + per-turn inject).
|
|
75
|
+
function copyHook(fname) {
|
|
76
|
+
const src = path.join(__dirname, '..', 'hooks', fname);
|
|
77
|
+
const dst = path.join(HOOKS_DIR, fname);
|
|
78
|
+
try {
|
|
79
|
+
fs.copyFileSync(src, dst);
|
|
80
|
+
try { fs.chmodSync(dst, 0o755); } catch (_) {}
|
|
81
|
+
} catch (e) {
|
|
82
|
+
log('WARN: could not copy hook from ' + src + ' (' + e.message + ')');
|
|
83
|
+
}
|
|
84
|
+
return dst;
|
|
85
|
+
}
|
|
86
|
+
const hookDst = copyHook('genius_capture.py');
|
|
87
|
+
const injectDst = copyHook('genius_inject.py');
|
|
88
|
+
|
|
89
|
+
// 2) ~/.claude.json mcpServers (merge, no clobber)
|
|
90
|
+
const claudeJsonPath = path.join(HOME, '.claude.json');
|
|
91
|
+
const claudeJson = readJson(claudeJsonPath);
|
|
92
|
+
backup(claudeJsonPath);
|
|
93
|
+
claudeJson.mcpServers = claudeJson.mcpServers || {};
|
|
94
|
+
claudeJson.mcpServers['beesmart_genius'] = {
|
|
95
|
+
type: 'http',
|
|
96
|
+
url: BASE + '/mcp/consultant',
|
|
97
|
+
headers: { Authorization: 'Bearer ' + TOKEN },
|
|
98
|
+
};
|
|
99
|
+
writeJson(claudeJsonPath, claudeJson, true); // contains the Bearer -> 0600
|
|
100
|
+
|
|
101
|
+
// 3) ~/.claude/settings.json hooks (merge by command, no dup)
|
|
102
|
+
const settingsPath = path.join(CLAUDE_DIR, 'settings.json');
|
|
103
|
+
const settings = readJson(settingsPath);
|
|
104
|
+
backup(settingsPath);
|
|
105
|
+
settings.hooks = settings.hooks || {};
|
|
106
|
+
function addHook(eventName, scriptPath) {
|
|
107
|
+
settings.hooks[eventName] = settings.hooks[eventName] || [];
|
|
108
|
+
const marker = path.basename(scriptPath);
|
|
109
|
+
const already = JSON.stringify(settings.hooks[eventName]).includes(marker);
|
|
110
|
+
if (!already) {
|
|
111
|
+
const cmd = `python3 ${JSON.stringify(scriptPath)} || true`;
|
|
112
|
+
settings.hooks[eventName].push({ hooks: [{ type: 'command', command: cmd }] });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Capture (transcript -> Brain) on session boundaries.
|
|
116
|
+
['SessionEnd', 'PreCompact', 'Stop'].forEach((e) => addHook(e, hookDst));
|
|
117
|
+
// Per-turn knowledge injection + session priming (consultant_access guarantee).
|
|
118
|
+
['UserPromptSubmit', 'SessionStart'].forEach((e) => addHook(e, injectDst));
|
|
119
|
+
writeJson(settingsPath, settings);
|
|
120
|
+
|
|
121
|
+
log('');
|
|
122
|
+
log(' Xmarts Genius is set up.');
|
|
123
|
+
log(' - MCP server : beesmart_genius -> ' + BASE + '/mcp/consultant');
|
|
124
|
+
log(' - Capture hook : ' + hookDst + ' (SessionEnd / PreCompact / Stop)');
|
|
125
|
+
log(' - Inject hook : ' + injectDst + ' (UserPromptSubmit / SessionStart)');
|
|
126
|
+
log(' - Config : ' + path.join(GENIUS_DIR, 'config.json'));
|
|
127
|
+
if (CLIENT) log(' - Default client : ' + CLIENT);
|
|
128
|
+
log('');
|
|
129
|
+
log(' Restart Claude Code. Per-turn Brain injection + automatic capture are now active — no further prompts.');
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Genius deterministic session-capture hook (P3).
|
|
3
|
+
|
|
4
|
+
Wired to Claude Code SessionEnd (+ PreCompact + Stop, guarded). The hook is the
|
|
5
|
+
COMMAND-EXECUTION guarantee: directives the model may ignore, a hook always runs.
|
|
6
|
+
It reads the session transcript and POSTs it to the Genius capture endpoint, so
|
|
7
|
+
the server-side miner extracts the learnings the agent forgot to share — with
|
|
8
|
+
zero consultant action.
|
|
9
|
+
|
|
10
|
+
Hard rules:
|
|
11
|
+
- ALWAYS exit 0 (never block or fail a session, even on network/parse errors).
|
|
12
|
+
- Persist-then-network: every payload is first written to a local outbox spool;
|
|
13
|
+
on a successful POST it is removed, otherwise it stays and is retried on the
|
|
14
|
+
next hook firing. A central outage therefore never loses a capture.
|
|
15
|
+
- Reads its config (endpoint + bearer token) from ~/.genius/config.json, written
|
|
16
|
+
once by the installer. No secrets are embedded here.
|
|
17
|
+
|
|
18
|
+
Input: Claude Code passes a JSON event on stdin including `transcript_path` and
|
|
19
|
+
`session_id`. We tolerate missing fields.
|
|
20
|
+
"""
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import re
|
|
24
|
+
import sys
|
|
25
|
+
import time
|
|
26
|
+
import urllib.request
|
|
27
|
+
|
|
28
|
+
HOME = os.path.expanduser("~")
|
|
29
|
+
CFG_PATH = os.path.join(HOME, ".genius", "config.json")
|
|
30
|
+
OUTBOX_DIR = os.path.join(HOME, ".genius", "outbox")
|
|
31
|
+
DEADLETTER_DIR = os.path.join(OUTBOX_DIR, "failed")
|
|
32
|
+
MAX_OUTBOX_FILES = 200
|
|
33
|
+
MAX_TRANSCRIPT_BYTES = 200_000
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _load_cfg():
|
|
37
|
+
try:
|
|
38
|
+
with open(CFG_PATH) as fh:
|
|
39
|
+
return json.load(fh)
|
|
40
|
+
except Exception:
|
|
41
|
+
return {}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _read_stdin_event():
|
|
45
|
+
try:
|
|
46
|
+
raw = sys.stdin.read()
|
|
47
|
+
return json.loads(raw) if raw.strip() else {}
|
|
48
|
+
except Exception:
|
|
49
|
+
return {}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _read_transcript(path):
|
|
53
|
+
if not path or not os.path.isfile(path):
|
|
54
|
+
return ""
|
|
55
|
+
try:
|
|
56
|
+
with open(path, "rb") as fh:
|
|
57
|
+
try:
|
|
58
|
+
fh.seek(-MAX_TRANSCRIPT_BYTES, os.SEEK_END)
|
|
59
|
+
except OSError:
|
|
60
|
+
fh.seek(0)
|
|
61
|
+
return fh.read().decode("utf-8", "replace")
|
|
62
|
+
except Exception:
|
|
63
|
+
return ""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
_CRED_PATTERNS = [
|
|
67
|
+
re.compile(r'-----BEGIN[A-Z ]*PRIVATE KEY-----.*?-----END[A-Z ]*PRIVATE KEY-----', re.DOTALL),
|
|
68
|
+
re.compile(r'(?i)\bBearer\s+[A-Za-z0-9._\-]{20,}'),
|
|
69
|
+
re.compile(r'(?i)postgres(?:ql)?://[^:\s/]+:[^@\s]+@'),
|
|
70
|
+
re.compile(r'(?i)(?:^|[\s_=:\'"])(password|passwd|pwd|secret|api[_-]?key|'
|
|
71
|
+
r'access[_-]?token|auth[_-]?token|fernet|secret[_-]?key|'
|
|
72
|
+
r'db[_-]?password|admin[_-]?passwd|client[_-]?secret)'
|
|
73
|
+
r'\s*[:=]\s*[\'"]?[^\s\'"]{6,}'),
|
|
74
|
+
re.compile(r'\b[A-Za-z0-9_\-]{43}=(?![A-Za-z0-9_\-=])'),
|
|
75
|
+
re.compile(r'\bAKIA[0-9A-Z]{16}\b'),
|
|
76
|
+
re.compile(r'\bAIza[0-9A-Za-z_\-]{35}\b'),
|
|
77
|
+
re.compile(r'\beyJ[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}'),
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _redact(text):
|
|
82
|
+
"""Client-side defense-in-depth: mask obvious secrets BEFORE they leave the
|
|
83
|
+
machine. The server re-redacts authoritatively at inbox enqueue (APS-1); this
|
|
84
|
+
just means a credential never even transits the network."""
|
|
85
|
+
if not text:
|
|
86
|
+
return text
|
|
87
|
+
for pat in _CRED_PATTERNS:
|
|
88
|
+
text = pat.sub("[REDACTED]", text)
|
|
89
|
+
return text
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _spool_files():
|
|
93
|
+
if not os.path.isdir(OUTBOX_DIR):
|
|
94
|
+
return []
|
|
95
|
+
return sorted(f for f in os.listdir(OUTBOX_DIR) if f.endswith(".json"))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _spool(payload):
|
|
99
|
+
"""Persist a payload. (#pkg-2) One file per session_ref — repeated Stop
|
|
100
|
+
firings of the SAME session OVERWRITE one file instead of creating N — and a
|
|
101
|
+
true on-disk RING BUFFER cap, dropping the oldest when at MAX_OUTBOX_FILES,
|
|
102
|
+
so the directory can never grow without bound."""
|
|
103
|
+
os.makedirs(OUTBOX_DIR, exist_ok=True)
|
|
104
|
+
sref = (payload.get("session_ref") or "s")[:40].replace("/", "_").replace(".", "_")
|
|
105
|
+
# session dedup: a stable name per session -> overwrite, not append
|
|
106
|
+
name = "sess_%s.json" % sref if sref != "s" else "%d_anon.json" % int(time.time() * 1000)
|
|
107
|
+
path = os.path.join(OUTBOX_DIR, name)
|
|
108
|
+
if not os.path.exists(path):
|
|
109
|
+
existing = _spool_files()
|
|
110
|
+
if len(existing) >= MAX_OUTBOX_FILES:
|
|
111
|
+
for old in existing[:len(existing) - MAX_OUTBOX_FILES + 1]:
|
|
112
|
+
try:
|
|
113
|
+
os.remove(os.path.join(OUTBOX_DIR, old))
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
try:
|
|
117
|
+
with open(path, "w") as fh:
|
|
118
|
+
json.dump(payload, fh)
|
|
119
|
+
except Exception:
|
|
120
|
+
return None
|
|
121
|
+
return path
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _post(endpoint, token, payload, timeout=15):
|
|
125
|
+
body = json.dumps({"jsonrpc": "2.0", "method": "call",
|
|
126
|
+
"params": payload, "id": 1}).encode()
|
|
127
|
+
req = urllib.request.Request(endpoint, data=body)
|
|
128
|
+
req.add_header("Content-Type", "application/json")
|
|
129
|
+
req.add_header("Authorization", "Bearer %s" % token)
|
|
130
|
+
# Cloudflare in front of beesmart.digital 403s the default Python-urllib UA.
|
|
131
|
+
req.add_header("User-Agent", "XmartsGenius-Capture/1.0")
|
|
132
|
+
resp = urllib.request.urlopen(req, timeout=timeout)
|
|
133
|
+
data = json.loads(resp.read())
|
|
134
|
+
result = data.get("result", data)
|
|
135
|
+
if isinstance(result, dict) and result.get("status") == "error":
|
|
136
|
+
# server-side rejection of THIS payload -> permanent
|
|
137
|
+
raise PermanentError(result.get("message", "capture error"))
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class PermanentError(Exception):
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _flush_outbox(endpoint, token):
|
|
146
|
+
"""Deliver spooled payloads oldest-first. (#pkg-1) A permanent rejection
|
|
147
|
+
(4xx other than 408/429, or a JSON-RPC error envelope) is dead-lettered to
|
|
148
|
+
outbox/failed/ and we CONTINUE, so one poison payload can never wedge the
|
|
149
|
+
whole queue; a transient error (offline / 5xx / timeout) stops the pass and
|
|
150
|
+
is retried next firing."""
|
|
151
|
+
import urllib.error
|
|
152
|
+
for name in _spool_files()[:MAX_OUTBOX_FILES]:
|
|
153
|
+
path = os.path.join(OUTBOX_DIR, name)
|
|
154
|
+
try:
|
|
155
|
+
with open(path) as fh:
|
|
156
|
+
payload = json.load(fh)
|
|
157
|
+
_post(endpoint, token, payload)
|
|
158
|
+
os.remove(path) # delivered
|
|
159
|
+
except PermanentError:
|
|
160
|
+
_deadletter(path) # bad payload -> set aside, keep going
|
|
161
|
+
continue
|
|
162
|
+
except urllib.error.HTTPError as he:
|
|
163
|
+
if he.code in (408, 429) or he.code >= 500:
|
|
164
|
+
break # transient server state -> retry later
|
|
165
|
+
_deadletter(path) # 4xx (e.g. revoked token, 413) -> permanent
|
|
166
|
+
continue
|
|
167
|
+
except Exception:
|
|
168
|
+
break # offline / timeout -> retry next firing
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _deadletter(path):
|
|
172
|
+
try:
|
|
173
|
+
os.makedirs(DEADLETTER_DIR, exist_ok=True)
|
|
174
|
+
os.replace(path, os.path.join(DEADLETTER_DIR, os.path.basename(path)))
|
|
175
|
+
except Exception:
|
|
176
|
+
try:
|
|
177
|
+
os.remove(path)
|
|
178
|
+
except Exception:
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def main():
|
|
183
|
+
cfg = _load_cfg()
|
|
184
|
+
endpoint = cfg.get("capture_endpoint")
|
|
185
|
+
token = cfg.get("token")
|
|
186
|
+
if not endpoint or not token:
|
|
187
|
+
return # not configured -> silent no-op
|
|
188
|
+
|
|
189
|
+
event = _read_stdin_event()
|
|
190
|
+
transcript = _redact(_read_transcript(event.get("transcript_path")))
|
|
191
|
+
session_ref = event.get("session_id") or event.get("session_ref") or ""
|
|
192
|
+
|
|
193
|
+
if transcript.strip():
|
|
194
|
+
payload = {
|
|
195
|
+
"transcript": transcript,
|
|
196
|
+
"session_ref": session_ref,
|
|
197
|
+
"source": "session_end_hook",
|
|
198
|
+
}
|
|
199
|
+
if cfg.get("target_client"):
|
|
200
|
+
payload["target_client"] = cfg["target_client"]
|
|
201
|
+
# persist first (durability), then try to deliver
|
|
202
|
+
_spool(payload)
|
|
203
|
+
|
|
204
|
+
# Always attempt to flush the spool (delivers this + any backlog)
|
|
205
|
+
try:
|
|
206
|
+
_flush_outbox(endpoint, token)
|
|
207
|
+
except Exception:
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
if __name__ == "__main__":
|
|
212
|
+
try:
|
|
213
|
+
main()
|
|
214
|
+
except Exception:
|
|
215
|
+
pass
|
|
216
|
+
sys.exit(0) # NEVER block a session
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Genius per-turn knowledge injection hook (P0 — consultant_access).
|
|
3
|
+
|
|
4
|
+
Wired to Claude Code UserPromptSubmit (every turn) and SessionStart (priming).
|
|
5
|
+
On UserPromptSubmit it asks the consultant-scoped Genius Brain for the top-k
|
|
6
|
+
learnings relevant to the current prompt and emits them as
|
|
7
|
+
`hookSpecificOutput.additionalContext`, so the consultant ALWAYS has the right
|
|
8
|
+
prior knowledge WITHOUT asking. Editor-agnostic (terminal / VSCode / headless).
|
|
9
|
+
|
|
10
|
+
Hard rules:
|
|
11
|
+
- ALWAYS exit 0 (never block a turn).
|
|
12
|
+
- Time-box the network call (<=2.5s) so injection adds no perceptible latency.
|
|
13
|
+
- Relevance-floored: nothing relevant -> inject nothing (zero noise).
|
|
14
|
+
- Reads endpoint + bearer from ~/.genius/config.json (written by the installer);
|
|
15
|
+
no secret is embedded here.
|
|
16
|
+
"""
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
import urllib.request
|
|
21
|
+
|
|
22
|
+
HOME = os.path.expanduser("~")
|
|
23
|
+
CFG_PATH = os.path.join(HOME, ".genius", "config.json")
|
|
24
|
+
TIMEOUT = 3.5
|
|
25
|
+
|
|
26
|
+
SESSION_PRIMER = (
|
|
27
|
+
"[Xmarts Genius — Brain connected]\n"
|
|
28
|
+
"The Genius Brain (beesmart_genius MCP) is active for this session. Relevant "
|
|
29
|
+
"prior learnings are injected automatically on each prompt — you do not need "
|
|
30
|
+
"to ask for them. New learnings you produce are captured automatically at "
|
|
31
|
+
"session end. You may still call query_knowledge for a deeper search and "
|
|
32
|
+
"get_my_profile for your profile."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _cfg():
|
|
37
|
+
try:
|
|
38
|
+
with open(CFG_PATH) as fh:
|
|
39
|
+
return json.load(fh)
|
|
40
|
+
except Exception:
|
|
41
|
+
return {}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _event():
|
|
45
|
+
try:
|
|
46
|
+
raw = sys.stdin.read()
|
|
47
|
+
return json.loads(raw) if raw.strip() else {}
|
|
48
|
+
except Exception:
|
|
49
|
+
return {}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _emit(event_name, context):
|
|
53
|
+
if context:
|
|
54
|
+
print(json.dumps({"hookSpecificOutput": {
|
|
55
|
+
"hookEventName": event_name, "additionalContext": context}}))
|
|
56
|
+
sys.exit(0)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _inject(cfg, prompt):
|
|
60
|
+
base = (cfg.get("base_url") or "https://beesmart.digital").rstrip("/")
|
|
61
|
+
token = cfg.get("token")
|
|
62
|
+
if not token or not prompt.strip():
|
|
63
|
+
return ""
|
|
64
|
+
body = json.dumps({"jsonrpc": "2.0", "method": "call", "id": 1,
|
|
65
|
+
"params": {"prompt": prompt[:4000], "limit": 4}}).encode()
|
|
66
|
+
req = urllib.request.Request(base + "/xma/genius/v1/inject", data=body)
|
|
67
|
+
req.add_header("Content-Type", "application/json")
|
|
68
|
+
req.add_header("Authorization", "Bearer %s" % token)
|
|
69
|
+
# Cloudflare in front of beesmart.digital 403s the default Python-urllib UA.
|
|
70
|
+
req.add_header("User-Agent", "XmartsGenius-Inject/1.0")
|
|
71
|
+
try:
|
|
72
|
+
resp = urllib.request.urlopen(req, timeout=TIMEOUT)
|
|
73
|
+
data = json.loads(resp.read())
|
|
74
|
+
result = data.get("result", data)
|
|
75
|
+
return (result or {}).get("context", "") or ""
|
|
76
|
+
except Exception:
|
|
77
|
+
return ""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def main():
|
|
81
|
+
cfg = _cfg()
|
|
82
|
+
if not cfg.get("token"):
|
|
83
|
+
return # not configured -> silent no-op
|
|
84
|
+
event = _event()
|
|
85
|
+
name = event.get("hook_event_name") or (
|
|
86
|
+
"UserPromptSubmit" if event.get("prompt") else "SessionStart")
|
|
87
|
+
if name == "SessionStart":
|
|
88
|
+
_emit("SessionStart", SESSION_PRIMER)
|
|
89
|
+
else:
|
|
90
|
+
_emit("UserPromptSubmit", _inject(cfg, event.get("prompt") or ""))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
try:
|
|
95
|
+
main()
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
sys.exit(0) # NEVER block a turn
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xmarts/genius-setup",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "One-command frictionless onboarding for Xmarts Genius (Brain MCP + per-turn injection + automatic session capture).",
|
|
5
|
+
"bin": {
|
|
6
|
+
"genius-setup": "bin/genius-setup.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/genius-setup.js",
|
|
10
|
+
"hooks/genius_inject.py",
|
|
11
|
+
"hooks/genius_capture.py",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=16"
|
|
16
|
+
},
|
|
17
|
+
"license": "UNLICENSED",
|
|
18
|
+
"author": "Xmarts",
|
|
19
|
+
"private": false
|
|
20
|
+
}
|