nexo-brain 0.1.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/LICENSE +21 -0
- package/README.md +241 -0
- package/bin/create-nexo.js +593 -0
- package/package.json +32 -0
- package/scripts/pre-commit-check.sh +55 -0
- package/src/cognitive.py +1224 -0
- package/src/db.py +2283 -0
- package/src/hooks/caffeinate-guard.sh +8 -0
- package/src/hooks/capture-session.sh +19 -0
- package/src/hooks/session-start.sh +27 -0
- package/src/hooks/session-stop.sh +11 -0
- package/src/plugin_loader.py +136 -0
- package/src/plugins/__init__.py +0 -0
- package/src/plugins/agents.py +52 -0
- package/src/plugins/backup.py +103 -0
- package/src/plugins/cognitive_memory.py +305 -0
- package/src/plugins/entities.py +61 -0
- package/src/plugins/episodic_memory.py +391 -0
- package/src/plugins/evolution.py +113 -0
- package/src/plugins/guard.py +346 -0
- package/src/plugins/preferences.py +47 -0
- package/src/scripts/nexo-auto-update.py +213 -0
- package/src/scripts/nexo-catchup.py +179 -0
- package/src/scripts/nexo-cognitive-decay.py +82 -0
- package/src/scripts/nexo-daily-self-audit.py +532 -0
- package/src/scripts/nexo-postmortem-consolidator.py +594 -0
- package/src/scripts/nexo-sleep.py +762 -0
- package/src/scripts/nexo-synthesis.py +537 -0
- package/src/server.py +560 -0
- package/src/tools_coordination.py +102 -0
- package/src/tools_credentials.py +64 -0
- package/src/tools_learnings.py +180 -0
- package/src/tools_menu.py +208 -0
- package/src/tools_reminders.py +80 -0
- package/src/tools_reminders_crud.py +157 -0
- package/src/tools_sessions.py +169 -0
- package/src/tools_task_history.py +57 -0
- package/templates/CLAUDE.md.template +89 -0
|
@@ -0,0 +1,593 @@
|
|
|
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: Generate CLAUDE.md template
|
|
537
|
+
log("Generating operator instructions...");
|
|
538
|
+
const templateSrc = path.join(templateDir, "CLAUDE.md.template");
|
|
539
|
+
let claudeMd = "";
|
|
540
|
+
if (fs.existsSync(templateSrc)) {
|
|
541
|
+
claudeMd = fs
|
|
542
|
+
.readFileSync(templateSrc, "utf8")
|
|
543
|
+
.replace(/\{\{NAME\}\}/g, operatorName)
|
|
544
|
+
.replace(/\{\{NEXO_HOME\}\}/g, NEXO_HOME);
|
|
545
|
+
} else {
|
|
546
|
+
claudeMd = `# ${operatorName} — Cognitive Co-Operator
|
|
547
|
+
|
|
548
|
+
Instructions for ${operatorName} are generated during setup.
|
|
549
|
+
See ~/.nexo/ for configuration.
|
|
550
|
+
`;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Write to user's global CLAUDE.md if it doesn't exist
|
|
554
|
+
const userClaudeMd = path.join(require("os").homedir(), ".claude", "CLAUDE.md");
|
|
555
|
+
if (!fs.existsSync(userClaudeMd)) {
|
|
556
|
+
fs.writeFileSync(userClaudeMd, claudeMd);
|
|
557
|
+
log("Created ~/.claude/CLAUDE.md with operator instructions.");
|
|
558
|
+
} else {
|
|
559
|
+
// Save as reference
|
|
560
|
+
fs.writeFileSync(path.join(NEXO_HOME, "CLAUDE.md.generated"), claudeMd);
|
|
561
|
+
log(
|
|
562
|
+
"~/.claude/CLAUDE.md already exists. Generated template saved to ~/.nexo/CLAUDE.md.generated"
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
console.log("");
|
|
567
|
+
console.log(
|
|
568
|
+
" ╔══════════════════════════════════════════════════════════╗"
|
|
569
|
+
);
|
|
570
|
+
console.log(
|
|
571
|
+
` ║ ${operatorName} is ready.${" ".repeat(Math.max(0, 39 - operatorName.length))}║`
|
|
572
|
+
);
|
|
573
|
+
console.log(
|
|
574
|
+
" ║ ║"
|
|
575
|
+
);
|
|
576
|
+
console.log(
|
|
577
|
+
" ║ Open Claude Code and start a conversation. ║"
|
|
578
|
+
);
|
|
579
|
+
console.log(
|
|
580
|
+
` ║ ${operatorName} will introduce ${operatorName.length > 4 ? "itself" : "itself"} on first message.${" ".repeat(Math.max(0, 27 - operatorName.length))}║`
|
|
581
|
+
);
|
|
582
|
+
console.log(
|
|
583
|
+
" ╚══════════════════════════════════════════════════════════╝"
|
|
584
|
+
);
|
|
585
|
+
console.log("");
|
|
586
|
+
|
|
587
|
+
rl.close();
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
main().catch((err) => {
|
|
591
|
+
console.error("Setup failed:", err.message);
|
|
592
|
+
process.exit(1);
|
|
593
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nexo-brain",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "NEXO — Cognitive co-operator for Claude Code. Atkinson-Shiffrin memory, semantic RAG, trust scoring, and metacognitive error prevention.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"nexo-brain": "./bin/create-nexo.js"
|
|
7
|
+
},
|
|
8
|
+
"keywords": [
|
|
9
|
+
"claude-code",
|
|
10
|
+
"mcp",
|
|
11
|
+
"cognitive-architecture",
|
|
12
|
+
"memory",
|
|
13
|
+
"ai-assistant",
|
|
14
|
+
"vector-search",
|
|
15
|
+
"atkinson-shiffrin"
|
|
16
|
+
],
|
|
17
|
+
"author": "Francisco Garcia <hello@wazion.com>",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/wazionapps/nexo.git"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"bin/",
|
|
28
|
+
"src/",
|
|
29
|
+
"templates/",
|
|
30
|
+
"scripts/"
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Pre-commit hook: prevent private data from being committed to the public repo.
|
|
3
|
+
# Installed by create-nexo or manually: cp scripts/pre-commit-check.sh .git/hooks/pre-commit
|
|
4
|
+
|
|
5
|
+
RED='\033[0;31m'
|
|
6
|
+
NC='\033[0m'
|
|
7
|
+
|
|
8
|
+
# Add patterns specific to your private data here.
|
|
9
|
+
# These are checked against staged files to prevent accidental leaks.
|
|
10
|
+
# The pre-commit-check.sh script itself is excluded from scanning.
|
|
11
|
+
BLOCKED_PATTERNS=(
|
|
12
|
+
# Add your own patterns below, e.g.:
|
|
13
|
+
# "my-private-api-key"
|
|
14
|
+
# "my-private-domain.com"
|
|
15
|
+
# "my-server-ip"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR)
|
|
19
|
+
|
|
20
|
+
if [ -z "$STAGED_FILES" ]; then
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
FOUND=0
|
|
25
|
+
for pattern in "${BLOCKED_PATTERNS[@]}"; do
|
|
26
|
+
MATCHES=$(echo "$STAGED_FILES" | xargs grep -l "$pattern" 2>/dev/null)
|
|
27
|
+
if [ -n "$MATCHES" ]; then
|
|
28
|
+
echo -e "${RED}BLOCKED: Found private data pattern '$pattern' in:${NC}"
|
|
29
|
+
echo "$MATCHES" | sed 's/^/ /'
|
|
30
|
+
FOUND=1
|
|
31
|
+
fi
|
|
32
|
+
done
|
|
33
|
+
|
|
34
|
+
# Also check for .db files, tokens, credentials
|
|
35
|
+
DB_FILES=$(echo "$STAGED_FILES" | grep -E '\.(db|db-wal|db-shm|key|pem)$')
|
|
36
|
+
if [ -n "$DB_FILES" ]; then
|
|
37
|
+
echo -e "${RED}BLOCKED: Database/key files staged:${NC}"
|
|
38
|
+
echo "$DB_FILES" | sed 's/^/ /'
|
|
39
|
+
FOUND=1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
TOKEN_FILES=$(echo "$STAGED_FILES" | grep -E '_token\.|credentials|\.env$')
|
|
43
|
+
if [ -n "$TOKEN_FILES" ]; then
|
|
44
|
+
echo -e "${RED}BLOCKED: Token/credential files staged:${NC}"
|
|
45
|
+
echo "$TOKEN_FILES" | sed 's/^/ /'
|
|
46
|
+
FOUND=1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
if [ $FOUND -eq 1 ]; then
|
|
50
|
+
echo ""
|
|
51
|
+
echo -e "${RED}Commit blocked. Remove private data before pushing to public repo.${NC}"
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
exit 0
|