@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.
- package/context/skills/gws-setup/SKILL.md +71 -0
- package/dist/auth/workspace-auth-service.d.ts +10 -0
- package/dist/auth/workspace-auth-service.js +78 -0
- package/dist/auth/workspace-auth-service.js.map +1 -0
- package/dist/cli/commands/setup.js +158 -5
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/stop.js +3 -0
- package/dist/cli/commands/stop.js.map +1 -1
- package/dist/supervisor/dashboard-service.d.ts +12 -0
- package/dist/supervisor/dashboard-service.js +109 -0
- package/dist/supervisor/dashboard-service.js.map +1 -0
- package/dist/supervisor/gemini-engine.js +26 -4
- package/dist/supervisor/gemini-engine.js.map +1 -1
- package/dist/supervisor/main.js +4 -0
- package/dist/supervisor/main.js.map +1 -1
- package/package.json +2 -1
- package/stock_apps/dashboard/DEPLOY.md +30 -0
- package/stock_apps/dashboard/README.md +36 -0
- package/stock_apps/dashboard/dash.log +134 -0
- package/stock_apps/dashboard/eslint.config.mjs +19 -0
- package/stock_apps/dashboard/next.config.ts +12 -0
- package/stock_apps/dashboard/package-lock.json +8581 -0
- package/stock_apps/dashboard/package.json +42 -0
- package/stock_apps/dashboard/postcss.config.mjs +5 -0
- package/stock_apps/dashboard/public/file.svg +1 -0
- package/stock_apps/dashboard/public/globe.svg +1 -0
- package/stock_apps/dashboard/public/next.svg +1 -0
- package/stock_apps/dashboard/public/tars-logo.png +0 -0
- package/stock_apps/dashboard/public/vercel.svg +1 -0
- package/stock_apps/dashboard/public/window.svg +1 -0
- package/stock_apps/dashboard/server.js +488 -0
- package/stock_apps/dashboard/src/app/globals.css +122 -0
- package/stock_apps/dashboard/src/app/icon.png +0 -0
- package/stock_apps/dashboard/src/app/layout.tsx +35 -0
- package/stock_apps/dashboard/src/app/page.tsx +170 -0
- package/stock_apps/dashboard/src/components/FileExplorer.tsx +238 -0
- package/stock_apps/dashboard/src/components/IntelligencePanel.tsx +322 -0
- package/stock_apps/dashboard/src/components/MetricsPanel.tsx +347 -0
- package/stock_apps/dashboard/src/components/SystemActions.tsx +168 -0
- package/stock_apps/dashboard/src/context/SocketContext.tsx +62 -0
- package/stock_apps/dashboard/src/lib/socket.ts +10 -0
- package/stock_apps/dashboard/tsconfig.json +27 -0
- package/dist/discord/discord-bot.d.ts +0 -37
- package/dist/discord/discord-bot.js +0 -210
- package/dist/discord/discord-bot.js.map +0 -1
- package/dist/discord/message-formatter.d.ts +0 -95
- package/dist/discord/message-formatter.js +0 -482
- 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 @@
|
|
|
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>
|
|
Binary file
|
|
@@ -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
|
+
}
|
|
Binary file
|