instar 0.8.8 → 0.8.10
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.
|
@@ -196,6 +196,30 @@ Strip the \`[telegram:N]\` prefix before interpreting the message. Respond natur
|
|
|
196
196
|
else {
|
|
197
197
|
result.skipped.push('CLAUDE.md: Private Viewer section already present');
|
|
198
198
|
}
|
|
199
|
+
// Dashboard section
|
|
200
|
+
if (!content.includes('**Dashboard**') && !content.includes('/dashboard')) {
|
|
201
|
+
const section = `
|
|
202
|
+
**Dashboard** — Visual web interface for monitoring and managing sessions. Accessible from any device (phone, tablet, laptop) via tunnel.
|
|
203
|
+
- Local: \`http://localhost:${port}/dashboard\`
|
|
204
|
+
- Remote: When a tunnel is running, the dashboard is accessible at \`{tunnelUrl}/dashboard\`
|
|
205
|
+
- Authentication: Uses a 6-digit PIN (configured via \`dashboardPin\` in \`.instar/config.json\`) — no need to enter the full bearer token
|
|
206
|
+
- Features: Real-time terminal streaming of all running sessions, session management, model badges, mobile-responsive
|
|
207
|
+
- **Sharing the dashboard**: When the user wants to check on sessions from their phone, give them the tunnel URL + PIN. Check tunnel status: \`curl -H "Authorization: Bearer $AUTH" http://localhost:${port}/tunnel\`
|
|
208
|
+
`;
|
|
209
|
+
// Insert after Server Status or before Scripts section
|
|
210
|
+
const insertBefore = content.indexOf('**Scripts**');
|
|
211
|
+
if (insertBefore >= 0) {
|
|
212
|
+
content = content.slice(0, insertBefore) + section + '\n' + content.slice(insertBefore);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
content += '\n' + section;
|
|
216
|
+
}
|
|
217
|
+
patched = true;
|
|
218
|
+
result.upgraded.push('CLAUDE.md: added Dashboard section');
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
result.skipped.push('CLAUDE.md: Dashboard section already present');
|
|
222
|
+
}
|
|
199
223
|
if (patched) {
|
|
200
224
|
try {
|
|
201
225
|
fs.writeFileSync(claudeMdPath, content);
|
|
@@ -268,6 +268,13 @@ This routes feedback to the Instar maintainers automatically. Valid types: \`bug
|
|
|
268
268
|
**Server Status** — Detailed runtime information beyond health checks.
|
|
269
269
|
- Status: \`curl -H "Authorization: Bearer $AUTH" http://localhost:${port}/status\`
|
|
270
270
|
|
|
271
|
+
**Dashboard** — Visual web interface for monitoring and managing sessions. Accessible from any device (phone, tablet, laptop) via tunnel.
|
|
272
|
+
- Local: \`http://localhost:${port}/dashboard\`
|
|
273
|
+
- Remote: When a tunnel is running, the dashboard is accessible at \`{tunnelUrl}/dashboard\`
|
|
274
|
+
- Authentication: Uses a 6-digit PIN (configured via \`dashboardPin\` in \`.instar/config.json\`) — no need to enter the full bearer token
|
|
275
|
+
- Features: Real-time terminal streaming of all running sessions, session management, model badges, mobile-responsive
|
|
276
|
+
- **Sharing the dashboard**: When the user wants to check on sessions from their phone, give them the tunnel URL + PIN. Check tunnel status: \`curl -H "Authorization: Bearer $AUTH" http://localhost:${port}/tunnel\`
|
|
277
|
+
|
|
271
278
|
**Scripts** — Reusable capabilities in \`.claude/scripts/\`.
|
|
272
279
|
|
|
273
280
|
**Skills** — Reusable behavioral capabilities in \`.claude/skills/\`.
|
|
@@ -36,11 +36,13 @@ export declare class WebSocketManager {
|
|
|
36
36
|
private sessionManager;
|
|
37
37
|
private state;
|
|
38
38
|
private authToken?;
|
|
39
|
+
private registryPath?;
|
|
39
40
|
constructor(options: {
|
|
40
41
|
server: HttpServer;
|
|
41
42
|
sessionManager: SessionManager;
|
|
42
43
|
state: StateManager;
|
|
43
44
|
authToken?: string;
|
|
45
|
+
instarDir?: string;
|
|
44
46
|
});
|
|
45
47
|
private authenticate;
|
|
46
48
|
private verifyToken;
|
|
@@ -50,6 +52,12 @@ export declare class WebSocketManager {
|
|
|
50
52
|
* Uses diff-based approach: only sends new content since last capture.
|
|
51
53
|
*/
|
|
52
54
|
private startStreaming;
|
|
55
|
+
/**
|
|
56
|
+
* Resolve display names by cross-referencing the topic-session registry.
|
|
57
|
+
* Maps tmux session names to their Telegram topic names.
|
|
58
|
+
*/
|
|
59
|
+
private getTopicDisplayNames;
|
|
60
|
+
private buildSessionList;
|
|
53
61
|
private sendSessionList;
|
|
54
62
|
private broadcastSessionList;
|
|
55
63
|
private clientId;
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
27
27
|
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
28
|
+
import fs from 'node:fs';
|
|
29
|
+
import path from 'node:path';
|
|
28
30
|
export class WebSocketManager {
|
|
29
31
|
wss;
|
|
30
32
|
clients = new Map();
|
|
@@ -35,10 +37,14 @@ export class WebSocketManager {
|
|
|
35
37
|
sessionManager;
|
|
36
38
|
state;
|
|
37
39
|
authToken;
|
|
40
|
+
registryPath;
|
|
38
41
|
constructor(options) {
|
|
39
42
|
this.sessionManager = options.sessionManager;
|
|
40
43
|
this.state = options.state;
|
|
41
44
|
this.authToken = options.authToken;
|
|
45
|
+
if (options.instarDir) {
|
|
46
|
+
this.registryPath = path.join(options.instarDir, 'topic-session-registry.json');
|
|
47
|
+
}
|
|
42
48
|
this.wss = new WebSocketServer({
|
|
43
49
|
noServer: true,
|
|
44
50
|
});
|
|
@@ -226,32 +232,52 @@ export class WebSocketManager {
|
|
|
226
232
|
}, 500);
|
|
227
233
|
this.streamInterval.unref();
|
|
228
234
|
}
|
|
229
|
-
|
|
235
|
+
/**
|
|
236
|
+
* Resolve display names by cross-referencing the topic-session registry.
|
|
237
|
+
* Maps tmux session names to their Telegram topic names.
|
|
238
|
+
*/
|
|
239
|
+
getTopicDisplayNames() {
|
|
240
|
+
const map = new Map();
|
|
241
|
+
if (!this.registryPath)
|
|
242
|
+
return map;
|
|
243
|
+
try {
|
|
244
|
+
const data = JSON.parse(fs.readFileSync(this.registryPath, 'utf-8'));
|
|
245
|
+
const topicToSession = data.topicToSession || {};
|
|
246
|
+
const topicToName = data.topicToName || {};
|
|
247
|
+
// Build reverse map: tmux session name → topic display name
|
|
248
|
+
for (const [topicId, tmuxSession] of Object.entries(topicToSession)) {
|
|
249
|
+
const name = topicToName[topicId];
|
|
250
|
+
if (name) {
|
|
251
|
+
map.set(tmuxSession, name);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
// Registry missing or corrupt — skip
|
|
257
|
+
}
|
|
258
|
+
return map;
|
|
259
|
+
}
|
|
260
|
+
buildSessionList() {
|
|
230
261
|
const running = this.sessionManager.listRunningSessions();
|
|
231
|
-
const
|
|
262
|
+
const displayNames = this.getTopicDisplayNames();
|
|
263
|
+
return running.map(s => ({
|
|
232
264
|
id: s.id,
|
|
233
|
-
name: s.name,
|
|
265
|
+
name: displayNames.get(s.tmuxSession) || s.name,
|
|
234
266
|
tmuxSession: s.tmuxSession,
|
|
235
267
|
status: s.status,
|
|
236
268
|
startedAt: s.startedAt,
|
|
237
269
|
jobSlug: s.jobSlug,
|
|
238
270
|
model: s.model,
|
|
239
271
|
}));
|
|
272
|
+
}
|
|
273
|
+
sendSessionList(ws) {
|
|
274
|
+
const sessions = this.buildSessionList();
|
|
240
275
|
this.send(ws, { type: 'sessions', sessions });
|
|
241
276
|
}
|
|
242
277
|
broadcastSessionList() {
|
|
243
278
|
if (this.clients.size === 0)
|
|
244
279
|
return;
|
|
245
|
-
const
|
|
246
|
-
const sessions = running.map(s => ({
|
|
247
|
-
id: s.id,
|
|
248
|
-
name: s.name,
|
|
249
|
-
tmuxSession: s.tmuxSession,
|
|
250
|
-
status: s.status,
|
|
251
|
-
startedAt: s.startedAt,
|
|
252
|
-
jobSlug: s.jobSlug,
|
|
253
|
-
model: s.model,
|
|
254
|
-
}));
|
|
280
|
+
const sessions = this.buildSessionList();
|
|
255
281
|
const msg = JSON.stringify({ type: 'sessions', sessions });
|
|
256
282
|
for (const client of this.clients.values()) {
|
|
257
283
|
if (client.ws.readyState === WebSocket.OPEN) {
|