nexo-brain 0.1.2 → 0.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 +61 -0
- package/bin/nexo-brain 2.js +610 -0
- package/package.json +4 -2
- package/scripts/pre-commit-check 2.sh +55 -0
- package/src/cognitive 2.py +1224 -0
- package/src/db 2.py +2283 -0
- package/src/plugin_loader 2.py +136 -0
- package/src/server 2.py +560 -0
- package/src/tools_coordination 2.py +102 -0
- package/src/tools_credentials 2.py +64 -0
- package/src/tools_learnings 2.py +180 -0
- package/src/tools_menu 2.py +208 -0
- package/src/tools_reminders 2.py +80 -0
- package/src/tools_reminders_crud 2.py +157 -0
- package/src/tools_sessions 2.py +169 -0
- package/src/tools_task_history 2.py +57 -0
- package/templates/CLAUDE.md 2.template +89 -0
- package/templates/openclaw.json +13 -0
package/README.md
CHANGED
|
@@ -239,6 +239,67 @@ NEXO isn't just engineering — it's applied cognitive psychology:
|
|
|
239
239
|
| Synaptic Pruning | Automated cleanup of weak, unused memories |
|
|
240
240
|
| Associative Memory | Semantic search finds related concepts, not just matching words |
|
|
241
241
|
|
|
242
|
+
## OpenClaw Integration
|
|
243
|
+
|
|
244
|
+
NEXO Brain works as a cognitive memory backend for [OpenClaw](https://github.com/openclaw/openclaw). Three integration paths, from instant to deep:
|
|
245
|
+
|
|
246
|
+
### Path 1: MCP Bridge (Zero Code — Works Now)
|
|
247
|
+
|
|
248
|
+
Add NEXO Brain to your OpenClaw config at `~/.openclaw/openclaw.json`:
|
|
249
|
+
|
|
250
|
+
```json
|
|
251
|
+
{
|
|
252
|
+
"mcp": {
|
|
253
|
+
"servers": {
|
|
254
|
+
"nexo-brain": {
|
|
255
|
+
"command": "python3",
|
|
256
|
+
"args": ["~/.nexo/src/server.py"],
|
|
257
|
+
"env": {
|
|
258
|
+
"NEXO_HOME": "~/.nexo"
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Or via CLI:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
openclaw mcp set nexo-brain '{"command":"python3","args":["~/.nexo/src/server.py"],"env":{"NEXO_HOME":"~/.nexo"}}'
|
|
270
|
+
openclaw gateway restart
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
All 76 NEXO tools become available to your OpenClaw agent immediately.
|
|
274
|
+
|
|
275
|
+
> **First time?** Run `npx nexo-brain` first to install the cognitive engine and dependencies.
|
|
276
|
+
|
|
277
|
+
### Path 2: ClawHub Skill (Install in Seconds)
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
npx clawhub@latest install nexo-brain
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Path 3: Native Memory Plugin (Replaces Default Memory)
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
npm install @wazionapps/openclaw-memory-nexo-brain
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Configure in `~/.openclaw/openclaw.json`:
|
|
290
|
+
|
|
291
|
+
```json
|
|
292
|
+
{
|
|
293
|
+
"plugins": {
|
|
294
|
+
"slots": {
|
|
295
|
+
"memory": "memory-nexo-brain"
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
```
|
|
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 76 tools.
|
|
302
|
+
|
|
242
303
|
## Contributing
|
|
243
304
|
|
|
244
305
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. Issues and PRs welcome.
|
|
@@ -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.2.0",
|
|
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": {
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
"memory",
|
|
14
14
|
"ai-assistant",
|
|
15
15
|
"vector-search",
|
|
16
|
-
"atkinson-shiffrin"
|
|
16
|
+
"atkinson-shiffrin",
|
|
17
|
+
"openclaw",
|
|
18
|
+
"openclaw-plugin"
|
|
17
19
|
],
|
|
18
20
|
"author": "Francisco Garcia <hello@wazion.com>",
|
|
19
21
|
"license": "MIT",
|