instar 0.7.53 → 0.8.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/dist/core/AutoUpdater.js
CHANGED
|
@@ -54,11 +54,17 @@ export class AutoUpdater {
|
|
|
54
54
|
if (this.interval)
|
|
55
55
|
return;
|
|
56
56
|
const intervalMs = this.config.checkIntervalMinutes * 60 * 1000;
|
|
57
|
-
//
|
|
57
|
+
// Detect npx cache — auto-apply and restart cause infinite loops when
|
|
58
|
+
// running from npx because the cache still resolves to the old version
|
|
59
|
+
// after npm installs the update. The restart finds the update again,
|
|
60
|
+
// applies it again, restarts again — forever, killing all sessions each time.
|
|
58
61
|
const scriptPath = process.argv[1] || '';
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
const runningFromNpx = scriptPath.includes('.npm/_npx') || scriptPath.includes('/_npx/');
|
|
63
|
+
if (runningFromNpx) {
|
|
64
|
+
this.config.autoApply = false;
|
|
65
|
+
this.config.autoRestart = false;
|
|
66
|
+
console.warn('[AutoUpdater] Running from npx cache. Auto-apply and auto-restart disabled to prevent restart loops.\n' +
|
|
67
|
+
'[AutoUpdater] Run: npm install -g instar (then restart with: instar server start)');
|
|
62
68
|
}
|
|
63
69
|
console.log(`[AutoUpdater] Started (every ${this.config.checkIntervalMinutes}m, ` +
|
|
64
70
|
`autoApply: ${this.config.autoApply}, autoRestart: ${this.config.autoRestart})`);
|
|
@@ -102,9 +102,10 @@ export declare class SessionManager extends EventEmitter {
|
|
|
102
102
|
injectTelegramMessage(tmuxSession: string, topicId: number, text: string): void;
|
|
103
103
|
/**
|
|
104
104
|
* Send text to a tmux session via send-keys.
|
|
105
|
-
* For
|
|
106
|
-
*
|
|
107
|
-
*
|
|
105
|
+
* For multi-line text, uses bracketed paste mode escape sequences so the
|
|
106
|
+
* terminal treats newlines as literal text rather than Enter keypresses.
|
|
107
|
+
* This avoids tmux load-buffer/paste-buffer which trigger macOS TCC
|
|
108
|
+
* "access data from other apps" permission prompts.
|
|
108
109
|
*/
|
|
109
110
|
private injectMessage;
|
|
110
111
|
/**
|
|
@@ -470,29 +470,30 @@ export class SessionManager extends EventEmitter {
|
|
|
470
470
|
}
|
|
471
471
|
/**
|
|
472
472
|
* Send text to a tmux session via send-keys.
|
|
473
|
-
* For
|
|
474
|
-
*
|
|
475
|
-
*
|
|
473
|
+
* For multi-line text, uses bracketed paste mode escape sequences so the
|
|
474
|
+
* terminal treats newlines as literal text rather than Enter keypresses.
|
|
475
|
+
* This avoids tmux load-buffer/paste-buffer which trigger macOS TCC
|
|
476
|
+
* "access data from other apps" permission prompts.
|
|
476
477
|
*/
|
|
477
478
|
injectMessage(tmuxSession, text) {
|
|
478
479
|
const exactTarget = `=${tmuxSession}:`;
|
|
479
480
|
try {
|
|
480
481
|
if (text.includes('\n')) {
|
|
481
|
-
// Multi-line:
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
482
|
+
// Multi-line: use bracketed paste mode.
|
|
483
|
+
// The terminal (and Claude Code's readline) treats everything between
|
|
484
|
+
// \e[200~ and \e[201~ as a single paste — newlines are literal, not Enter.
|
|
485
|
+
// This completely avoids load-buffer/paste-buffer and their TCC prompts.
|
|
486
|
+
execFileSync(this.config.tmuxPath, ['send-keys', '-t', exactTarget, '\x1b[200~'], {
|
|
487
|
+
encoding: 'utf-8', timeout: 5000,
|
|
488
|
+
});
|
|
489
|
+
execFileSync(this.config.tmuxPath, ['send-keys', '-t', exactTarget, '-l', text], {
|
|
490
|
+
encoding: 'utf-8', timeout: 5000,
|
|
488
491
|
});
|
|
489
|
-
execFileSync(this.config.tmuxPath, ['
|
|
492
|
+
execFileSync(this.config.tmuxPath, ['send-keys', '-t', exactTarget, '\x1b[201~'], {
|
|
490
493
|
encoding: 'utf-8', timeout: 5000,
|
|
491
494
|
});
|
|
492
|
-
// Brief delay to let the terminal process the paste
|
|
493
|
-
|
|
494
|
-
// the message sits in the input buffer without being submitted.
|
|
495
|
-
execFileSync('/bin/sleep', ['0.3'], { timeout: 2000 });
|
|
495
|
+
// Brief delay to let the terminal process the bracketed paste
|
|
496
|
+
execFileSync('/bin/sleep', ['0.1'], { timeout: 2000 });
|
|
496
497
|
// Send Enter to submit
|
|
497
498
|
execFileSync(this.config.tmuxPath, ['send-keys', '-t', exactTarget, 'Enter'], {
|
|
498
499
|
encoding: 'utf-8', timeout: 5000,
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides health checks, session management, job triggering,
|
|
5
5
|
* and event querying over a simple REST API.
|
|
6
|
+
*
|
|
7
|
+
* Also serves the dashboard UI at /dashboard and handles
|
|
8
|
+
* WebSocket connections for real-time terminal streaming.
|
|
6
9
|
*/
|
|
7
10
|
import { type Express } from 'express';
|
|
8
11
|
import type { SessionManager } from '../core/SessionManager.js';
|
|
@@ -25,8 +28,11 @@ import type { SessionWatchdog } from '../monitoring/SessionWatchdog.js';
|
|
|
25
28
|
export declare class AgentServer {
|
|
26
29
|
private app;
|
|
27
30
|
private server;
|
|
31
|
+
private wsManager;
|
|
28
32
|
private config;
|
|
29
33
|
private startTime;
|
|
34
|
+
private sessionManager;
|
|
35
|
+
private state;
|
|
30
36
|
constructor(options: {
|
|
31
37
|
config: InstarConfig;
|
|
32
38
|
sessionManager: SessionManager;
|
|
@@ -46,6 +52,12 @@ export declare class AgentServer {
|
|
|
46
52
|
evolution?: EvolutionManager;
|
|
47
53
|
watchdog?: SessionWatchdog;
|
|
48
54
|
});
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the dashboard directory.
|
|
57
|
+
* In dev: ../../../dashboard (relative to src/server/)
|
|
58
|
+
* In dist (published): ../../dashboard (relative to dist/server/)
|
|
59
|
+
*/
|
|
60
|
+
private resolveDashboardDir;
|
|
49
61
|
/**
|
|
50
62
|
* Start the HTTP server.
|
|
51
63
|
*/
|
|
@@ -3,22 +3,41 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides health checks, session management, job triggering,
|
|
5
5
|
* and event querying over a simple REST API.
|
|
6
|
+
*
|
|
7
|
+
* Also serves the dashboard UI at /dashboard and handles
|
|
8
|
+
* WebSocket connections for real-time terminal streaming.
|
|
6
9
|
*/
|
|
7
10
|
import express from 'express';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
8
14
|
import { createRoutes } from './routes.js';
|
|
9
15
|
import { corsMiddleware, authMiddleware, requestTimeout, errorHandler } from './middleware.js';
|
|
16
|
+
import { WebSocketManager } from './WebSocketManager.js';
|
|
10
17
|
export class AgentServer {
|
|
11
18
|
app;
|
|
12
19
|
server = null;
|
|
20
|
+
wsManager = null;
|
|
13
21
|
config;
|
|
14
22
|
startTime;
|
|
23
|
+
sessionManager;
|
|
24
|
+
state;
|
|
15
25
|
constructor(options) {
|
|
16
26
|
this.config = options.config;
|
|
17
27
|
this.startTime = new Date();
|
|
28
|
+
this.sessionManager = options.sessionManager;
|
|
29
|
+
this.state = options.state;
|
|
18
30
|
this.app = express();
|
|
19
31
|
// Middleware
|
|
20
32
|
this.app.use(express.json({ limit: '1mb' }));
|
|
21
33
|
this.app.use(corsMiddleware);
|
|
34
|
+
// Dashboard static files — served BEFORE auth middleware so the page loads
|
|
35
|
+
// without a token. Auth happens via WebSocket/API calls from the page itself.
|
|
36
|
+
const dashboardDir = this.resolveDashboardDir();
|
|
37
|
+
this.app.get('/dashboard', (_req, res) => {
|
|
38
|
+
res.sendFile(path.join(dashboardDir, 'index.html'));
|
|
39
|
+
});
|
|
40
|
+
this.app.use('/dashboard', express.static(dashboardDir));
|
|
22
41
|
this.app.use(authMiddleware(options.config.authToken));
|
|
23
42
|
this.app.use(requestTimeout(options.config.requestTimeoutMs));
|
|
24
43
|
// Routes
|
|
@@ -46,6 +65,23 @@ export class AgentServer {
|
|
|
46
65
|
// Error handler (must be last)
|
|
47
66
|
this.app.use(errorHandler);
|
|
48
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Resolve the dashboard directory.
|
|
70
|
+
* In dev: ../../../dashboard (relative to src/server/)
|
|
71
|
+
* In dist (published): ../../dashboard (relative to dist/server/)
|
|
72
|
+
*/
|
|
73
|
+
resolveDashboardDir() {
|
|
74
|
+
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
75
|
+
// Try dist layout first (package root/dashboard)
|
|
76
|
+
const fromDist = path.resolve(thisDir, '..', '..', 'dashboard');
|
|
77
|
+
// Try dev layout (src/server -> project root/dashboard)
|
|
78
|
+
const fromSrc = path.resolve(thisDir, '..', '..', '..', 'dashboard');
|
|
79
|
+
if (fs.existsSync(fromDist))
|
|
80
|
+
return fromDist;
|
|
81
|
+
if (fs.existsSync(fromSrc))
|
|
82
|
+
return fromSrc;
|
|
83
|
+
return fromDist;
|
|
84
|
+
}
|
|
49
85
|
/**
|
|
50
86
|
* Start the HTTP server.
|
|
51
87
|
*/
|
|
@@ -54,6 +90,14 @@ export class AgentServer {
|
|
|
54
90
|
const host = this.config.host || '127.0.0.1';
|
|
55
91
|
this.server = this.app.listen(this.config.port, host, () => {
|
|
56
92
|
console.log(`[instar] Server listening on ${host}:${this.config.port}`);
|
|
93
|
+
console.log(`[instar] Dashboard: http://${host}:${this.config.port}/dashboard`);
|
|
94
|
+
// Initialize WebSocket manager after server is listening
|
|
95
|
+
this.wsManager = new WebSocketManager({
|
|
96
|
+
server: this.server,
|
|
97
|
+
sessionManager: this.sessionManager,
|
|
98
|
+
state: this.state,
|
|
99
|
+
authToken: this.config.authToken,
|
|
100
|
+
});
|
|
57
101
|
resolve();
|
|
58
102
|
});
|
|
59
103
|
this.server.on('error', (err) => {
|
|
@@ -71,6 +115,11 @@ export class AgentServer {
|
|
|
71
115
|
* Closes keep-alive connections after a timeout to prevent hanging.
|
|
72
116
|
*/
|
|
73
117
|
async stop() {
|
|
118
|
+
// Shutdown WebSocket manager first
|
|
119
|
+
if (this.wsManager) {
|
|
120
|
+
this.wsManager.shutdown();
|
|
121
|
+
this.wsManager = null;
|
|
122
|
+
}
|
|
74
123
|
return new Promise((resolve) => {
|
|
75
124
|
if (!this.server) {
|
|
76
125
|
resolve();
|