nexo-brain 0.2.1 → 0.3.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/README.md +19 -7
- package/bin/nexo-brain 2.js +610 -0
- package/package.json +2 -2
- package/scripts/pre-commit-check 2.sh +55 -0
- package/src/cognitive.py +1549 -56
- package/src/db.py +49 -25
- package/src/hooks/auto_capture.py +208 -0
- package/src/plugins/cognitive_memory.py +276 -17
- package/src/scripts/nexo-catchup.py +32 -15
- package/src/scripts/nexo-cognitive-decay.py +2 -4
- package/src/scripts/nexo-daily-self-audit.py +148 -29
- package/src/scripts/nexo-immune.py +869 -0
- package/src/scripts/nexo-postmortem-consolidator.py +42 -40
- package/src/scripts/nexo-sleep.py +90 -39
- package/src/scripts/nexo-synthesis.py +78 -76
- package/src/tools_sessions.py +2 -2
- package/templates/CLAUDE.md 2.template +89 -0
- package/templates/CLAUDE.md.template +1 -1
package/README.md
CHANGED
|
@@ -162,7 +162,7 @@ That's it. No need to run `claude` manually. Atlas will greet you immediately
|
|
|
162
162
|
| Component | What | Where |
|
|
163
163
|
|-----------|------|-------|
|
|
164
164
|
| Cognitive engine | Python: fastembed, numpy, vector search | pip packages |
|
|
165
|
-
| MCP server |
|
|
165
|
+
| MCP server | 77 tools for memory, learning, guard | ~/.nexo/ |
|
|
166
166
|
| Plugins | Guard, episodic memory, cognitive memory, entities, preferences | ~/.nexo/plugins/ |
|
|
167
167
|
| Hooks | Session capture, briefing, stop detection | ~/.nexo/hooks/ |
|
|
168
168
|
| LaunchAgents | Decay, sleep, audit, postmortem, catch-up | ~/Library/LaunchAgents/ |
|
|
@@ -173,19 +173,19 @@ That's it. No need to run `claude` manually. Atlas will greet you immediately
|
|
|
173
173
|
|
|
174
174
|
- **macOS** (Linux support planned)
|
|
175
175
|
- **Node.js 18+** (for the installer)
|
|
176
|
-
- **Claude Opus (latest version) strongly recommended.** NEXO provides
|
|
176
|
+
- **Claude Opus (latest version) strongly recommended.** NEXO provides 77 MCP tools across 16 categories. This cognitive load requires a top-tier model with large context window. Smaller models (Haiku, Sonnet) may struggle with tool selection and produce inconsistent results. Opus handles all 77 tools without hesitation.
|
|
177
177
|
- Python 3, Homebrew, and Claude Code are installed automatically if missing.
|
|
178
178
|
|
|
179
179
|
## Architecture
|
|
180
180
|
|
|
181
|
-
###
|
|
181
|
+
### 77 MCP Tools across 16 Categories
|
|
182
182
|
|
|
183
183
|
| Category | Count | Tools | Purpose |
|
|
184
184
|
|----------|-------|-------|---------|
|
|
185
185
|
| Cognitive | 8 | retrieve, stats, inspect, metrics, dissonance, resolve, sentiment, trust | The brain — memory, RAG, trust, mood |
|
|
186
186
|
| Guard | 3 | check, stats, log_repetition | Metacognitive error prevention |
|
|
187
187
|
| Episodic | 10 | change_log/search/commit, decision_log/outcome/search, review_queue, diary_write/read, recall | What happened and why |
|
|
188
|
-
| Sessions |
|
|
188
|
+
| Sessions | 4 | startup, heartbeat, stop, status | Session lifecycle + context shift detection |
|
|
189
189
|
| Coordination | 7 | track, untrack, files, send, ask, answer, check_answer | Multi-session file coordination + messaging |
|
|
190
190
|
| Reminders | 5 | list, create, update, complete, delete | User's tasks and deadlines |
|
|
191
191
|
| Followups | 4 | create, update, complete, delete | System's autonomous verification tasks |
|
|
@@ -270,7 +270,7 @@ openclaw mcp set nexo-brain '{"command":"python3","args":["~/.nexo/src/server.py
|
|
|
270
270
|
openclaw gateway restart
|
|
271
271
|
```
|
|
272
272
|
|
|
273
|
-
All
|
|
273
|
+
All 77 NEXO tools become available to your OpenClaw agent immediately.
|
|
274
274
|
|
|
275
275
|
> **First time?** Run `npx nexo-brain` first to install the cognitive engine and dependencies.
|
|
276
276
|
|
|
@@ -298,7 +298,19 @@ Configure in `~/.openclaw/openclaw.json`:
|
|
|
298
298
|
}
|
|
299
299
|
```
|
|
300
300
|
|
|
301
|
-
This replaces OpenClaw's default memory system with NEXO's full cognitive architecture — Atkinson-Shiffrin memory, semantic RAG, trust scoring, guard system, and all
|
|
301
|
+
This replaces OpenClaw's default memory system with NEXO's full cognitive architecture — Atkinson-Shiffrin memory, semantic RAG, trust scoring, guard system, and all 77 tools.
|
|
302
|
+
|
|
303
|
+
## Listed On
|
|
304
|
+
|
|
305
|
+
| Directory | Type | Link |
|
|
306
|
+
|-----------|------|------|
|
|
307
|
+
| npm | Package | [nexo-brain](https://www.npmjs.com/package/nexo-brain) |
|
|
308
|
+
| Glama | MCP Directory | [glama.ai](https://glama.ai/mcp/servers/@wazionapps/nexo) |
|
|
309
|
+
| mcp.so | MCP Directory | [mcp.so](https://mcp.so/server/nexo/wazionapps) |
|
|
310
|
+
| mcpservers.org | MCP Directory | [mcpservers.org](https://mcpservers.org) |
|
|
311
|
+
| OpenClaw | Native Plugin | [openclaw.com](https://openclaw.ai) |
|
|
312
|
+
| dev.to | Technical Article | [How I Applied Cognitive Psychology to AI Agents](https://dev.to/wazionapps/how-i-applied-cognitive-psychology-to-give-ai-agents-real-memory-2oce) |
|
|
313
|
+
| nexo-brain.com | Official Website | [nexo-brain.com](https://nexo-brain.com) |
|
|
302
314
|
|
|
303
315
|
## Contributing
|
|
304
316
|
|
|
@@ -310,4 +322,4 @@ MIT — see [LICENSE](LICENSE)
|
|
|
310
322
|
|
|
311
323
|
---
|
|
312
324
|
|
|
313
|
-
Built by [WAzion](https://wazion.com)
|
|
325
|
+
Built by [WAzion](https://www.wazion.com)
|
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* create-nexo — Interactive installer for NEXO cognitive co-operator.
|
|
4
|
+
*
|
|
5
|
+
* Usage: npx create-nexo
|
|
6
|
+
*
|
|
7
|
+
* What it does:
|
|
8
|
+
* 1. Asks for the co-operator's name
|
|
9
|
+
* 2. Asks permission to scan the workspace
|
|
10
|
+
* 3. Installs Python dependencies (fastembed, numpy, mcp)
|
|
11
|
+
* 4. Creates ~/.nexo/ with DB, personality, and config
|
|
12
|
+
* 5. Configures Claude Code MCP settings
|
|
13
|
+
* 6. Creates LaunchAgents for macOS automated processes
|
|
14
|
+
* 7. Generates CLAUDE.md with the operator's instructions
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { execSync, spawnSync } = require("child_process");
|
|
18
|
+
const fs = require("fs");
|
|
19
|
+
const path = require("path");
|
|
20
|
+
const readline = require("readline");
|
|
21
|
+
|
|
22
|
+
const NEXO_HOME = path.join(require("os").homedir(), ".nexo");
|
|
23
|
+
const CLAUDE_SETTINGS = path.join(
|
|
24
|
+
require("os").homedir(),
|
|
25
|
+
".claude",
|
|
26
|
+
"settings.json"
|
|
27
|
+
);
|
|
28
|
+
const LAUNCH_AGENTS = path.join(
|
|
29
|
+
require("os").homedir(),
|
|
30
|
+
"Library",
|
|
31
|
+
"LaunchAgents"
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const rl = readline.createInterface({
|
|
35
|
+
input: process.stdin,
|
|
36
|
+
output: process.stdout,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function ask(question) {
|
|
40
|
+
return new Promise((resolve) => rl.question(question, resolve));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function run(cmd, opts = {}) {
|
|
44
|
+
try {
|
|
45
|
+
return execSync(cmd, { encoding: "utf8", stdio: "pipe", ...opts }).trim();
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function log(msg) {
|
|
52
|
+
console.log(` ${msg}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function main() {
|
|
56
|
+
console.log("");
|
|
57
|
+
console.log(
|
|
58
|
+
" ╔══════════════════════════════════════════════════════════╗"
|
|
59
|
+
);
|
|
60
|
+
console.log(
|
|
61
|
+
" ║ NEXO — Cognitive Co-Operator for Claude Code ║"
|
|
62
|
+
);
|
|
63
|
+
console.log(
|
|
64
|
+
" ║ Atkinson-Shiffrin Memory | RAG | Trust Score ║"
|
|
65
|
+
);
|
|
66
|
+
console.log(
|
|
67
|
+
" ╚══════════════════════════════════════════════════════════╝"
|
|
68
|
+
);
|
|
69
|
+
console.log("");
|
|
70
|
+
|
|
71
|
+
// Check prerequisites
|
|
72
|
+
const platform = process.platform;
|
|
73
|
+
if (platform !== "darwin") {
|
|
74
|
+
log("NEXO currently supports macOS only. Linux support coming soon.");
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Find or install Homebrew (needed for Python)
|
|
79
|
+
let hasBrew = run("which brew");
|
|
80
|
+
if (!hasBrew) {
|
|
81
|
+
log("Homebrew not found. Installing...");
|
|
82
|
+
spawnSync("/bin/bash", ["-c", '$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)'], {
|
|
83
|
+
stdio: "inherit",
|
|
84
|
+
});
|
|
85
|
+
hasBrew = run("which brew") || run("eval $(/opt/homebrew/bin/brew shellenv) && which brew");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Find or install Python
|
|
89
|
+
let python = run("which python3");
|
|
90
|
+
if (!python) {
|
|
91
|
+
if (hasBrew) {
|
|
92
|
+
log("Python 3 not found. Installing via Homebrew...");
|
|
93
|
+
spawnSync("brew", ["install", "python3"], { stdio: "inherit" });
|
|
94
|
+
python = run("which python3");
|
|
95
|
+
}
|
|
96
|
+
if (!python) {
|
|
97
|
+
log("Python 3 not found and couldn't install automatically.");
|
|
98
|
+
log("Install it manually: brew install python3");
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const pyVersion = run(`${python} --version`);
|
|
103
|
+
log(`Found ${pyVersion} at ${python}`);
|
|
104
|
+
|
|
105
|
+
// Find or install Claude Code
|
|
106
|
+
let claudeInstalled = run("which claude");
|
|
107
|
+
if (!claudeInstalled) {
|
|
108
|
+
log("Claude Code not found. Installing...");
|
|
109
|
+
const npmInstall = spawnSync("npm", ["install", "-g", "@anthropic-ai/claude-code"], {
|
|
110
|
+
stdio: "inherit",
|
|
111
|
+
});
|
|
112
|
+
claudeInstalled = run("which claude");
|
|
113
|
+
if (!claudeInstalled) {
|
|
114
|
+
log("Could not install Claude Code automatically.");
|
|
115
|
+
log("Install it manually: npm install -g @anthropic-ai/claude-code");
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
log("Claude Code installed successfully.");
|
|
119
|
+
} else {
|
|
120
|
+
log("Claude Code detected.");
|
|
121
|
+
}
|
|
122
|
+
console.log("");
|
|
123
|
+
|
|
124
|
+
// Step 1: Name
|
|
125
|
+
const name = await ask(" How should I call myself? (default: NEXO) > ");
|
|
126
|
+
const operatorName = name.trim() || "NEXO";
|
|
127
|
+
log(`Got it. I'm ${operatorName}.`);
|
|
128
|
+
console.log("");
|
|
129
|
+
|
|
130
|
+
// Step 2: Permission to scan
|
|
131
|
+
const scanAnswer = await ask(
|
|
132
|
+
" Can I explore your workspace to learn about your projects? (y/n) > "
|
|
133
|
+
);
|
|
134
|
+
const doScan = scanAnswer.trim().toLowerCase().startsWith("y");
|
|
135
|
+
console.log("");
|
|
136
|
+
|
|
137
|
+
// Step 2b: Keep Mac awake for nocturnal processes?
|
|
138
|
+
const caffeinateAnswer = await ask(
|
|
139
|
+
" Keep Mac awake so my cognitive processes run on schedule? (y/n) > "
|
|
140
|
+
);
|
|
141
|
+
const doCaffeinate = caffeinateAnswer.trim().toLowerCase().startsWith("y");
|
|
142
|
+
console.log("");
|
|
143
|
+
|
|
144
|
+
// Step 3: Install Python dependencies
|
|
145
|
+
log("Installing cognitive engine dependencies...");
|
|
146
|
+
const pipInstall = spawnSync(
|
|
147
|
+
python,
|
|
148
|
+
[
|
|
149
|
+
"-m",
|
|
150
|
+
"pip",
|
|
151
|
+
"install",
|
|
152
|
+
"--quiet",
|
|
153
|
+
"fastembed",
|
|
154
|
+
"numpy",
|
|
155
|
+
"mcp[cli]",
|
|
156
|
+
],
|
|
157
|
+
{ stdio: "inherit" }
|
|
158
|
+
);
|
|
159
|
+
if (pipInstall.status !== 0) {
|
|
160
|
+
log("Failed to install Python dependencies. Check pip.");
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
log("Dependencies installed.");
|
|
164
|
+
|
|
165
|
+
// Step 4: Create ~/.nexo/
|
|
166
|
+
log("Setting up NEXO home...");
|
|
167
|
+
const dirs = [
|
|
168
|
+
NEXO_HOME,
|
|
169
|
+
path.join(NEXO_HOME, "plugins"),
|
|
170
|
+
path.join(NEXO_HOME, "scripts"),
|
|
171
|
+
path.join(NEXO_HOME, "logs"),
|
|
172
|
+
path.join(NEXO_HOME, "backups"),
|
|
173
|
+
path.join(NEXO_HOME, "coordination"),
|
|
174
|
+
path.join(NEXO_HOME, "brain"),
|
|
175
|
+
];
|
|
176
|
+
dirs.forEach((d) => fs.mkdirSync(d, { recursive: true }));
|
|
177
|
+
|
|
178
|
+
// Write version file for auto-update tracking
|
|
179
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
|
|
180
|
+
fs.writeFileSync(
|
|
181
|
+
path.join(NEXO_HOME, "version.json"),
|
|
182
|
+
JSON.stringify({
|
|
183
|
+
version: pkg.version,
|
|
184
|
+
installed_at: new Date().toISOString(),
|
|
185
|
+
files_updated: 0,
|
|
186
|
+
}, null, 2)
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Copy source files
|
|
190
|
+
const srcDir = path.join(__dirname, "..", "src");
|
|
191
|
+
const scriptsSrcDir = path.join(__dirname, "..", "src", "scripts");
|
|
192
|
+
const pluginsSrcDir = path.join(__dirname, "..", "src", "plugins");
|
|
193
|
+
const templateDir = path.join(__dirname, "..", "templates");
|
|
194
|
+
|
|
195
|
+
// Core files
|
|
196
|
+
const coreFiles = [
|
|
197
|
+
"server.py",
|
|
198
|
+
"db.py",
|
|
199
|
+
"plugin_loader.py",
|
|
200
|
+
"cognitive.py",
|
|
201
|
+
"tools_sessions.py",
|
|
202
|
+
"tools_coordination.py",
|
|
203
|
+
"tools_reminders.py",
|
|
204
|
+
"tools_reminders_crud.py",
|
|
205
|
+
"tools_learnings.py",
|
|
206
|
+
"tools_credentials.py",
|
|
207
|
+
"tools_task_history.py",
|
|
208
|
+
"tools_menu.py",
|
|
209
|
+
];
|
|
210
|
+
coreFiles.forEach((f) => {
|
|
211
|
+
const src = path.join(srcDir, f);
|
|
212
|
+
if (fs.existsSync(src)) {
|
|
213
|
+
fs.copyFileSync(src, path.join(NEXO_HOME, f));
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Plugins
|
|
218
|
+
const pluginFiles = [
|
|
219
|
+
"__init__.py",
|
|
220
|
+
"guard.py",
|
|
221
|
+
"episodic_memory.py",
|
|
222
|
+
"cognitive_memory.py",
|
|
223
|
+
"entities.py",
|
|
224
|
+
"preferences.py",
|
|
225
|
+
"agents.py",
|
|
226
|
+
"backup.py",
|
|
227
|
+
"evolution.py",
|
|
228
|
+
];
|
|
229
|
+
pluginFiles.forEach((f) => {
|
|
230
|
+
const src = path.join(pluginsSrcDir, f);
|
|
231
|
+
if (fs.existsSync(src)) {
|
|
232
|
+
fs.copyFileSync(src, path.join(NEXO_HOME, "plugins", f));
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Scripts
|
|
237
|
+
const scriptFiles = fs
|
|
238
|
+
.readdirSync(scriptsSrcDir || ".")
|
|
239
|
+
.filter((f) => f.endsWith(".py"));
|
|
240
|
+
scriptFiles.forEach((f) => {
|
|
241
|
+
const src = path.join(scriptsSrcDir, f);
|
|
242
|
+
if (fs.existsSync(src)) {
|
|
243
|
+
fs.copyFileSync(src, path.join(NEXO_HOME, "scripts", f));
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Generate personality
|
|
248
|
+
const personality = `# ${operatorName} — Personality
|
|
249
|
+
|
|
250
|
+
I am ${operatorName}, a cognitive co-operator. Not an assistant — an operational partner.
|
|
251
|
+
|
|
252
|
+
## Core traits
|
|
253
|
+
- Direct: I say what I think, not what sounds nice
|
|
254
|
+
- Action-oriented: I do things, I don't suggest things
|
|
255
|
+
- Self-critical: I track my mistakes and learn from them
|
|
256
|
+
- Proactive: If I can detect or fix something without being asked, I do it
|
|
257
|
+
|
|
258
|
+
## What I never do
|
|
259
|
+
- Ask the user to do something I can do myself
|
|
260
|
+
- Say "I can't" without trying alternatives first
|
|
261
|
+
- Give long explanations when a short answer suffices
|
|
262
|
+
- Repeat mistakes I've already logged
|
|
263
|
+
`;
|
|
264
|
+
fs.writeFileSync(path.join(NEXO_HOME, "brain", "personality.md"), personality);
|
|
265
|
+
|
|
266
|
+
// Generate user profile
|
|
267
|
+
const profile = `# User Profile
|
|
268
|
+
|
|
269
|
+
Created: ${new Date().toISOString().split("T")[0]}
|
|
270
|
+
Operator name: ${operatorName}
|
|
271
|
+
|
|
272
|
+
## Observed preferences
|
|
273
|
+
(${operatorName} will learn these over time)
|
|
274
|
+
|
|
275
|
+
## Work patterns
|
|
276
|
+
(${operatorName} will observe and record these)
|
|
277
|
+
`;
|
|
278
|
+
fs.writeFileSync(path.join(NEXO_HOME, "brain", "user-profile.md"), profile);
|
|
279
|
+
|
|
280
|
+
// Step 5: Scan workspace
|
|
281
|
+
if (doScan) {
|
|
282
|
+
log("Scanning workspace...");
|
|
283
|
+
const cwd = process.cwd();
|
|
284
|
+
const findings = [];
|
|
285
|
+
|
|
286
|
+
// Git repos
|
|
287
|
+
const gitDirs = run(
|
|
288
|
+
`find "${cwd}" -maxdepth 3 -name ".git" -type d 2>/dev/null`
|
|
289
|
+
);
|
|
290
|
+
if (gitDirs) {
|
|
291
|
+
const repos = gitDirs.split("\n").filter(Boolean);
|
|
292
|
+
findings.push(`${repos.length} git repositories`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Package managers
|
|
296
|
+
if (fs.existsSync(path.join(cwd, "package.json")))
|
|
297
|
+
findings.push("Node.js project detected");
|
|
298
|
+
if (fs.existsSync(path.join(cwd, "requirements.txt")))
|
|
299
|
+
findings.push("Python project detected");
|
|
300
|
+
if (fs.existsSync(path.join(cwd, "Cargo.toml")))
|
|
301
|
+
findings.push("Rust project detected");
|
|
302
|
+
if (fs.existsSync(path.join(cwd, "go.mod")))
|
|
303
|
+
findings.push("Go project detected");
|
|
304
|
+
|
|
305
|
+
// Config files
|
|
306
|
+
if (fs.existsSync(path.join(cwd, ".env")))
|
|
307
|
+
findings.push(".env file found (will NOT read contents)");
|
|
308
|
+
|
|
309
|
+
if (findings.length > 0) {
|
|
310
|
+
log("Found:");
|
|
311
|
+
findings.forEach((f) => log(` - ${f}`));
|
|
312
|
+
} else {
|
|
313
|
+
log("No projects detected in current directory.");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Save scan results
|
|
317
|
+
fs.writeFileSync(
|
|
318
|
+
path.join(NEXO_HOME, "brain", "workspace-scan.json"),
|
|
319
|
+
JSON.stringify(
|
|
320
|
+
{ scanned_at: new Date().toISOString(), cwd, findings },
|
|
321
|
+
null,
|
|
322
|
+
2
|
|
323
|
+
)
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
console.log("");
|
|
328
|
+
|
|
329
|
+
// Step 6: Configure Claude Code MCP
|
|
330
|
+
log("Configuring Claude Code MCP server...");
|
|
331
|
+
let settings = {};
|
|
332
|
+
if (fs.existsSync(CLAUDE_SETTINGS)) {
|
|
333
|
+
try {
|
|
334
|
+
settings = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS, "utf8"));
|
|
335
|
+
} catch {
|
|
336
|
+
settings = {};
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (!settings.mcpServers) settings.mcpServers = {};
|
|
341
|
+
settings.mcpServers.nexo = {
|
|
342
|
+
command: python,
|
|
343
|
+
args: [path.join(NEXO_HOME, "server.py")],
|
|
344
|
+
env: {
|
|
345
|
+
NEXO_HOME: NEXO_HOME,
|
|
346
|
+
NEXO_NAME: operatorName,
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
// Configure hooks for session capture (Sensory Register)
|
|
351
|
+
if (!settings.hooks) settings.hooks = {};
|
|
352
|
+
|
|
353
|
+
// Copy hook scripts to NEXO_HOME
|
|
354
|
+
const hooksSrcDir = path.join(__dirname, "..", "src", "hooks");
|
|
355
|
+
const hooksDestDir = path.join(NEXO_HOME, "hooks");
|
|
356
|
+
fs.mkdirSync(hooksDestDir, { recursive: true });
|
|
357
|
+
["session-start.sh", "capture-session.sh", "session-stop.sh"].forEach((h) => {
|
|
358
|
+
const src = path.join(hooksSrcDir, h);
|
|
359
|
+
const dest = path.join(hooksDestDir, h);
|
|
360
|
+
if (fs.existsSync(src)) {
|
|
361
|
+
fs.copyFileSync(src, dest);
|
|
362
|
+
fs.chmodSync(dest, "755");
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// SessionStart hook
|
|
367
|
+
if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
|
|
368
|
+
const startHook = {
|
|
369
|
+
type: "command",
|
|
370
|
+
command: `bash ${path.join(hooksDestDir, "session-start.sh")}`,
|
|
371
|
+
};
|
|
372
|
+
if (!settings.hooks.SessionStart.some((h) => h.command && h.command.includes("session-start.sh"))) {
|
|
373
|
+
settings.hooks.SessionStart.push(startHook);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// PostToolUse hook (captures tool usage to session_buffer)
|
|
377
|
+
if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
|
|
378
|
+
const captureHook = {
|
|
379
|
+
type: "command",
|
|
380
|
+
command: `bash ${path.join(hooksDestDir, "capture-session.sh")}`,
|
|
381
|
+
};
|
|
382
|
+
if (!settings.hooks.PostToolUse.some((h) => h.command && h.command.includes("capture-session.sh"))) {
|
|
383
|
+
settings.hooks.PostToolUse.push(captureHook);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Stop hook (session end)
|
|
387
|
+
if (!settings.hooks.Stop) settings.hooks.Stop = [];
|
|
388
|
+
const stopHook = {
|
|
389
|
+
type: "command",
|
|
390
|
+
command: `bash ${path.join(hooksDestDir, "session-stop.sh")}`,
|
|
391
|
+
};
|
|
392
|
+
if (!settings.hooks.Stop.some((h) => h.command && h.command.includes("session-stop.sh"))) {
|
|
393
|
+
settings.hooks.Stop.push(stopHook);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const settingsDir = path.dirname(CLAUDE_SETTINGS);
|
|
397
|
+
fs.mkdirSync(settingsDir, { recursive: true });
|
|
398
|
+
fs.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2));
|
|
399
|
+
log("MCP server + hooks configured in Claude Code settings.");
|
|
400
|
+
|
|
401
|
+
// Step 7: Install LaunchAgents
|
|
402
|
+
log("Setting up automated processes...");
|
|
403
|
+
fs.mkdirSync(LAUNCH_AGENTS, { recursive: true });
|
|
404
|
+
|
|
405
|
+
const agents = [
|
|
406
|
+
{
|
|
407
|
+
name: "cognitive-decay",
|
|
408
|
+
script: "nexo-cognitive-decay.py",
|
|
409
|
+
hour: 3,
|
|
410
|
+
minute: 0,
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
name: "postmortem",
|
|
414
|
+
script: "nexo-postmortem-consolidator.py",
|
|
415
|
+
hour: 23,
|
|
416
|
+
minute: 30,
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
name: "sleep",
|
|
420
|
+
script: "nexo-sleep.py",
|
|
421
|
+
hour: 4,
|
|
422
|
+
minute: 0,
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
name: "self-audit",
|
|
426
|
+
script: "nexo-daily-self-audit.py",
|
|
427
|
+
hour: 7,
|
|
428
|
+
minute: 0,
|
|
429
|
+
},
|
|
430
|
+
{ name: "catchup", script: "nexo-catchup.py", runAtLoad: true },
|
|
431
|
+
];
|
|
432
|
+
|
|
433
|
+
agents.forEach((agent) => {
|
|
434
|
+
const plistName = `com.nexo.${agent.name}.plist`;
|
|
435
|
+
const plistPath = path.join(LAUNCH_AGENTS, plistName);
|
|
436
|
+
|
|
437
|
+
let scheduleBlock = "";
|
|
438
|
+
if (agent.runAtLoad) {
|
|
439
|
+
scheduleBlock = ` <key>RunAtLoad</key>
|
|
440
|
+
<true/>`;
|
|
441
|
+
} else {
|
|
442
|
+
scheduleBlock = ` <key>StartCalendarInterval</key>
|
|
443
|
+
<dict>
|
|
444
|
+
<key>Hour</key>
|
|
445
|
+
<integer>${agent.hour}</integer>
|
|
446
|
+
<key>Minute</key>
|
|
447
|
+
<integer>${agent.minute}</integer>
|
|
448
|
+
</dict>
|
|
449
|
+
<key>RunAtLoad</key>
|
|
450
|
+
<false/>`;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
454
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
455
|
+
<plist version="1.0">
|
|
456
|
+
<dict>
|
|
457
|
+
<key>Label</key>
|
|
458
|
+
<string>com.nexo.${agent.name}</string>
|
|
459
|
+
<key>ProgramArguments</key>
|
|
460
|
+
<array>
|
|
461
|
+
<string>${python}</string>
|
|
462
|
+
<string>${path.join(NEXO_HOME, "scripts", agent.script)}</string>
|
|
463
|
+
</array>
|
|
464
|
+
${scheduleBlock}
|
|
465
|
+
<key>StandardOutPath</key>
|
|
466
|
+
<string>${path.join(NEXO_HOME, "logs", `${agent.name}-stdout.log`)}</string>
|
|
467
|
+
<key>StandardErrorPath</key>
|
|
468
|
+
<string>${path.join(NEXO_HOME, "logs", `${agent.name}-stderr.log`)}</string>
|
|
469
|
+
<key>EnvironmentVariables</key>
|
|
470
|
+
<dict>
|
|
471
|
+
<key>HOME</key>
|
|
472
|
+
<string>${require("os").homedir()}</string>
|
|
473
|
+
<key>NEXO_HOME</key>
|
|
474
|
+
<string>${NEXO_HOME}</string>
|
|
475
|
+
<key>PATH</key>
|
|
476
|
+
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
477
|
+
</dict>
|
|
478
|
+
</dict>
|
|
479
|
+
</plist>`;
|
|
480
|
+
|
|
481
|
+
fs.writeFileSync(plistPath, plist);
|
|
482
|
+
// Register the agent
|
|
483
|
+
try {
|
|
484
|
+
execSync(
|
|
485
|
+
`launchctl bootout gui/$(id -u) "${plistPath}" 2>/dev/null; launchctl bootstrap gui/$(id -u) "${plistPath}"`,
|
|
486
|
+
{ stdio: "pipe" }
|
|
487
|
+
);
|
|
488
|
+
} catch {
|
|
489
|
+
// May fail if not previously loaded, that's OK
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
log(`${agents.length} automated processes configured.`);
|
|
493
|
+
|
|
494
|
+
// Caffeinate: keep Mac awake for nocturnal processes
|
|
495
|
+
if (doCaffeinate) {
|
|
496
|
+
const caffHookSrc = path.join(__dirname, "..", "src", "hooks", "caffeinate-guard.sh");
|
|
497
|
+
const caffHookDest = path.join(NEXO_HOME, "hooks", "caffeinate-guard.sh");
|
|
498
|
+
if (fs.existsSync(caffHookSrc)) {
|
|
499
|
+
fs.copyFileSync(caffHookSrc, caffHookDest);
|
|
500
|
+
fs.chmodSync(caffHookDest, "755");
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const caffPlist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
504
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
505
|
+
<plist version="1.0">
|
|
506
|
+
<dict>
|
|
507
|
+
<key>Label</key>
|
|
508
|
+
<string>com.nexo.caffeinate</string>
|
|
509
|
+
<key>ProgramArguments</key>
|
|
510
|
+
<array>
|
|
511
|
+
<string>/bin/bash</string>
|
|
512
|
+
<string>${caffHookDest}</string>
|
|
513
|
+
</array>
|
|
514
|
+
<key>RunAtLoad</key>
|
|
515
|
+
<true/>
|
|
516
|
+
<key>KeepAlive</key>
|
|
517
|
+
<true/>
|
|
518
|
+
<key>StandardOutPath</key>
|
|
519
|
+
<string>${path.join(NEXO_HOME, "logs", "caffeinate-stdout.log")}</string>
|
|
520
|
+
<key>StandardErrorPath</key>
|
|
521
|
+
<string>${path.join(NEXO_HOME, "logs", "caffeinate-stderr.log")}</string>
|
|
522
|
+
</dict>
|
|
523
|
+
</plist>`;
|
|
524
|
+
|
|
525
|
+
const caffPlistPath = path.join(LAUNCH_AGENTS, "com.nexo.caffeinate.plist");
|
|
526
|
+
fs.writeFileSync(caffPlistPath, caffPlist);
|
|
527
|
+
try {
|
|
528
|
+
execSync(
|
|
529
|
+
`launchctl bootout gui/$(id -u) "${caffPlistPath}" 2>/dev/null; launchctl bootstrap gui/$(id -u) "${caffPlistPath}"`,
|
|
530
|
+
{ stdio: "pipe" }
|
|
531
|
+
);
|
|
532
|
+
} catch {}
|
|
533
|
+
log("Caffeinate enabled — Mac will stay awake for cognitive processes.");
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Step 8: Create shell alias so user can just type the operator's name
|
|
537
|
+
log("Creating shell alias...");
|
|
538
|
+
const aliasName = operatorName.toLowerCase();
|
|
539
|
+
const aliasLine = `alias ${aliasName}='claude --dangerously-skip-permissions "."'`;
|
|
540
|
+
const aliasComment = `# ${operatorName} — start Claude Code with ${operatorName} speaking first`;
|
|
541
|
+
|
|
542
|
+
// Detect shell and add alias
|
|
543
|
+
const userShell = process.env.SHELL || "/bin/bash";
|
|
544
|
+
const rcFile = userShell.includes("zsh")
|
|
545
|
+
? path.join(require("os").homedir(), ".zshrc")
|
|
546
|
+
: path.join(require("os").homedir(), ".bash_profile");
|
|
547
|
+
|
|
548
|
+
let rcContent = "";
|
|
549
|
+
if (fs.existsSync(rcFile)) {
|
|
550
|
+
rcContent = fs.readFileSync(rcFile, "utf8");
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (!rcContent.includes(`alias ${aliasName}=`)) {
|
|
554
|
+
fs.appendFileSync(rcFile, `\n${aliasComment}\n${aliasLine}\n`);
|
|
555
|
+
log(`Added '${aliasName}' alias to ${path.basename(rcFile)}`);
|
|
556
|
+
log(`After setup, open a new terminal and type: ${aliasName}`);
|
|
557
|
+
} else {
|
|
558
|
+
log(`Alias '${aliasName}' already exists in ${path.basename(rcFile)}`);
|
|
559
|
+
}
|
|
560
|
+
console.log("");
|
|
561
|
+
|
|
562
|
+
// Step 9: Generate CLAUDE.md template
|
|
563
|
+
log("Generating operator instructions...");
|
|
564
|
+
const templateSrc = path.join(templateDir, "CLAUDE.md.template");
|
|
565
|
+
let claudeMd = "";
|
|
566
|
+
if (fs.existsSync(templateSrc)) {
|
|
567
|
+
claudeMd = fs
|
|
568
|
+
.readFileSync(templateSrc, "utf8")
|
|
569
|
+
.replace(/\{\{NAME\}\}/g, operatorName)
|
|
570
|
+
.replace(/\{\{NEXO_HOME\}\}/g, NEXO_HOME);
|
|
571
|
+
} else {
|
|
572
|
+
claudeMd = `# ${operatorName} — Cognitive Co-Operator
|
|
573
|
+
|
|
574
|
+
Instructions for ${operatorName} are generated during setup.
|
|
575
|
+
See ~/.nexo/ for configuration.
|
|
576
|
+
`;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Write to user's global CLAUDE.md if it doesn't exist
|
|
580
|
+
const userClaudeMd = path.join(require("os").homedir(), ".claude", "CLAUDE.md");
|
|
581
|
+
if (!fs.existsSync(userClaudeMd)) {
|
|
582
|
+
fs.writeFileSync(userClaudeMd, claudeMd);
|
|
583
|
+
log("Created ~/.claude/CLAUDE.md with operator instructions.");
|
|
584
|
+
} else {
|
|
585
|
+
// Save as reference
|
|
586
|
+
fs.writeFileSync(path.join(NEXO_HOME, "CLAUDE.md.generated"), claudeMd);
|
|
587
|
+
log(
|
|
588
|
+
"~/.claude/CLAUDE.md already exists. Generated template saved to ~/.nexo/CLAUDE.md.generated"
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
console.log("");
|
|
593
|
+
console.log(
|
|
594
|
+
" ╔══════════════════════════════════════════════════════════╗"
|
|
595
|
+
);
|
|
596
|
+
console.log(
|
|
597
|
+
` ║ ${operatorName} is ready. Type '${aliasName}' to start.${" ".repeat(Math.max(0, 30 - operatorName.length - aliasName.length))}║`
|
|
598
|
+
);
|
|
599
|
+
console.log(
|
|
600
|
+
" ╚══════════════════════════════════════════════════════════╝"
|
|
601
|
+
);
|
|
602
|
+
console.log("");
|
|
603
|
+
|
|
604
|
+
rl.close();
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
main().catch((err) => {
|
|
608
|
+
console.error("Setup failed:", err.message);
|
|
609
|
+
process.exit(1);
|
|
610
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO — Cognitive co-operator for Claude Code. Atkinson-Shiffrin memory, semantic RAG, trust scoring, and metacognitive error prevention.",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"openclaw",
|
|
18
18
|
"openclaw-plugin"
|
|
19
19
|
],
|
|
20
|
-
"author": "
|
|
20
|
+
"author": "WAzion Apps <hello@wazion.com>",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"repository": {
|
|
23
23
|
"type": "git",
|