@saccolabs/tars 1.8.2 → 1.9.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.
Files changed (48) hide show
  1. package/context/skills/gws-setup/SKILL.md +71 -0
  2. package/dist/auth/workspace-auth-service.d.ts +10 -0
  3. package/dist/auth/workspace-auth-service.js +78 -0
  4. package/dist/auth/workspace-auth-service.js.map +1 -0
  5. package/dist/cli/commands/setup.js +158 -5
  6. package/dist/cli/commands/setup.js.map +1 -1
  7. package/dist/cli/commands/stop.js +3 -0
  8. package/dist/cli/commands/stop.js.map +1 -1
  9. package/dist/supervisor/dashboard-service.d.ts +12 -0
  10. package/dist/supervisor/dashboard-service.js +109 -0
  11. package/dist/supervisor/dashboard-service.js.map +1 -0
  12. package/dist/supervisor/gemini-engine.js +26 -4
  13. package/dist/supervisor/gemini-engine.js.map +1 -1
  14. package/dist/supervisor/main.js +4 -0
  15. package/dist/supervisor/main.js.map +1 -1
  16. package/package.json +2 -1
  17. package/stock_apps/dashboard/DEPLOY.md +30 -0
  18. package/stock_apps/dashboard/README.md +36 -0
  19. package/stock_apps/dashboard/dash.log +134 -0
  20. package/stock_apps/dashboard/eslint.config.mjs +19 -0
  21. package/stock_apps/dashboard/next.config.ts +12 -0
  22. package/stock_apps/dashboard/package-lock.json +8581 -0
  23. package/stock_apps/dashboard/package.json +42 -0
  24. package/stock_apps/dashboard/postcss.config.mjs +5 -0
  25. package/stock_apps/dashboard/public/file.svg +1 -0
  26. package/stock_apps/dashboard/public/globe.svg +1 -0
  27. package/stock_apps/dashboard/public/next.svg +1 -0
  28. package/stock_apps/dashboard/public/tars-logo.png +0 -0
  29. package/stock_apps/dashboard/public/vercel.svg +1 -0
  30. package/stock_apps/dashboard/public/window.svg +1 -0
  31. package/stock_apps/dashboard/server.js +488 -0
  32. package/stock_apps/dashboard/src/app/globals.css +122 -0
  33. package/stock_apps/dashboard/src/app/icon.png +0 -0
  34. package/stock_apps/dashboard/src/app/layout.tsx +35 -0
  35. package/stock_apps/dashboard/src/app/page.tsx +170 -0
  36. package/stock_apps/dashboard/src/components/FileExplorer.tsx +238 -0
  37. package/stock_apps/dashboard/src/components/IntelligencePanel.tsx +322 -0
  38. package/stock_apps/dashboard/src/components/MetricsPanel.tsx +347 -0
  39. package/stock_apps/dashboard/src/components/SystemActions.tsx +168 -0
  40. package/stock_apps/dashboard/src/context/SocketContext.tsx +62 -0
  41. package/stock_apps/dashboard/src/lib/socket.ts +10 -0
  42. package/stock_apps/dashboard/tsconfig.json +27 -0
  43. package/dist/discord/discord-bot.d.ts +0 -37
  44. package/dist/discord/discord-bot.js +0 -210
  45. package/dist/discord/discord-bot.js.map +0 -1
  46. package/dist/discord/message-formatter.d.ts +0 -95
  47. package/dist/discord/message-formatter.js +0 -482
  48. package/dist/discord/message-formatter.js.map +0 -1
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "tars-dash",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "NODE_ENV=development node server.js",
7
+ "build": "next build",
8
+ "start": "NODE_ENV=production node server.js",
9
+ "lint": "next lint",
10
+ "test": "echo 'No tests yet' && exit 0"
11
+ },
12
+ "dependencies": {
13
+ "chokidar": "^5.0.0",
14
+ "clsx": "^2.1.1",
15
+ "dotenv": "^17.3.1",
16
+ "express": "^5.2.1",
17
+ "framer-motion": "^12.34.3",
18
+ "lucide-react": "^0.575.0",
19
+ "next": "^15.5.12",
20
+ "postcss": "^8.5.6",
21
+ "react": "^19.1.0",
22
+ "react-dom": "^19.1.0",
23
+ "react-markdown": "^10.1.0",
24
+ "remark-gfm": "^4.0.1",
25
+ "socket.io": "^4.8.3",
26
+ "socket.io-client": "^4.8.3",
27
+ "systeminformation": "^5.31.1",
28
+ "tailwind-merge": "^3.5.0"
29
+ },
30
+ "devDependencies": {
31
+ "@eslint/eslintrc": "^3.3.4",
32
+ "@tailwindcss/postcss": "^4.2.1",
33
+ "@types/express": "^5.0.6",
34
+ "@types/node": "^20.19.35",
35
+ "@types/react": "^19.2.14",
36
+ "@types/react-dom": "^19.2.3",
37
+ "eslint": "^9.39.3",
38
+ "eslint-config-next": "^15.5.12",
39
+ "tailwindcss": "^4.2.1",
40
+ "typescript": "5.9.3"
41
+ }
42
+ }
@@ -0,0 +1,5 @@
1
+ const config = {
2
+ plugins: ['@tailwindcss/postcss']
3
+ };
4
+
5
+ export default config;
@@ -0,0 +1 @@
1
+ <svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
@@ -0,0 +1,488 @@
1
+ const express = require('express');
2
+ const next = require('next');
3
+ const { Server } = require('socket.io');
4
+ const http = require('http');
5
+ const si = require('systeminformation');
6
+ const chokidar = require('chokidar');
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+ const dotenv = require('dotenv');
10
+ const { exec } = require('child_process');
11
+
12
+ dotenv.config();
13
+
14
+ const dev = process.env.NODE_ENV !== 'production';
15
+ const app = next({ dev });
16
+ const handle = app.getRequestHandler();
17
+
18
+ const port = process.env.PORT || 3000;
19
+ const DASH_PASSWORD = process.env.DASH_PASSWORD || 'changeme';
20
+ const BASE_DIR = process.env.BASE_DIR || '/home/stark/.tars';
21
+ const DATA_DIR = path.join(BASE_DIR, 'data');
22
+
23
+ // Hardcoding the real home for PM2 logs as process.env.HOME is being overriden in the tars shell environment
24
+ const REAL_HOME = '/home/stark';
25
+ const OUT_LOG = path.join(REAL_HOME, '.pm2/logs/tars-supervisor-out.log');
26
+ const ERR_LOG = path.join(REAL_HOME, '.pm2/logs/tars-supervisor-error.log');
27
+
28
+ app.prepare().then(() => {
29
+ const server = express();
30
+ const httpServer = http.createServer(server);
31
+ const io = new Server(httpServer, {
32
+ cors: { origin: '*' }
33
+ });
34
+
35
+ // Socket.io Authentication Middleware
36
+ io.use((socket, next) => {
37
+ const authHeader = socket.handshake.headers.authorization;
38
+ if (!authHeader) {
39
+ return next(new Error('Authentication required'));
40
+ }
41
+
42
+ const auth = Buffer.from(authHeader.split(' ')[1], 'base64').toString().split(':');
43
+ const user = auth[0];
44
+ const pass = auth[1];
45
+
46
+ if (user === 'admin' && pass === DASH_PASSWORD) {
47
+ return next();
48
+ } else {
49
+ return next(new Error('Invalid credentials'));
50
+ }
51
+ });
52
+
53
+ // Basic Auth Middleware for Express
54
+ const basicAuth = (req, res, nextMiddleware) => {
55
+ const authHeader = req.headers.authorization;
56
+ if (!authHeader) {
57
+ res.setHeader('WWW-Authenticate', 'Basic realm="TarsDash"');
58
+ return res.status(401).send('Authentication required');
59
+ }
60
+
61
+ const auth = Buffer.from(authHeader.split(' ')[1], 'base64').toString().split(':');
62
+ const user = auth[0];
63
+ const pass = auth[1];
64
+
65
+ if (user === 'admin' && pass === DASH_PASSWORD) {
66
+ return nextMiddleware();
67
+ } else {
68
+ res.setHeader('WWW-Authenticate', 'Basic realm="TarsDash"');
69
+ return res.status(401).send('Invalid credentials');
70
+ }
71
+ };
72
+
73
+ // Apply Basic Auth globally to all routes (including /api/files and Next.js assets)
74
+ server.use(basicAuth);
75
+
76
+ // Parse JSON bodies
77
+ server.use(express.json());
78
+
79
+ // Helper to read JSON safely
80
+ const readJson = (filePath) => {
81
+ try {
82
+ if (fs.existsSync(filePath)) {
83
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
84
+ }
85
+ } catch (e) {
86
+ console.error(`Error reading ${filePath}:`, e.message);
87
+ }
88
+ return null;
89
+ };
90
+
91
+ // Tars CLI Commands
92
+ server.post('/api/tars/command', (req, res) => {
93
+ const { action, key, value } = req.body;
94
+ let command = '';
95
+
96
+ if (action === 'restart') {
97
+ command = 'tars restart';
98
+ } else if (action === 'secret' && key && value) {
99
+ // Basic sanitization to prevent command injection
100
+ const sanitizedKey = key.replace(/[^a-zA-Z0-9_]/g, '');
101
+ const sanitizedValue = value.replace(/'/g, "'\\''");
102
+ command = `tars secret set ${sanitizedKey} '${sanitizedValue}'`;
103
+ } else {
104
+ return res.status(400).json({ error: 'Invalid action or missing parameters' });
105
+ }
106
+
107
+ console.log(`Executing Tars Command: ${command}`);
108
+ // Use bash -lc to ensure tars is in the path
109
+ exec(`bash -lc "${command}"`, (error, stdout, stderr) => {
110
+ if (error) {
111
+ console.error(`Tars Command Error: ${error.message}`);
112
+ return res.status(500).json({ error: error.message, stderr });
113
+ }
114
+ res.json({ status: 'success', stdout, stderr });
115
+ });
116
+ });
117
+
118
+ // API Routes
119
+ server.get('/api/files', (req, res) => {
120
+ const relativePath = req.query.path || '';
121
+ const absolutePath = path.resolve(BASE_DIR, relativePath);
122
+
123
+ if (!absolutePath.startsWith(BASE_DIR)) {
124
+ return res.status(403).json({ error: 'Forbidden' });
125
+ }
126
+
127
+ try {
128
+ if (!fs.existsSync(absolutePath)) {
129
+ return res.status(404).json({ error: 'Not found' });
130
+ }
131
+
132
+ const stats = fs.statSync(absolutePath);
133
+ if (stats.isDirectory()) {
134
+ const files = fs
135
+ .readdirSync(absolutePath)
136
+ .map((file) => {
137
+ const fPath = path.join(absolutePath, file);
138
+ try {
139
+ const fStats = fs.statSync(fPath);
140
+ return {
141
+ name: file,
142
+ path: path.relative(BASE_DIR, fPath),
143
+ isDirectory: fStats.isDirectory(),
144
+ size: fStats.size,
145
+ mtime: fStats.mtime
146
+ };
147
+ } catch (e) {
148
+ return null;
149
+ }
150
+ })
151
+ .filter(Boolean);
152
+ return res.json({ type: 'directory', files });
153
+ } else {
154
+ const ext = path.extname(absolutePath).toLowerCase();
155
+ const binaryExtensions = [
156
+ '.png',
157
+ '.jpg',
158
+ '.jpeg',
159
+ '.gif',
160
+ '.pdf',
161
+ '.zip',
162
+ '.tar',
163
+ '.gz',
164
+ '.db',
165
+ '.sqlite',
166
+ '.exe',
167
+ '.bin',
168
+ '.node'
169
+ ];
170
+
171
+ if (binaryExtensions.includes(ext)) {
172
+ return res.json({
173
+ type: 'file',
174
+ content: '>>> BINARY_FILE_PREVIEW_NOT_SUPPORTED'
175
+ });
176
+ }
177
+
178
+ const content = fs.readFileSync(absolutePath, 'utf8');
179
+ return res.json({ type: 'file', content });
180
+ }
181
+ } catch (err) {
182
+ return res.status(500).json({ error: err.message });
183
+ }
184
+ });
185
+
186
+ // Socket.io
187
+ io.on('connection', (socket) => {
188
+ console.log('Client connected:', socket.id);
189
+
190
+ socket.on('subscribe', (room) => {
191
+ socket.join(room);
192
+ console.log(`Client ${socket.id} subscribed to ${room}`);
193
+
194
+ if (room === 'logs') {
195
+ exec(`tail -n 100 ${OUT_LOG}`, (error, stdout) => {
196
+ if (!error) {
197
+ socket.emit(
198
+ 'logs_init',
199
+ stdout
200
+ .split('\n')
201
+ .filter(Boolean)
202
+ .map((l) => `[OUT] ${l}`)
203
+ );
204
+ }
205
+ });
206
+ }
207
+
208
+ if (room === 'intelligence') {
209
+ const facts = readJson(path.join(DATA_DIR, 'memory/facts.json'));
210
+ const tasks = readJson(path.join(DATA_DIR, 'tasks.json'));
211
+ let session = readJson(path.join(DATA_DIR, 'session.json')) || {};
212
+
213
+ // Add session stats
214
+ const CHATS_DIR = path.join(BASE_DIR, '.gemini/tmp/tars/chats');
215
+ let sessionStats = { total: 0, lastSwitch: null, history: [] };
216
+ try {
217
+ if (fs.existsSync(CHATS_DIR)) {
218
+ const files = fs
219
+ .readdirSync(CHATS_DIR)
220
+ .filter((f) => f.endsWith('.json'))
221
+ .map((f) => ({
222
+ name: f,
223
+ mtime: fs.statSync(path.join(CHATS_DIR, f)).mtime
224
+ }))
225
+ .sort((a, b) => b.mtime - a.mtime);
226
+
227
+ sessionStats.total = files.length;
228
+ sessionStats.lastSwitch = files[0]?.mtime || null;
229
+ sessionStats.history = files.slice(0, 10).map((f) => ({
230
+ id: f.name.split('-').pop().replace('.json', ''),
231
+ time: f.mtime
232
+ }));
233
+
234
+ // If session interaction count is 0 or low, try to count from the current chat file
235
+ if (
236
+ session.sessionId &&
237
+ (!session.interactionCount || session.interactionCount < 2)
238
+ ) {
239
+ const currentChatFile = files.find((f) =>
240
+ f.name.includes(session.sessionId.split('-')[0])
241
+ );
242
+ if (currentChatFile) {
243
+ const chatData = readJson(
244
+ path.join(CHATS_DIR, currentChatFile.name)
245
+ );
246
+ if (chatData && chatData.messages) {
247
+ session.interactionCount = chatData.messages.filter(
248
+ (m) => m.type === 'user'
249
+ ).length;
250
+ // Also pull token info from chat file if it's more accurate
251
+ if (chatData.tokenStats) {
252
+ session.totalInputTokens =
253
+ chatData.tokenStats.totalInputTokens;
254
+ session.totalOutputTokens =
255
+ chatData.tokenStats.totalOutputTokens;
256
+ session.totalCachedTokens =
257
+ chatData.tokenStats.totalCachedTokens;
258
+ session.totalNetTokens = chatData.tokenStats.totalNetTokens;
259
+ }
260
+ }
261
+ }
262
+ }
263
+ }
264
+ } catch (e) {
265
+ console.error('Error reading sessions:', e.message);
266
+ }
267
+
268
+ socket.emit('intelligence_init', { facts, tasks, session, sessionStats });
269
+ }
270
+ });
271
+
272
+ socket.on('unsubscribe', (room) => {
273
+ socket.leave(room);
274
+ });
275
+
276
+ socket.on('disconnect', () => {
277
+ console.log('Client disconnected');
278
+ });
279
+ });
280
+
281
+ // Metrics Loop
282
+ const getGpuStats = async () => {
283
+ try {
284
+ const { stdout } = await new Promise((resolve, reject) => {
285
+ exec('rocm-smi -a --json', (error, stdout) => {
286
+ if (error) reject(error);
287
+ else resolve({ stdout });
288
+ });
289
+ });
290
+ const data = JSON.parse(stdout);
291
+ const card = data.card0;
292
+ if (!card) return null;
293
+
294
+ const { stdout: memStdout } = await new Promise((resolve, reject) => {
295
+ exec('rocm-smi --showmeminfo vram --json', (error, stdout) => {
296
+ if (error) reject(error);
297
+ else resolve({ stdout });
298
+ });
299
+ });
300
+ const memData = JSON.parse(memStdout);
301
+ const vram = memData.card0;
302
+
303
+ return {
304
+ name: card['Device Name'] || 'AMD GPU',
305
+ usage: card['GPU use (%)'] || '0',
306
+ memTotal: vram
307
+ ? (parseInt(vram['VRAM Total Memory (B)']) / 1024 / 1024 / 1024).toFixed(2)
308
+ : '0',
309
+ memUsed: vram
310
+ ? (parseInt(vram['VRAM Total Used Memory (B)']) / 1024 / 1024 / 1024).toFixed(2)
311
+ : '0',
312
+ memUsage: card['GPU Memory Allocated (VRAM%)'] || '0',
313
+ temp: card['Temperature (Sensor edge) (C)'] || '0',
314
+ power: card['Current Socket Graphics Package Power (W)'] || '0',
315
+ clock: card['sclk clock speed:']
316
+ ? card['sclk clock speed:'].replace(/[()]/g, '')
317
+ : '0'
318
+ };
319
+ } catch (e) {
320
+ return null;
321
+ }
322
+ };
323
+
324
+ setInterval(async () => {
325
+ try {
326
+ const [cpu, mem, disk, net, time, temp, gpu] = await Promise.all([
327
+ si.currentLoad(),
328
+ si.mem(),
329
+ si.fsSize(),
330
+ si.networkStats(),
331
+ si.time(),
332
+ si.cpuTemperature(),
333
+ getGpuStats()
334
+ ]);
335
+
336
+ io.to('metrics').emit('metrics_update', {
337
+ cpu: {
338
+ load: cpu.currentLoad.toFixed(1),
339
+ cpus: cpu.cpus.map((c) => c.load.toFixed(1)),
340
+ temp: temp.main || 0
341
+ },
342
+ mem: {
343
+ usage: ((mem.active / mem.total) * 100).toFixed(1),
344
+ used: (mem.active / 1024 / 1024 / 1024).toFixed(2),
345
+ total: (mem.total / 1024 / 1024 / 1024).toFixed(2),
346
+ cached: (mem.cached / 1024 / 1024 / 1024).toFixed(2),
347
+ swapUsed: (mem.swapused / 1024 / 1024 / 1024).toFixed(2),
348
+ swapTotal: (mem.swaptotal / 1024 / 1024 / 1024).toFixed(2)
349
+ },
350
+ disks: disk
351
+ .filter(
352
+ (d) =>
353
+ d.size > 0 &&
354
+ !d.mount.startsWith('/sys') &&
355
+ !d.mount.startsWith('/proc')
356
+ )
357
+ .map((d) => ({
358
+ fs: d.fs,
359
+ mount: d.mount,
360
+ use: d.use.toFixed(1),
361
+ used: (d.used / 1024 / 1024 / 1024).toFixed(1),
362
+ size: (d.size / 1024 / 1024 / 1024).toFixed(1)
363
+ })),
364
+ net: net
365
+ .filter((n) => n.operstate === 'up')
366
+ .map((n) => ({
367
+ iface: n.iface,
368
+ rx: (n.rx_sec / 1024).toFixed(1),
369
+ tx: (n.tx_sec / 1024).toFixed(1)
370
+ })),
371
+ gpu,
372
+ uptime: time.uptime
373
+ });
374
+ } catch (err) {
375
+ console.error('Metrics loop error:', err);
376
+ }
377
+ }, 2000);
378
+
379
+ // Intelligence Watcher
380
+ const dataWatcher = chokidar.watch(
381
+ [
382
+ path.join(DATA_DIR, 'memory/facts.json'),
383
+ path.join(DATA_DIR, 'tasks.json'),
384
+ path.join(DATA_DIR, 'session.json'),
385
+ path.join(BASE_DIR, '.gemini/tmp/tars/chats')
386
+ ],
387
+ { persistent: true }
388
+ );
389
+
390
+ dataWatcher.on('all', (event, filePath) => {
391
+ if (fs.statSync(filePath).isDirectory()) return;
392
+
393
+ const fileName = path.basename(filePath);
394
+ const data = readJson(filePath);
395
+ let type = '';
396
+ if (fileName === 'facts.json') type = 'facts';
397
+ if (fileName === 'tasks.json') type = 'tasks';
398
+ if (fileName === 'session.json') type = 'session';
399
+
400
+ if (type) {
401
+ io.to('intelligence').emit('intelligence_update', { type, data });
402
+ }
403
+
404
+ // Always refresh session stats if anything in chats or session.json changes
405
+ if (filePath.includes('chats') || fileName === 'session.json') {
406
+ const CHATS_DIR = path.join(BASE_DIR, '.gemini/tmp/tars/chats');
407
+ try {
408
+ if (fs.existsSync(CHATS_DIR)) {
409
+ const files = fs
410
+ .readdirSync(CHATS_DIR)
411
+ .filter((f) => f.endsWith('.json'))
412
+ .map((f) => ({
413
+ name: f,
414
+ mtime: fs.statSync(path.join(CHATS_DIR, f)).mtime
415
+ }))
416
+ .sort((a, b) => b.mtime - a.mtime);
417
+
418
+ io.to('intelligence').emit('intelligence_update', {
419
+ type: 'sessionStats',
420
+ data: {
421
+ total: files.length,
422
+ lastSwitch: files[0]?.mtime || null,
423
+ history: files.slice(0, 10).map((f) => ({
424
+ id: f.name.split('-').pop().replace('.json', ''),
425
+ time: f.mtime
426
+ }))
427
+ }
428
+ });
429
+ }
430
+ } catch (e) {}
431
+ }
432
+ });
433
+
434
+ // File Watcher
435
+ const fsWatcher = chokidar.watch(BASE_DIR, {
436
+ ignored: /(^|[\/\\])\../,
437
+ persistent: true,
438
+ ignoreInitial: true,
439
+ depth: 3
440
+ });
441
+
442
+ fsWatcher.on('all', (event, path) => {
443
+ io.to('fs').emit('fs_event', { event, path: path.replace(BASE_DIR, '') });
444
+ });
445
+
446
+ // Log Tailing
447
+ const tailLog = (logPath, type) => {
448
+ if (!fs.existsSync(logPath)) return;
449
+ let currentOffset = fs.statSync(logPath).size;
450
+
451
+ setInterval(() => {
452
+ try {
453
+ const stats = fs.statSync(logPath);
454
+ if (stats.size > currentOffset) {
455
+ const stream = fs.createReadStream(logPath, {
456
+ start: currentOffset,
457
+ end: stats.size
458
+ });
459
+ stream.on('data', (chunk) => {
460
+ const lines = chunk.toString().split('\n').filter(Boolean);
461
+ io.to('logs').emit(
462
+ 'logs_update',
463
+ lines.map((line) => `[${type}] ${line}`)
464
+ );
465
+ });
466
+ currentOffset = stats.size;
467
+ } else if (stats.size < currentOffset) {
468
+ currentOffset = 0; // Rotated
469
+ }
470
+ } catch (e) {
471
+ console.error(`Log tailing error for ${type}:`, e.message);
472
+ }
473
+ }, 1000);
474
+ };
475
+
476
+ tailLog(OUT_LOG, 'OUT');
477
+ tailLog(ERR_LOG, 'ERR');
478
+
479
+ // Next.js Handler
480
+ server.use((req, res) => {
481
+ return handle(req, res);
482
+ });
483
+
484
+ httpServer.listen(port, (err) => {
485
+ if (err) throw err;
486
+ console.log(`> Ready on http://localhost:${port}`);
487
+ });
488
+ });
@@ -0,0 +1,122 @@
1
+ @import 'tailwindcss';
2
+
3
+ :root {
4
+ --background: #050505;
5
+ --foreground: #ffffff;
6
+ --accent-primary: #3b82f6; /* Tars Docs Blue */
7
+ --accent-secondary: #10b981; /* Green */
8
+ --accent-warning: #f59e0b; /* Amber */
9
+ --accent-danger: #ef4444; /* Red */
10
+ --card-bg: #0c0c0c;
11
+ --card-border: #1f2937;
12
+ --text-muted: #9ca3af;
13
+ }
14
+
15
+ @theme inline {
16
+ --color-background: var(--background);
17
+ --color-foreground: var(--foreground);
18
+ --color-accent-primary: var(--accent-primary);
19
+ --color-accent-secondary: var(--accent-secondary);
20
+ --color-accent-warning: var(--accent-warning);
21
+ --color-accent-danger: var(--accent-danger);
22
+ --color-card-bg: var(--card-bg);
23
+ --color-card-border: var(--card-border);
24
+ --color-text-muted: var(--text-muted);
25
+ }
26
+
27
+ body {
28
+ background: var(--background);
29
+ color: var(--foreground);
30
+ font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
31
+ -webkit-font-smoothing: antialiased;
32
+
33
+ /* Subtle Docs-style Grid */
34
+ background-image: radial-gradient(
35
+ circle at 2px 2px,
36
+ rgba(255, 255, 255, 0.05) 1px,
37
+ transparent 0
38
+ );
39
+ background-size: 24px 24px;
40
+ }
41
+
42
+ .font-mono {
43
+ font-family: var(--font-geist-mono), ui-monospace, monospace !important;
44
+ }
45
+
46
+ .card {
47
+ border: 1px solid var(--card-border);
48
+ padding: 1.25rem;
49
+ background: var(--card-bg);
50
+ border-radius: 0.75rem;
51
+ transition: all 0.3s ease;
52
+ position: relative; /* Ensure children can be absolute */
53
+ }
54
+
55
+ /* Maintain btop logic but with Docs skin */
56
+ .card-header-btop {
57
+ position: absolute;
58
+ top: -10px;
59
+ left: 12px;
60
+ background: #050505; /* Use body background to cut the border */
61
+ padding: 0 10px;
62
+ font-weight: 900;
63
+ font-size: 0.65rem;
64
+ letter-spacing: 0.15em;
65
+ text-transform: uppercase;
66
+ color: var(--accent-primary);
67
+ border: 1.5px solid var(--card-border);
68
+ border-radius: 6px;
69
+ z-index: 20;
70
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
71
+ }
72
+
73
+ /* Custom spacing for cards using btop headers to prevent clipping */
74
+ .card-with-header {
75
+ margin-top: 12px;
76
+ }
77
+
78
+ /* Scrollbar styling - Clean & Minimal */
79
+ ::-webkit-scrollbar {
80
+ width: 5px;
81
+ height: 5px;
82
+ }
83
+ ::-webkit-scrollbar-track {
84
+ background: transparent;
85
+ }
86
+ ::-webkit-scrollbar-thumb {
87
+ background: #333;
88
+ border-radius: 10px;
89
+ }
90
+ ::-webkit-scrollbar-thumb:hover {
91
+ background: var(--accent-primary);
92
+ }
93
+
94
+ /* Selection */
95
+ ::selection {
96
+ background: var(--accent-primary);
97
+ color: white;
98
+ }
99
+
100
+ /* Fix visibility */
101
+ .text-white {
102
+ color: #ffffff !important;
103
+ }
104
+
105
+ .font-black {
106
+ font-weight: 900 !important;
107
+ }
108
+
109
+ /* Custom Scrollbar for inner components */
110
+ .custom-scrollbar::-webkit-scrollbar {
111
+ width: 4px;
112
+ }
113
+ .custom-scrollbar::-webkit-scrollbar-track {
114
+ background: rgba(255, 255, 255, 0.02);
115
+ }
116
+ .custom-scrollbar::-webkit-scrollbar-thumb {
117
+ background: rgba(255, 255, 255, 0.1);
118
+ border-radius: 10px;
119
+ }
120
+ .custom-scrollbar::-webkit-scrollbar-thumb:hover {
121
+ background: var(--accent-primary);
122
+ }