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.
@@ -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
- // Warn if running from npx cache (auto-updates won't work properly)
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
- if (scriptPath.includes('.npm/_npx') || scriptPath.includes('/_npx/')) {
60
- console.warn('[AutoUpdater] WARNING: Running from npx cache. Auto-updates require a global install.\n' +
61
- '[AutoUpdater] Run: npm install -g instar');
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 single-line text, uses -l (literal) flag directly.
106
- * For multi-line text, writes to a temp file and uses tmux load-buffer/paste-buffer
107
- * to avoid newlines being interpreted as Enter keypresses.
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 single-line text, uses -l (literal) flag directly.
474
- * For multi-line text, writes to a temp file and uses tmux load-buffer/paste-buffer
475
- * to avoid newlines being interpreted as Enter keypresses.
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: pipe into tmux load-buffer via stdin, then paste into pane.
482
- // This avoids newlines being treated as Enter keypresses which would
483
- // fragment the message into multiple Claude prompts.
484
- // Uses stdin pipe (load-buffer -) instead of temp files to avoid
485
- // macOS TCC "access data from other apps" permission prompts.
486
- execFileSync(this.config.tmuxPath, ['load-buffer', '-'], {
487
- encoding: 'utf-8', timeout: 5000, input: text,
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, ['paste-buffer', '-t', exactTarget, '-p'], {
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 before sending Enter.
493
- // Without this, the Enter arrives before paste processing completes and
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "0.7.53",
3
+ "version": "0.8.0",
4
4
  "description": "Persistent autonomy infrastructure for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",