@cleocode/cleo 2026.3.26 → 2026.3.27

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.
@@ -0,0 +1,338 @@
1
+ #!/usr/bin/env node
2
+ // CLEO Brain Observation Worker
3
+ // HTTP server that receives hook events and stores observations via cleo CLI.
4
+ // Designed as a background daemon — starts fast, handles errors silently.
5
+
6
+ const http = require('node:http');
7
+ const { execFileSync, execFile, fork } = require('node:child_process');
8
+ const fs = require('node:fs');
9
+ const path = require('node:path');
10
+ const os = require('node:os');
11
+
12
+ const PORT = 37778;
13
+ const PID_FILE = path.join(os.homedir(), '.cleo', 'brain-worker.pid');
14
+ const LOG_FILE = path.join(os.homedir(), '.cleo', 'logs', 'brain-worker.log');
15
+ const CLEO_BIN = path.join(os.homedir(), '.cleo', 'bin', 'cleo');
16
+
17
+ // Tools to skip (too noisy or meta)
18
+ const SKIP_TOOLS = new Set([
19
+ 'ListMcpResourcesTool', 'SlashCommand', 'Skill', 'TodoWrite',
20
+ 'AskUserQuestion', 'TaskList', 'TaskUpdate', 'TaskCreate',
21
+ 'TeamCreate', 'SendMessage', 'ToolSearch',
22
+ ]);
23
+ const SKIP_PREFIXES = ['mcp__cleo', 'mcp__claude-mem', 'mcp__plugin_claude-mem'];
24
+
25
+ // --- Logging ---
26
+
27
+ function ensureLogDir() {
28
+ const dir = path.dirname(LOG_FILE);
29
+ if (!fs.existsSync(dir)) {
30
+ fs.mkdirSync(dir, { recursive: true });
31
+ }
32
+ }
33
+
34
+ function log(msg) {
35
+ try {
36
+ ensureLogDir();
37
+ const ts = new Date().toISOString();
38
+ fs.appendFileSync(LOG_FILE, `[${ts}] ${msg}\n`);
39
+ } catch {
40
+ // Silent — never crash
41
+ }
42
+ }
43
+
44
+ // --- CLI helpers ---
45
+
46
+ function cleoObserve(text, title) {
47
+ try {
48
+ const args = ['memory', 'observe', text];
49
+ if (title) {
50
+ args.push('--title', title);
51
+ }
52
+ execFileSync(CLEO_BIN, args, {
53
+ timeout: 10000,
54
+ stdio: 'ignore',
55
+ cwd: process.env.CLEO_PROJECT_DIR || process.cwd(),
56
+ });
57
+ return true;
58
+ } catch (err) {
59
+ log(`cleoObserve failed: ${err.message}`);
60
+ return false;
61
+ }
62
+ }
63
+
64
+ // --- Observation summarizer ---
65
+
66
+ function summarizeTool(toolName, toolInput) {
67
+ const inp = toolInput || {};
68
+ switch (toolName) {
69
+ case 'Bash':
70
+ return `Ran: ${String(inp.command || '').slice(0, 120)}`;
71
+ case 'Write':
72
+ return `Wrote: ${inp.file_path || inp.path || 'unknown'}`;
73
+ case 'Edit':
74
+ return `Edited: ${inp.file_path || inp.path || 'unknown'}`;
75
+ case 'Read':
76
+ return `Read: ${inp.file_path || inp.path || 'unknown'}`;
77
+ case 'Glob':
78
+ return `Glob: ${String(inp.pattern || '').slice(0, 80)}`;
79
+ case 'Grep':
80
+ return `Grep: ${String(inp.pattern || '').slice(0, 60)} in ${String(inp.path || '.').slice(0, 60)}`;
81
+ case 'Agent':
82
+ return `Spawned agent: ${String(inp.prompt || inp.description || '').slice(0, 80)}`;
83
+ case 'WebFetch':
84
+ return `Fetched: ${String(inp.url || '').slice(0, 120)}`;
85
+ case 'WebSearch':
86
+ return `Searched: ${String(inp.query || '').slice(0, 80)}`;
87
+ default:
88
+ return `${toolName} called`;
89
+ }
90
+ }
91
+
92
+ function shouldSkip(toolName) {
93
+ if (SKIP_TOOLS.has(toolName)) return true;
94
+ for (const prefix of SKIP_PREFIXES) {
95
+ if (toolName.startsWith(prefix)) return true;
96
+ }
97
+ return false;
98
+ }
99
+
100
+ // --- Event handlers ---
101
+
102
+ function handleObservation(data) {
103
+ const toolName = data.tool_name || 'unknown';
104
+ if (shouldSkip(toolName)) {
105
+ log(`Skipped noisy tool: ${toolName}`);
106
+ return;
107
+ }
108
+ const summary = summarizeTool(toolName, data.tool_input);
109
+ const title = `[hook] ${toolName}`;
110
+ log(`Observing: ${title} — ${summary}`);
111
+ cleoObserve(summary, title);
112
+ }
113
+
114
+ function handleSummarize(_data) {
115
+ log('Generating session summary...');
116
+ // Best-effort session summary: capture what we can from cleo
117
+ let sessionInfo = 'Claude Code session ended';
118
+ try {
119
+ const raw = execFileSync(CLEO_BIN, ['session', 'status', '--json'], {
120
+ timeout: 10000,
121
+ encoding: 'utf8',
122
+ cwd: process.env.CLEO_PROJECT_DIR || process.cwd(),
123
+ });
124
+ const parsed = JSON.parse(raw);
125
+ const s = parsed?.result?.session || parsed?.session || {};
126
+ if (s.scope || s.currentTask) {
127
+ sessionInfo = `Session ended: ${s.scope || 'unknown'} scope, task: ${s.currentTask || 'none'}`;
128
+ }
129
+ } catch {
130
+ // Use default
131
+ }
132
+ cleoObserve(sessionInfo, '[hook] session-end');
133
+ }
134
+
135
+ function handleSessionInit(_data) {
136
+ log('Session init received (no-op).');
137
+ // No-op for now — placeholder for future context injection
138
+ }
139
+
140
+ // --- HTTP Server ---
141
+
142
+ function createServer() {
143
+ return http.createServer((req, res) => {
144
+ if (req.method === 'POST' && req.url === '/hook') {
145
+ let body = '';
146
+ req.on('data', (chunk) => { body += chunk; });
147
+ req.on('end', () => {
148
+ res.writeHead(200, { 'Content-Type': 'application/json' });
149
+ res.end('{"ok":true}');
150
+
151
+ // Process async after responding
152
+ try {
153
+ const payload = JSON.parse(body);
154
+ const event = payload.event;
155
+ const data = typeof payload.data === 'string' ? JSON.parse(payload.data) : (payload.data || {});
156
+
157
+ switch (event) {
158
+ case 'observation':
159
+ handleObservation(data);
160
+ break;
161
+ case 'summarize':
162
+ handleSummarize(data);
163
+ break;
164
+ case 'session-init':
165
+ handleSessionInit(data);
166
+ break;
167
+ default:
168
+ log(`Unknown event: ${event}`);
169
+ }
170
+ } catch (err) {
171
+ log(`Error processing hook: ${err.message}`);
172
+ }
173
+ });
174
+ } else if (req.method === 'GET' && req.url === '/health') {
175
+ res.writeHead(200, { 'Content-Type': 'application/json' });
176
+ res.end(JSON.stringify({ ok: true, pid: process.pid, uptime: process.uptime() }));
177
+ } else {
178
+ res.writeHead(404);
179
+ res.end('Not found');
180
+ }
181
+ });
182
+ }
183
+
184
+ // --- PID management ---
185
+
186
+ function readPid() {
187
+ try {
188
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim(), 10);
189
+ if (isNaN(pid)) return null;
190
+ return pid;
191
+ } catch {
192
+ return null;
193
+ }
194
+ }
195
+
196
+ function isRunning(pid) {
197
+ try {
198
+ process.kill(pid, 0);
199
+ return true;
200
+ } catch {
201
+ return false;
202
+ }
203
+ }
204
+
205
+ function writePid(pid) {
206
+ const dir = path.dirname(PID_FILE);
207
+ if (!fs.existsSync(dir)) {
208
+ fs.mkdirSync(dir, { recursive: true });
209
+ }
210
+ fs.writeFileSync(PID_FILE, String(pid));
211
+ }
212
+
213
+ function removePid() {
214
+ try {
215
+ fs.unlinkSync(PID_FILE);
216
+ } catch {
217
+ // Ignore
218
+ }
219
+ }
220
+
221
+ // --- Commands ---
222
+
223
+ function startCommand() {
224
+ // Check if already running
225
+ const existingPid = readPid();
226
+ if (existingPid && isRunning(existingPid)) {
227
+ console.log(`Brain worker already running (PID ${existingPid})`);
228
+ process.exit(0);
229
+ }
230
+
231
+ // Also check if port is already in use
232
+ const net = require('node:net');
233
+ const probe = net.createServer();
234
+ probe.once('error', (err) => {
235
+ if (err.code === 'EADDRINUSE') {
236
+ console.log(`Port ${PORT} already in use — worker likely running`);
237
+ process.exit(0);
238
+ }
239
+ });
240
+ probe.once('listening', () => {
241
+ probe.close(() => {
242
+ // Port free — daemonize
243
+ daemonize();
244
+ });
245
+ });
246
+ probe.listen(PORT, '127.0.0.1');
247
+ }
248
+
249
+ function daemonize() {
250
+ // Fork a detached child that runs the server
251
+ const child = fork(__filename, ['--serve'], {
252
+ detached: true,
253
+ stdio: 'ignore',
254
+ env: { ...process.env, CLEO_PROJECT_DIR: process.cwd() },
255
+ });
256
+ child.unref();
257
+ writePid(child.pid);
258
+ console.log(`Brain worker started (PID ${child.pid}, port ${PORT})`);
259
+ process.exit(0);
260
+ }
261
+
262
+ function serveCommand() {
263
+ // This runs in the daemonized child
264
+ writePid(process.pid);
265
+ log(`Brain worker starting on port ${PORT} (PID ${process.pid})`);
266
+
267
+ const server = createServer();
268
+ server.listen(PORT, '127.0.0.1', () => {
269
+ log(`Brain worker listening on 127.0.0.1:${PORT}`);
270
+ });
271
+
272
+ server.on('error', (err) => {
273
+ log(`Server error: ${err.message}`);
274
+ removePid();
275
+ process.exit(1);
276
+ });
277
+
278
+ // Graceful shutdown
279
+ const shutdown = () => {
280
+ log('Brain worker shutting down...');
281
+ server.close();
282
+ removePid();
283
+ process.exit(0);
284
+ };
285
+ process.on('SIGTERM', shutdown);
286
+ process.on('SIGINT', shutdown);
287
+ }
288
+
289
+ function stopCommand() {
290
+ const pid = readPid();
291
+ if (!pid) {
292
+ console.log('Brain worker not running (no PID file)');
293
+ process.exit(0);
294
+ }
295
+ if (!isRunning(pid)) {
296
+ console.log(`Brain worker not running (stale PID ${pid})`);
297
+ removePid();
298
+ process.exit(0);
299
+ }
300
+ try {
301
+ process.kill(pid, 'SIGTERM');
302
+ console.log(`Brain worker stopped (PID ${pid})`);
303
+ } catch (err) {
304
+ console.log(`Failed to stop brain worker: ${err.message}`);
305
+ }
306
+ removePid();
307
+ }
308
+
309
+ function statusCommand() {
310
+ const pid = readPid();
311
+ if (pid && isRunning(pid)) {
312
+ console.log(`Brain worker running (PID ${pid}, port ${PORT})`);
313
+ } else {
314
+ console.log('Brain worker not running');
315
+ if (pid) removePid();
316
+ }
317
+ }
318
+
319
+ // --- Main ---
320
+
321
+ const command = process.argv[2];
322
+ switch (command) {
323
+ case 'start':
324
+ startCommand();
325
+ break;
326
+ case '--serve':
327
+ serveCommand();
328
+ break;
329
+ case 'stop':
330
+ stopCommand();
331
+ break;
332
+ case 'status':
333
+ statusCommand();
334
+ break;
335
+ default:
336
+ console.log('Usage: brain-worker.js <start|stop|status>');
337
+ process.exit(1);
338
+ }
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env bash
2
+ # PostToolUse: capture tool observations into CLEO brain.db
3
+ # Best-effort — always exits 0 so it never blocks Claude Code
4
+ set -euo pipefail
5
+
6
+ CLEO_BIN="${HOME}/.cleo/bin/cleo"
7
+ [ ! -x "$CLEO_BIN" ] && exit 0
8
+ [ ! -d ".cleo" ] && exit 0
9
+
10
+ # Read stdin (tool use JSON from Claude Code)
11
+ INPUT=$(cat) || exit 0
12
+ TOOL=$(echo "$INPUT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('tool_name','unknown'))" 2>/dev/null || echo "unknown")
13
+
14
+ # Only observe meaningful tools (skip trivial ones)
15
+ case "$TOOL" in
16
+ Read|Write|Edit|Bash|Glob|Grep|Agent) ;;
17
+ *) exit 0 ;;
18
+ esac
19
+
20
+ # Extract a short summary
21
+ SUMMARY=$(echo "$INPUT" | python3 -c "
22
+ import json, sys
23
+ d = json.load(sys.stdin)
24
+ tool = d.get('tool_name', 'unknown')
25
+ inp = d.get('tool_input', {})
26
+ if tool == 'Bash':
27
+ print(f'Bash: {str(inp.get(\"command\",\"\"))[:80]}')
28
+ elif tool in ('Read','Write','Edit'):
29
+ print(f'{tool}: {inp.get(\"file_path\",inp.get(\"path\",\"\"))[:80]}')
30
+ elif tool == 'Grep':
31
+ print(f'Grep: {inp.get(\"pattern\",\"\")[:40]} in {inp.get(\"path\",\".\")[:40]}')
32
+ elif tool == 'Glob':
33
+ print(f'Glob: {inp.get(\"pattern\",\"\")[:60]}')
34
+ elif tool == 'Agent':
35
+ print(f'Agent: {str(inp.get(\"prompt\",\"\"))[:80]}')
36
+ else:
37
+ print(f'{tool} called')
38
+ " 2>/dev/null || echo "$TOOL called")
39
+
40
+ "$CLEO_BIN" memory observe "$SUMMARY" --title "[hook] $TOOL" >/dev/null 2>&1 || true
41
+ exit 0
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env bash
2
+ # SessionStart hook: Auto-bind CLEO session to Claude Code terminal
3
+ # Part of CLEO universal subagent architecture (v0.70.0+)
4
+
5
+ set -euo pipefail
6
+
7
+ # Find cleo installation
8
+ CLEO_DIR="${HOME}/.cleo"
9
+ CLEO_BIN="${CLEO_DIR}/cleo"
10
+
11
+ # Only proceed if cleo is installed
12
+ if [[ ! -x "$CLEO_BIN" ]]; then
13
+ exit 0
14
+ fi
15
+
16
+ # Check if .cleo project directory exists
17
+ if [[ ! -d ".cleo" ]]; then
18
+ exit 0
19
+ fi
20
+
21
+ # Check if there's a current session
22
+ CURRENT_SESSION_FILE=".cleo/.current-session"
23
+ if [[ ! -f "$CURRENT_SESSION_FILE" ]]; then
24
+ exit 0
25
+ fi
26
+
27
+ # Read current session ID
28
+ SESSION_ID=$(cat "$CURRENT_SESSION_FILE" 2>/dev/null || echo "")
29
+ if [[ -z "$SESSION_ID" ]]; then
30
+ exit 0
31
+ fi
32
+
33
+ # Verify session exists and is active
34
+ SESSION_STATUS=$("$CLEO_BIN" session status --session "$SESSION_ID" --format json 2>/dev/null || echo "{}")
35
+ IS_ACTIVE=$(echo "$SESSION_STATUS" | jq -r '.session.status // "unknown"' 2>/dev/null || echo "unknown")
36
+
37
+ if [[ "$IS_ACTIVE" == "active" ]]; then
38
+ # Export session to environment for all subsequent commands
39
+ export CLEO_SESSION="$SESSION_ID"
40
+
41
+ # Write to a file that can be sourced by the shell
42
+ echo "export CLEO_SESSION=\"$SESSION_ID\"" > ".cleo/.session-env"
43
+
44
+ # Optional: Display session info (visible in terminal)
45
+ echo "✓ CLEO session bound: $SESSION_ID" >&2
46
+ fi
47
+
48
+ exit 0
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env bash
2
+ # Stop: save session summary to CLEO brain
3
+ # Best-effort — always exits 0 so it never blocks Claude Code
4
+ set -euo pipefail
5
+
6
+ CLEO_BIN="${HOME}/.cleo/bin/cleo"
7
+ [ ! -x "$CLEO_BIN" ] && exit 0
8
+ [ ! -d ".cleo" ] && exit 0
9
+
10
+ # Get current session info if available
11
+ SESSION_INFO=$("$CLEO_BIN" session status --json 2>/dev/null | python3 -c "
12
+ import json, sys
13
+ d = json.load(sys.stdin)
14
+ r = d.get('result', {})
15
+ s = r.get('session', {})
16
+ if s:
17
+ print(f'Session ended: {s.get(\"scope\",\"unknown\")} scope, task: {s.get(\"currentTask\",\"none\")}')
18
+ else:
19
+ print('Session ended')
20
+ " 2>/dev/null || echo "Claude Code session ended")
21
+
22
+ "$CLEO_BIN" memory observe "$SESSION_INFO" --title "[hook] session-end" >/dev/null 2>&1 || true
23
+ exit 0
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "cleo",
3
+ "version": "0.70.1",
4
+ "description": "CLEO task management system with custom agents for LLM-driven development workflows",
5
+ "author": {
6
+ "name": "CLEO",
7
+ "url": "https://github.com/kryptobaseddev/cleo"
8
+ },
9
+ "capabilities": {
10
+ "task_management": true,
11
+ "multi_session": true,
12
+ "orchestration": true
13
+ },
14
+ "hooks": {
15
+ "enabled": true,
16
+ "directory": "hooks",
17
+ "manifest": "hooks/hooks.json"
18
+ },
19
+ "metadata": {
20
+ "schema_version": "1.0.0",
21
+ "min_claude_version": "1.0.0",
22
+ "license": "MIT"
23
+ }
24
+ }