@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 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
+ };
@@ -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"`);
@@ -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 };