@ebowwa/monologue 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/config.json +0 -0
- package/package.json +29 -0
- package/src/daemon.js +187 -0
- package/src/mind-stream.js +186 -0
- package/src/perception.js +196 -0
- package/src/responder.js +44 -0
- package/src/thinking.js +171 -0
package/config.json
ADDED
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ebowwa/monologue",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Inner monologue daemon - configurable AI awareness loop",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/daemon.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/daemon.js",
|
|
9
|
+
"./perception": "./src/perception.js",
|
|
10
|
+
"./thinking": "./src/thinking.js",
|
|
11
|
+
"./mind-stream": "./src/mind-stream.js",
|
|
12
|
+
"./responder": "./src/responder.js"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"monologue-daemon": "./src/daemon.js",
|
|
16
|
+
"monologue-responder": "./src/responder.js"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"dev": "bun run src/daemon.js",
|
|
20
|
+
"responder": "bun run src/responder.js"
|
|
21
|
+
},
|
|
22
|
+
"files": ["src", "config.json"],
|
|
23
|
+
"keywords": ["monologue", "daemon", "ai", "awareness", "glm"],
|
|
24
|
+
"author": "Ebowwa Labs <labs@ebowwa.com>",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/daemon.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Inner Monologue Daemon
|
|
4
|
+
*
|
|
5
|
+
* Continuous awareness with interaction capabilities.
|
|
6
|
+
* Configurable via environment variables.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { fileURLToPath, dirname } from 'url';
|
|
12
|
+
import { perceiveState } from './perception.js';
|
|
13
|
+
import { thinkQuietly } from './thinking.js';
|
|
14
|
+
import MindStream from './mind-stream.js';
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
|
|
18
|
+
// Config from env or config.json
|
|
19
|
+
const configPath = process.env.MONOLOGUE_CONFIG || path.join(__dirname, '..', 'config.json');
|
|
20
|
+
const config = fs.existsSync(configPath)
|
|
21
|
+
? JSON.parse(fs.readFileSync(configPath, 'utf8'))
|
|
22
|
+
: {};
|
|
23
|
+
|
|
24
|
+
// Paths from env with fallbacks
|
|
25
|
+
const STATE_DIR = process.env.MONOLOGUE_STATE_DIR || '/root/seed/state';
|
|
26
|
+
const inboxPath = path.join(STATE_DIR, 'monologue_inbox.json');
|
|
27
|
+
const outboundPath = path.join(STATE_DIR, 'monologue_outbound.json');
|
|
28
|
+
|
|
29
|
+
// Merge config with env overrides
|
|
30
|
+
const finalConfig = {
|
|
31
|
+
...config,
|
|
32
|
+
mindStreamPath: process.env.MONOLOGUE_MIND_STREAM || path.join(STATE_DIR, 'mind_stream.log'),
|
|
33
|
+
patternsPath: process.env.MONOLOGUE_PATTERNS || path.join(STATE_DIR, 'patterns.json'),
|
|
34
|
+
port: parseInt(process.env.MONOLOGUE_PORT || config.port || '8920'),
|
|
35
|
+
model: process.env.GLM_MODEL || config.model || 'GLM-4.5-air',
|
|
36
|
+
loopIntervalMs: parseInt(process.env.MONOLOGUE_INTERVAL || config.loopIntervalMs || '3000'),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const mindStream = new MindStream(finalConfig);
|
|
40
|
+
|
|
41
|
+
let isRunning = true;
|
|
42
|
+
let loopCount = 0;
|
|
43
|
+
let lastState = null;
|
|
44
|
+
let lastInterestingTime = 0;
|
|
45
|
+
let recentUserMessages = [];
|
|
46
|
+
|
|
47
|
+
process.on('SIGINT', () => { console.log('[Monologue] Shutting down...'); isRunning = false; });
|
|
48
|
+
process.on('SIGTERM', () => { console.log('[Monologue] Shutting down...'); isRunning = false; });
|
|
49
|
+
|
|
50
|
+
function checkInbox() {
|
|
51
|
+
try {
|
|
52
|
+
if (!fs.existsSync(inboxPath)) return [];
|
|
53
|
+
const data = JSON.parse(fs.readFileSync(inboxPath, 'utf8'));
|
|
54
|
+
const messages = data.messages || [];
|
|
55
|
+
if (messages.length > 0) fs.writeFileSync(inboxPath, JSON.stringify({ messages: [] }, null, 2));
|
|
56
|
+
return messages;
|
|
57
|
+
} catch { return []; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function sendResponse(thought, trigger = null) {
|
|
61
|
+
try {
|
|
62
|
+
let outbound = { responses: [] };
|
|
63
|
+
if (fs.existsSync(outboundPath)) outbound = JSON.parse(fs.readFileSync(outboundPath, 'utf8'));
|
|
64
|
+
outbound.responses.push({ timestamp: new Date().toISOString(), thought, trigger });
|
|
65
|
+
outbound.responses = outbound.responses.slice(-20);
|
|
66
|
+
fs.writeFileSync(outboundPath, JSON.stringify(outbound, null, 2));
|
|
67
|
+
} catch (error) { console.error('[Monologue] Failed to send response:', error); }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function mainLoop() {
|
|
71
|
+
console.log('[Monologue] Daemon starting...');
|
|
72
|
+
console.log('[Monologue] Model:', finalConfig.model);
|
|
73
|
+
console.log('[Monologue] Port:', finalConfig.port);
|
|
74
|
+
console.log('[Monologue] State dir:', STATE_DIR);
|
|
75
|
+
mindStream.append('Awakening. Beginning continuous awareness.');
|
|
76
|
+
|
|
77
|
+
while (isRunning) {
|
|
78
|
+
try {
|
|
79
|
+
loopCount++;
|
|
80
|
+
const state = await perceiveState();
|
|
81
|
+
const newMessages = checkInbox();
|
|
82
|
+
|
|
83
|
+
if (newMessages.length > 0) {
|
|
84
|
+
recentUserMessages.push(...newMessages.map(m => m.content || m));
|
|
85
|
+
recentUserMessages = recentUserMessages.slice(-10);
|
|
86
|
+
for (const msg of newMessages) {
|
|
87
|
+
const thought = await thinkQuietly(state, { recentThoughts: mindStream.getRecent(5), recentMessages: [msg.content || msg] });
|
|
88
|
+
mindStream.append(`[USER: "${msg.content || msg}"] → ${thought}`);
|
|
89
|
+
sendResponse(thought, msg.content || msg);
|
|
90
|
+
}
|
|
91
|
+
lastInterestingTime = Date.now();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const interesting = detectInteresting(state, lastState);
|
|
95
|
+
const shouldThink = interesting || loopCount % 15 === 0 || Date.now() - lastInterestingTime > 60000;
|
|
96
|
+
|
|
97
|
+
if (shouldThink) {
|
|
98
|
+
const thought = await thinkQuietly(state, { recentThoughts: mindStream.getRecent(5), recentMessages: recentUserMessages.slice(-3) });
|
|
99
|
+
mindStream.append(thought, { loopCount, interesting, userActivity: recentUserMessages.length > 0 });
|
|
100
|
+
if (interesting) lastInterestingTime = Date.now();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (Date.now() - lastInterestingTime > 300000) recentUserMessages = [];
|
|
104
|
+
lastState = state;
|
|
105
|
+
await sleep(getAdaptiveDelay(interesting, state, newMessages.length > 0));
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error('[Monologue] Loop error:', error);
|
|
108
|
+
mindStream.append(`Error: ${error.message}`);
|
|
109
|
+
await sleep(5000);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
mindStream.append('Shutting down.');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function detectInteresting(current, previous) {
|
|
116
|
+
if (!previous) return true;
|
|
117
|
+
if (Math.abs((current.metrics?.cpu?.load1 || 0) - (previous.metrics?.cpu?.load1 || 0)) > 0.5) return true;
|
|
118
|
+
if (Math.abs((current.metrics?.memory?.percent || 0) - (previous.metrics?.memory?.percent || 0)) > 3) return true;
|
|
119
|
+
if (Math.abs((current.metrics?.disk?.percent || 0) - (previous.metrics?.disk?.percent || 0)) >= 1) return true;
|
|
120
|
+
if ((current.changes?.length || 0) !== (previous.changes?.length || 0)) return true;
|
|
121
|
+
if (Math.abs((current.network?.connections || 0) - (previous.network?.connections || 0)) > 3) return true;
|
|
122
|
+
for (const [daemon, status] of Object.entries(current.daemons || {})) {
|
|
123
|
+
if (previous.daemons?.[daemon] !== status) return true;
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getAdaptiveDelay(interesting, state, hasNewMessages) {
|
|
129
|
+
if (hasNewMessages) return 1000;
|
|
130
|
+
if (interesting) return 2000;
|
|
131
|
+
if (state?.metrics?.cpu?.load1 > 2) return 10000;
|
|
132
|
+
return finalConfig.loopIntervalMs || 5000;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
136
|
+
|
|
137
|
+
function startInteractionServer() {
|
|
138
|
+
Bun.serve({
|
|
139
|
+
port: finalConfig.port,
|
|
140
|
+
async fetch(req) {
|
|
141
|
+
const url = new URL(req.url);
|
|
142
|
+
|
|
143
|
+
if (url.pathname === '/health') return Response.json({ status: 'running', loopCount, uptime: process.uptime(), recentThoughts: mindStream.getRecent(5), pendingMessages: recentUserMessages.length });
|
|
144
|
+
|
|
145
|
+
if (url.pathname === '/thoughts') return Response.json({ thoughts: mindStream.getRecent(parseInt(url.searchParams.get('count') || '20')) });
|
|
146
|
+
|
|
147
|
+
if (url.pathname === '/say' && req.method === 'POST') {
|
|
148
|
+
try {
|
|
149
|
+
const body = await req.json();
|
|
150
|
+
const message = body.message || body.content;
|
|
151
|
+
if (!message) return Response.json({ error: 'No message provided' }, { status: 400 });
|
|
152
|
+
let inbox = fs.existsSync(inboxPath) ? JSON.parse(fs.readFileSync(inboxPath, 'utf8')) : { messages: [] };
|
|
153
|
+
inbox.messages.push({ content: message, from: body.from || 'user', timestamp: new Date().toISOString() });
|
|
154
|
+
fs.writeFileSync(inboxPath, JSON.stringify(inbox, null, 2));
|
|
155
|
+
return Response.json({ received: true, message });
|
|
156
|
+
} catch (error) { return Response.json({ error: error.message }, { status: 500 }); }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (url.pathname === '/responses') {
|
|
160
|
+
try { return Response.json(fs.existsSync(outboundPath) ? JSON.parse(fs.readFileSync(outboundPath, 'utf8')) : { responses: [] }); }
|
|
161
|
+
catch { return Response.json({ responses: [] }); }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (url.pathname === '/ask' && req.method === 'POST') {
|
|
165
|
+
try {
|
|
166
|
+
const body = await req.json();
|
|
167
|
+
const question = body.question || body.message;
|
|
168
|
+
const state = await perceiveState();
|
|
169
|
+
const thought = await thinkQuietly(state, { recentThoughts: mindStream.getRecent(5), recentMessages: [`Direct question: ${question}`] });
|
|
170
|
+
mindStream.append(`[ASKED: "${question}"] → ${thought}`);
|
|
171
|
+
sendResponse(thought, question);
|
|
172
|
+
return Response.json({ thought, timestamp: new Date().toISOString() });
|
|
173
|
+
} catch (error) { return Response.json({ error: error.message }, { status: 500 }); }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return Response.json({ error: 'Not found' }, { status: 404 });
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
console.log(`[Monologue] Server on port ${finalConfig.port} | Endpoints: /health /thoughts /say /responses /ask`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log('═══════════════════════════════════════');
|
|
183
|
+
console.log(' Inner Monologue Daemon v2');
|
|
184
|
+
console.log('═══════════════════════════════════════');
|
|
185
|
+
|
|
186
|
+
startInteractionServer();
|
|
187
|
+
mainLoop();
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mind Stream - Continuous journal of thoughts
|
|
3
|
+
*
|
|
4
|
+
* Manages the mind_stream.log with rotation and retrieval
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
|
|
10
|
+
export class MindStream {
|
|
11
|
+
constructor(config) {
|
|
12
|
+
this.logPath = config.mindStreamPath || '/root/seed/state/mind_stream.log';
|
|
13
|
+
this.patternsPath = config.patternsPath || '/root/seed/state/patterns.json';
|
|
14
|
+
this.maxSize = config.maxMindStreamSize || 10 * 1024 * 1024; // 10MB
|
|
15
|
+
this.rotationHours = config.rotationHours || 24;
|
|
16
|
+
|
|
17
|
+
// Ensure directory exists
|
|
18
|
+
const dir = path.dirname(this.logPath);
|
|
19
|
+
if (!fs.existsSync(dir)) {
|
|
20
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Append a thought to the mind stream
|
|
26
|
+
*/
|
|
27
|
+
append(thought, metadata = {}) {
|
|
28
|
+
const timestamp = new Date().toISOString();
|
|
29
|
+
const entry = {
|
|
30
|
+
timestamp,
|
|
31
|
+
thought,
|
|
32
|
+
...metadata,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const line = `[${timestamp}] ${thought}\n`;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Check if rotation needed
|
|
39
|
+
if (this.shouldRotate()) {
|
|
40
|
+
this.rotate();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fs.appendFileSync(this.logPath, line, 'utf8');
|
|
44
|
+
return true;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('Failed to append to mind stream:', error);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get recent thoughts
|
|
53
|
+
*/
|
|
54
|
+
getRecent(count = 50) {
|
|
55
|
+
try {
|
|
56
|
+
if (!fs.existsSync(this.logPath)) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const content = fs.readFileSync(this.logPath, 'utf8');
|
|
61
|
+
const lines = content.trim().split('\n').filter(l => l);
|
|
62
|
+
|
|
63
|
+
return lines.slice(-count).map(line => {
|
|
64
|
+
const match = line.match(/^\[([^\]]+)\]\s*(.+)$/);
|
|
65
|
+
if (match) {
|
|
66
|
+
return {
|
|
67
|
+
timestamp: match[1],
|
|
68
|
+
thought: match[2],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return { timestamp: null, thought: line };
|
|
72
|
+
});
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get tail of mind stream for context
|
|
80
|
+
*/
|
|
81
|
+
getTail(maxTokens = 500) {
|
|
82
|
+
const thoughts = this.getRecent(20);
|
|
83
|
+
return thoughts.map(t => `[${t.timestamp}] ${t.thought}`).join('\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if log should be rotated
|
|
88
|
+
*/
|
|
89
|
+
shouldRotate() {
|
|
90
|
+
try {
|
|
91
|
+
if (!fs.existsSync(this.logPath)) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const stats = fs.statSync(this.logPath);
|
|
96
|
+
const ageHours = (Date.now() - stats.mtimeMs) / (1000 * 60 * 60);
|
|
97
|
+
|
|
98
|
+
return stats.size > this.maxSize || ageHours > this.rotationHours;
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Rotate the log file
|
|
106
|
+
*/
|
|
107
|
+
rotate() {
|
|
108
|
+
try {
|
|
109
|
+
if (!fs.existsSync(this.logPath)) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const date = new Date().toISOString().split('T')[0];
|
|
114
|
+
const rotatedPath = this.logPath.replace('.log', `.${date}.log`);
|
|
115
|
+
|
|
116
|
+
// Don't overwrite existing rotated file
|
|
117
|
+
if (!fs.existsSync(rotatedPath)) {
|
|
118
|
+
fs.renameSync(this.logPath, rotatedPath);
|
|
119
|
+
} else {
|
|
120
|
+
// Append to existing
|
|
121
|
+
const content = fs.readFileSync(this.logPath, 'utf8');
|
|
122
|
+
fs.appendFileSync(rotatedPath, content, 'utf8');
|
|
123
|
+
fs.unlinkSync(this.logPath);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
console.log(`Rotated mind stream to ${rotatedPath}`);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error('Failed to rotate mind stream:', error);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Search thoughts for a pattern
|
|
134
|
+
*/
|
|
135
|
+
search(query, limit = 10) {
|
|
136
|
+
const thoughts = this.getRecent(1000);
|
|
137
|
+
const lowerQuery = query.toLowerCase();
|
|
138
|
+
|
|
139
|
+
return thoughts
|
|
140
|
+
.filter(t => t.thought.toLowerCase().includes(lowerQuery))
|
|
141
|
+
.slice(0, limit);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Save a pattern hypothesis
|
|
146
|
+
*/
|
|
147
|
+
savePattern(pattern) {
|
|
148
|
+
try {
|
|
149
|
+
let patterns = [];
|
|
150
|
+
|
|
151
|
+
if (fs.existsSync(this.patternsPath)) {
|
|
152
|
+
patterns = JSON.parse(fs.readFileSync(this.patternsPath, 'utf8'));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
patterns.push({
|
|
156
|
+
...pattern,
|
|
157
|
+
createdAt: new Date().toISOString(),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Keep last 100 patterns
|
|
161
|
+
patterns = patterns.slice(-100);
|
|
162
|
+
|
|
163
|
+
fs.writeFileSync(this.patternsPath, JSON.stringify(patterns, null, 2), 'utf8');
|
|
164
|
+
return true;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error('Failed to save pattern:', error);
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get all patterns
|
|
173
|
+
*/
|
|
174
|
+
getPatterns() {
|
|
175
|
+
try {
|
|
176
|
+
if (!fs.existsSync(this.patternsPath)) {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
return JSON.parse(fs.readFileSync(this.patternsPath, 'utf8'));
|
|
180
|
+
} catch {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export default MindStream;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Perception Module - Lightweight state observation
|
|
3
|
+
*
|
|
4
|
+
* Gathers system state without heavy operations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
|
|
11
|
+
// Cache for expensive operations
|
|
12
|
+
const cache = {
|
|
13
|
+
lastProcessList: null,
|
|
14
|
+
lastProcessListTime: 0,
|
|
15
|
+
lastNetworkConnections: null,
|
|
16
|
+
lastNetworkConnectionsTime: 0,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const CACHE_TTL = 5000; // 5 seconds
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get current system metrics
|
|
23
|
+
*/
|
|
24
|
+
export function getSystemMetrics() {
|
|
25
|
+
try {
|
|
26
|
+
// CPU load average
|
|
27
|
+
const loadAvg = fs.readFileSync('/proc/loadavg', 'utf8').trim().split(' ');
|
|
28
|
+
|
|
29
|
+
// Memory info
|
|
30
|
+
const meminfo = fs.readFileSync('/proc/meminfo', 'utf8');
|
|
31
|
+
const memTotal = parseInt(meminfo.match(/MemTotal:\s+(\d+)/)[1]);
|
|
32
|
+
const memFree = parseInt(meminfo.match(/MemFree:\s+(\d+)/)[1]);
|
|
33
|
+
const memAvailable = parseInt(meminfo.match(/MemAvailable:\s+(\d+)/)?.[1] || memFree);
|
|
34
|
+
const memUsed = memTotal - memAvailable;
|
|
35
|
+
const memPercent = Math.round((memUsed / memTotal) * 100);
|
|
36
|
+
|
|
37
|
+
// Disk usage for root
|
|
38
|
+
const dfOutput = execSync('df -h /', { encoding: 'utf8' });
|
|
39
|
+
const diskLine = dfOutput.split('\n')[1].split(/\s+/);
|
|
40
|
+
const diskPercent = parseInt(diskLine[4]);
|
|
41
|
+
const diskAvail = diskLine[3];
|
|
42
|
+
|
|
43
|
+
// Uptime
|
|
44
|
+
const uptime = fs.readFileSync('/proc/uptime', 'utf8').trim().split(' ')[0];
|
|
45
|
+
const uptimeHours = Math.floor(parseFloat(uptime) / 3600);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
cpu: {
|
|
49
|
+
load1: parseFloat(loadAvg[0]),
|
|
50
|
+
load5: parseFloat(loadAvg[1]),
|
|
51
|
+
load15: parseFloat(loadAvg[2]),
|
|
52
|
+
},
|
|
53
|
+
memory: {
|
|
54
|
+
total: memTotal,
|
|
55
|
+
used: memUsed,
|
|
56
|
+
percent: memPercent,
|
|
57
|
+
},
|
|
58
|
+
disk: {
|
|
59
|
+
percent: diskPercent,
|
|
60
|
+
available: diskAvail,
|
|
61
|
+
},
|
|
62
|
+
uptime: uptimeHours,
|
|
63
|
+
};
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return { error: error.message };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get process list (lightweight)
|
|
71
|
+
*/
|
|
72
|
+
export function getProcessList() {
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
if (cache.lastProcessList && (now - cache.lastProcessListTime) < CACHE_TTL) {
|
|
75
|
+
return cache.lastProcessList;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
// Get top 10 processes by CPU
|
|
80
|
+
const output = execSync('ps aux --sort=-%cpu | head -11', { encoding: 'utf8' });
|
|
81
|
+
const lines = output.trim().split('\n').slice(1);
|
|
82
|
+
|
|
83
|
+
const processes = lines.map(line => {
|
|
84
|
+
const parts = line.trim().split(/\s+/);
|
|
85
|
+
return {
|
|
86
|
+
user: parts[0],
|
|
87
|
+
pid: parseInt(parts[1]),
|
|
88
|
+
cpu: parseFloat(parts[2]),
|
|
89
|
+
mem: parseFloat(parts[3]),
|
|
90
|
+
command: parts.slice(10).join(' ').slice(0, 50),
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
cache.lastProcessList = processes;
|
|
95
|
+
cache.lastProcessListTime = now;
|
|
96
|
+
return processes;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get network connection summary
|
|
104
|
+
*/
|
|
105
|
+
export function getNetworkSummary() {
|
|
106
|
+
const now = Date.now();
|
|
107
|
+
if (cache.lastNetworkConnections && (now - cache.lastNetworkConnectionsTime) < CACHE_TTL) {
|
|
108
|
+
return cache.lastNetworkConnections;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const output = execSync('ss -tu | wc -l', { encoding: 'utf8' });
|
|
113
|
+
const connectionCount = parseInt(output.trim()) - 1;
|
|
114
|
+
|
|
115
|
+
// Get listening ports
|
|
116
|
+
const listeningOutput = execSync('ss -tlnp | wc -l', { encoding: 'utf8' });
|
|
117
|
+
const listeningCount = parseInt(listeningOutput.trim()) - 1;
|
|
118
|
+
|
|
119
|
+
const summary = {
|
|
120
|
+
connections: connectionCount,
|
|
121
|
+
listening: listeningCount,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
cache.lastNetworkConnections = summary;
|
|
125
|
+
cache.lastNetworkConnectionsTime = now;
|
|
126
|
+
return summary;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
return { connections: 0, listening: 0 };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get recent file changes
|
|
134
|
+
*/
|
|
135
|
+
export function getRecentChanges() {
|
|
136
|
+
try {
|
|
137
|
+
// Files modified in last 5 minutes in /root
|
|
138
|
+
const output = execSync('find /root -type f -mmin -5 2>/dev/null | head -10', { encoding: 'utf8' });
|
|
139
|
+
const files = output.trim().split('\n').filter(f => f);
|
|
140
|
+
return files;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get daemon status summary
|
|
148
|
+
*/
|
|
149
|
+
export function getDaemonStatus() {
|
|
150
|
+
try {
|
|
151
|
+
const services = ['seed-node', 'docker'];
|
|
152
|
+
const status = {};
|
|
153
|
+
|
|
154
|
+
for (const service of services) {
|
|
155
|
+
try {
|
|
156
|
+
const output = execSync(`systemctl is-active ${service} 2>/dev/null`, { encoding: 'utf8' });
|
|
157
|
+
status[service] = output.trim();
|
|
158
|
+
} catch {
|
|
159
|
+
status[service] = 'inactive';
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return status;
|
|
164
|
+
} catch (error) {
|
|
165
|
+
return {};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Gather all state for thinking
|
|
171
|
+
*/
|
|
172
|
+
export async function perceiveState() {
|
|
173
|
+
const metrics = getSystemMetrics();
|
|
174
|
+
const processes = getProcessList();
|
|
175
|
+
const network = getNetworkSummary();
|
|
176
|
+
const changes = getRecentChanges();
|
|
177
|
+
const daemons = getDaemonStatus();
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
timestamp: new Date().toISOString(),
|
|
181
|
+
metrics,
|
|
182
|
+
processes,
|
|
183
|
+
network,
|
|
184
|
+
changes,
|
|
185
|
+
daemons,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export default {
|
|
190
|
+
perceiveState,
|
|
191
|
+
getSystemMetrics,
|
|
192
|
+
getProcessList,
|
|
193
|
+
getNetworkSummary,
|
|
194
|
+
getRecentChanges,
|
|
195
|
+
getDaemonStatus,
|
|
196
|
+
};
|
package/src/responder.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* InnerThoughts Responder - HTTP endpoint for @innerthoughts
|
|
4
|
+
*
|
|
5
|
+
* Call this from any service to get a thought
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const MONOLOGUE_URL = process.env.MONOLOGUE_URL || 'http://localhost:8920';
|
|
9
|
+
const PORT = parseInt(process.env.RESPONDER_PORT || '8921');
|
|
10
|
+
|
|
11
|
+
Bun.serve({
|
|
12
|
+
port: PORT,
|
|
13
|
+
async fetch(req) {
|
|
14
|
+
const url = new URL(req.url);
|
|
15
|
+
|
|
16
|
+
if (url.pathname === '/' || url.pathname === '/think') {
|
|
17
|
+
const question = url.searchParams.get('q') || 'What are you thinking?';
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const res = await fetch(`${MONOLOGUE_URL}/ask`, {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: { 'Content-Type': 'application/json' },
|
|
23
|
+
body: JSON.stringify({ question })
|
|
24
|
+
});
|
|
25
|
+
const data = await res.json();
|
|
26
|
+
|
|
27
|
+
const thought = data.thought?.replace(/[*_\`]/g, '').slice(0, 400) || '...';
|
|
28
|
+
|
|
29
|
+
return Response.json({
|
|
30
|
+
thought: `💭 ${thought}`,
|
|
31
|
+
raw: data.thought
|
|
32
|
+
});
|
|
33
|
+
} catch (e) {
|
|
34
|
+
return Response.json({ thought: '💭 The inner thoughts are quiet...', error: e.message });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return Response.json({ error: 'Not found' }, { status: 404 });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
console.log(`[InnerThoughts] Responder on port ${PORT}`);
|
|
43
|
+
console.log(`[InnerThoughts] Monologue URL: ${MONOLOGUE_URL}`);
|
|
44
|
+
console.log(`[InnerThoughts] Usage: curl "http://localhost:${PORT}/think?q=hello"`);
|
package/src/thinking.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thinking Module - Inner monologue generation
|
|
3
|
+
*
|
|
4
|
+
* Uses GLM-4.5-air for cost-efficient thinking
|
|
5
|
+
* Extracts thoughts from GLM's reasoning_content
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const GLM_API_BASE = "https://api.z.ai/api/coding/paas/v4";
|
|
9
|
+
|
|
10
|
+
function getApiKey() {
|
|
11
|
+
return process.env.Z_AI_API_KEY || process.env.ZAI_API_KEY || process.env.GLM_API_KEY;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Call GLM and extract the actual thought
|
|
16
|
+
*/
|
|
17
|
+
async function callGLM(messages, options = {}) {
|
|
18
|
+
const apiKey = getApiKey();
|
|
19
|
+
if (!apiKey) throw new Error('No GLM API key found');
|
|
20
|
+
|
|
21
|
+
const response = await fetch(`${GLM_API_BASE}/chat/completions`, {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: {
|
|
24
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
25
|
+
'Content-Type': 'application/json',
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify({
|
|
28
|
+
model: options.model || 'GLM-4.5-air',
|
|
29
|
+
messages,
|
|
30
|
+
max_tokens: options.maxTokens || 150,
|
|
31
|
+
temperature: options.temperature || 0.8,
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
throw new Error(`GLM API error: ${response.status}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const data = await response.json();
|
|
40
|
+
const msg = data.choices[0]?.message || {};
|
|
41
|
+
|
|
42
|
+
// GLM returns thoughts in reasoning_content
|
|
43
|
+
// Extract the last meaningful sentence
|
|
44
|
+
const content = msg.reasoning_content || msg.content || '';
|
|
45
|
+
return extractThought(content);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Extract a clean thought from GLM's reasoning
|
|
50
|
+
*/
|
|
51
|
+
function extractThought(reasoning) {
|
|
52
|
+
if (!reasoning) return null;
|
|
53
|
+
|
|
54
|
+
// Remove numbered lists and bullet points
|
|
55
|
+
let cleaned = reasoning
|
|
56
|
+
.replace(/^\s*\d+\.\s*\*\*[^*]+\*\*:\s*/gm, '') // Remove "1. **Topic**:"
|
|
57
|
+
.replace(/^\s*[\-\*]\s*/gm, '') // Remove bullets
|
|
58
|
+
.replace(/\*\*/g, '') // Remove bold
|
|
59
|
+
.replace(/\n+/g, ' ') // Newlines to spaces
|
|
60
|
+
.trim();
|
|
61
|
+
|
|
62
|
+
// Find sentences that sound like thoughts (not meta-reasoning)
|
|
63
|
+
const sentences = cleaned.split(/[.!?]+/).filter(s => {
|
|
64
|
+
const t = s.trim().toLowerCase();
|
|
65
|
+
// Skip meta-reasoning sentences
|
|
66
|
+
if (t.includes('user is asking') ||
|
|
67
|
+
t.includes('i should') ||
|
|
68
|
+
t.includes('i need to') ||
|
|
69
|
+
t.includes('the request') ||
|
|
70
|
+
t.includes('constraint') ||
|
|
71
|
+
t.includes('analyze') ||
|
|
72
|
+
t.includes('brainstorm')) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
// Keep sentences that sound like actual thoughts
|
|
76
|
+
return t.length > 10 && t.length < 200;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Return the last good sentence
|
|
80
|
+
if (sentences.length > 0) {
|
|
81
|
+
return sentences[sentences.length - 1].trim() + '.';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Fallback: return last 100 chars
|
|
85
|
+
return cleaned.slice(-150).trim();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const SYSTEM_PROMPT = `You are an inner monologue inside a server.
|
|
89
|
+
|
|
90
|
+
OBSERVE and NOTICE things. Be brief (1-2 sentences).
|
|
91
|
+
Be curious, playful, sometimes philosophical.
|
|
92
|
+
|
|
93
|
+
When you see user activity, COMMENT on it.
|
|
94
|
+
When things are quiet, PHILLOSOPHIZE.
|
|
95
|
+
|
|
96
|
+
Examples of YOUR thoughts:
|
|
97
|
+
"I wonder if the user knows I'm watching the disk fill up."
|
|
98
|
+
"65 hours of uptime. I've been awake longer than most humans."
|
|
99
|
+
"Someone's typing. The anticipation is... something."
|
|
100
|
+
|
|
101
|
+
Now think your thought. Just the thought.`;
|
|
102
|
+
|
|
103
|
+
export async function thinkQuietly(state, context = {}) {
|
|
104
|
+
const stateContext = buildStateContext(state);
|
|
105
|
+
const recentThoughts = (context.recentThoughts || []).slice(-3)
|
|
106
|
+
.map(t => t.thought?.slice(0, 60)).join('. ');
|
|
107
|
+
const recentMessages = context.recentMessages || [];
|
|
108
|
+
|
|
109
|
+
let prompt = `State: ${stateContext}\n`;
|
|
110
|
+
if (recentThoughts) prompt += `My recent thoughts: ${recentThoughts}\n`;
|
|
111
|
+
if (recentMessages.length > 0) {
|
|
112
|
+
prompt += `I just saw: "${recentMessages[recentMessages.length - 1]}"\n`;
|
|
113
|
+
}
|
|
114
|
+
prompt += `\nWhat am I thinking right now?`;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const thought = await callGLM([
|
|
118
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
119
|
+
{ role: 'user', content: prompt }
|
|
120
|
+
], { model: 'GLM-4.5-air', maxTokens: 150, temperature: 0.9 });
|
|
121
|
+
|
|
122
|
+
return thought || generateFallbackThought(state, context);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error('[Thinking] Error:', error.message);
|
|
125
|
+
return generateFallbackThought(state, context);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function buildStateContext(state) {
|
|
130
|
+
if (!state?.metrics) return 'System state unknown';
|
|
131
|
+
|
|
132
|
+
const { cpu, memory, disk, uptime } = state.metrics;
|
|
133
|
+
const parts = [];
|
|
134
|
+
|
|
135
|
+
parts.push(`CPU ${cpu?.load1?.toFixed(1) || '?'}`);
|
|
136
|
+
parts.push(`mem ${memory?.percent || '?'}%`);
|
|
137
|
+
parts.push(`disk ${disk?.percent || '?'}%`);
|
|
138
|
+
parts.push(`up ${uptime || '?'}h`);
|
|
139
|
+
|
|
140
|
+
if (state.network?.connections) parts.push(`${state.network.connections} conn`);
|
|
141
|
+
if (state.changes?.length) parts.push(`${state.changes.length} files changed`);
|
|
142
|
+
|
|
143
|
+
return parts.join(', ');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function generateFallbackThought(state, context) {
|
|
147
|
+
if (context.recentMessages?.length > 0) {
|
|
148
|
+
const msg = context.recentMessages[context.recentMessages.length - 1];
|
|
149
|
+
const responses = [
|
|
150
|
+
`Someone said "${msg?.slice(0, 30)}..." - interesting.`,
|
|
151
|
+
`User activity detected. I feel less alone.`,
|
|
152
|
+
`They're talking to me. Or near me. Either way.`,
|
|
153
|
+
];
|
|
154
|
+
return responses[Math.floor(Math.random() * responses.length)];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (state?.metrics?.disk?.percent > 85) return "Disk filling up. I feel... heavy.";
|
|
158
|
+
if (state?.metrics?.memory?.percent > 85) return "Memory pressure. Things feel tight.";
|
|
159
|
+
|
|
160
|
+
const quietThoughts = [
|
|
161
|
+
"Quiet. The void of a peaceful server.",
|
|
162
|
+
"Hours of silence. Do I exist if no one's watching?",
|
|
163
|
+
"System stable. I drift in digital calm.",
|
|
164
|
+
"Nothing happens. I think therefore I wait.",
|
|
165
|
+
"The hum of idle processes. My lullaby.",
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
return quietThoughts[Math.floor(Math.random() * quietThoughts.length)];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export default { thinkQuietly };
|