mstro-app 0.2.0 → 0.3.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 (114) hide show
  1. package/PRIVACY.md +126 -0
  2. package/README.md +24 -23
  3. package/bin/commands/login.js +79 -49
  4. package/bin/mstro.js +240 -37
  5. package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
  6. package/dist/server/cli/headless/claude-invoker.js +133 -27
  7. package/dist/server/cli/headless/claude-invoker.js.map +1 -1
  8. package/dist/server/cli/headless/runner.d.ts.map +1 -1
  9. package/dist/server/cli/headless/runner.js +23 -0
  10. package/dist/server/cli/headless/runner.js.map +1 -1
  11. package/dist/server/cli/headless/stall-assessor.d.ts +3 -1
  12. package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
  13. package/dist/server/cli/headless/stall-assessor.js +20 -1
  14. package/dist/server/cli/headless/stall-assessor.js.map +1 -1
  15. package/dist/server/cli/headless/tool-watchdog.d.ts +4 -1
  16. package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
  17. package/dist/server/cli/headless/tool-watchdog.js +30 -24
  18. package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
  19. package/dist/server/cli/headless/types.d.ts +19 -1
  20. package/dist/server/cli/headless/types.d.ts.map +1 -1
  21. package/dist/server/cli/improvisation-session-manager.d.ts +28 -1
  22. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
  23. package/dist/server/cli/improvisation-session-manager.js +221 -29
  24. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  25. package/dist/server/index.js +0 -3
  26. package/dist/server/index.js.map +1 -1
  27. package/dist/server/services/analytics.d.ts.map +1 -1
  28. package/dist/server/services/analytics.js +13 -1
  29. package/dist/server/services/analytics.js.map +1 -1
  30. package/dist/server/services/platform.d.ts.map +1 -1
  31. package/dist/server/services/platform.js +13 -1
  32. package/dist/server/services/platform.js.map +1 -1
  33. package/dist/server/services/terminal/pty-manager.d.ts +2 -0
  34. package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
  35. package/dist/server/services/terminal/pty-manager.js +50 -3
  36. package/dist/server/services/terminal/pty-manager.js.map +1 -1
  37. package/dist/server/services/websocket/file-explorer-handlers.d.ts +5 -0
  38. package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -0
  39. package/dist/server/services/websocket/file-explorer-handlers.js +518 -0
  40. package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -0
  41. package/dist/server/services/websocket/git-handlers.d.ts +36 -0
  42. package/dist/server/services/websocket/git-handlers.d.ts.map +1 -0
  43. package/dist/server/services/websocket/git-handlers.js +797 -0
  44. package/dist/server/services/websocket/git-handlers.js.map +1 -0
  45. package/dist/server/services/websocket/git-pr-handlers.d.ts +4 -0
  46. package/dist/server/services/websocket/git-pr-handlers.d.ts.map +1 -0
  47. package/dist/server/services/websocket/git-pr-handlers.js +299 -0
  48. package/dist/server/services/websocket/git-pr-handlers.js.map +1 -0
  49. package/dist/server/services/websocket/git-worktree-handlers.d.ts +4 -0
  50. package/dist/server/services/websocket/git-worktree-handlers.d.ts.map +1 -0
  51. package/dist/server/services/websocket/git-worktree-handlers.js +353 -0
  52. package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -0
  53. package/dist/server/services/websocket/handler-context.d.ts +32 -0
  54. package/dist/server/services/websocket/handler-context.d.ts.map +1 -0
  55. package/dist/server/services/websocket/handler-context.js +4 -0
  56. package/dist/server/services/websocket/handler-context.js.map +1 -0
  57. package/dist/server/services/websocket/handler.d.ts +27 -359
  58. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  59. package/dist/server/services/websocket/handler.js +67 -2328
  60. package/dist/server/services/websocket/handler.js.map +1 -1
  61. package/dist/server/services/websocket/index.d.ts +1 -1
  62. package/dist/server/services/websocket/index.d.ts.map +1 -1
  63. package/dist/server/services/websocket/index.js.map +1 -1
  64. package/dist/server/services/websocket/session-handlers.d.ts +10 -0
  65. package/dist/server/services/websocket/session-handlers.d.ts.map +1 -0
  66. package/dist/server/services/websocket/session-handlers.js +507 -0
  67. package/dist/server/services/websocket/session-handlers.js.map +1 -0
  68. package/dist/server/services/websocket/settings-handlers.d.ts +6 -0
  69. package/dist/server/services/websocket/settings-handlers.d.ts.map +1 -0
  70. package/dist/server/services/websocket/settings-handlers.js +125 -0
  71. package/dist/server/services/websocket/settings-handlers.js.map +1 -0
  72. package/dist/server/services/websocket/tab-handlers.d.ts +10 -0
  73. package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -0
  74. package/dist/server/services/websocket/tab-handlers.js +131 -0
  75. package/dist/server/services/websocket/tab-handlers.js.map +1 -0
  76. package/dist/server/services/websocket/terminal-handlers.d.ts +9 -0
  77. package/dist/server/services/websocket/terminal-handlers.d.ts.map +1 -0
  78. package/dist/server/services/websocket/terminal-handlers.js +220 -0
  79. package/dist/server/services/websocket/terminal-handlers.js.map +1 -0
  80. package/dist/server/services/websocket/types.d.ts +63 -2
  81. package/dist/server/services/websocket/types.d.ts.map +1 -1
  82. package/package.json +4 -2
  83. package/server/README.md +176 -159
  84. package/server/cli/headless/claude-invoker.ts +155 -31
  85. package/server/cli/headless/output-utils.test.ts +225 -0
  86. package/server/cli/headless/runner.ts +25 -0
  87. package/server/cli/headless/stall-assessor.test.ts +165 -0
  88. package/server/cli/headless/stall-assessor.ts +25 -0
  89. package/server/cli/headless/tool-watchdog.test.ts +429 -0
  90. package/server/cli/headless/tool-watchdog.ts +33 -25
  91. package/server/cli/headless/types.ts +10 -1
  92. package/server/cli/improvisation-session-manager.ts +277 -30
  93. package/server/index.ts +0 -4
  94. package/server/mcp/README.md +59 -67
  95. package/server/mcp/bouncer-integration.test.ts +161 -0
  96. package/server/mcp/security-patterns.test.ts +258 -0
  97. package/server/services/analytics.ts +13 -1
  98. package/server/services/platform.ts +12 -1
  99. package/server/services/terminal/pty-manager.ts +53 -3
  100. package/server/services/websocket/autocomplete.test.ts +194 -0
  101. package/server/services/websocket/file-explorer-handlers.ts +587 -0
  102. package/server/services/websocket/git-handlers.ts +924 -0
  103. package/server/services/websocket/git-pr-handlers.ts +363 -0
  104. package/server/services/websocket/git-worktree-handlers.ts +403 -0
  105. package/server/services/websocket/handler-context.ts +44 -0
  106. package/server/services/websocket/handler.test.ts +1 -1
  107. package/server/services/websocket/handler.ts +83 -2678
  108. package/server/services/websocket/index.ts +1 -1
  109. package/server/services/websocket/session-handlers.ts +574 -0
  110. package/server/services/websocket/settings-handlers.ts +150 -0
  111. package/server/services/websocket/tab-handlers.ts +150 -0
  112. package/server/services/websocket/terminal-handlers.ts +277 -0
  113. package/server/services/websocket/types.ts +135 -0
  114. package/bin/release.sh +0 -110
@@ -87,6 +87,9 @@ export interface PTYSession {
87
87
  // Current dimensions
88
88
  cols: number;
89
89
  rows: number;
90
+ // Output coalescing: buffer small chunks into fewer WS messages
91
+ _outputBuffer: string;
92
+ _outputTimer: ReturnType<typeof setTimeout> | null;
90
93
  }
91
94
 
92
95
  /**
@@ -227,17 +230,55 @@ export class PTYManager extends EventEmitter {
227
230
  lastActivityAt: Date.now(),
228
231
  cols,
229
232
  rows,
233
+ _outputBuffer: '',
234
+ _outputTimer: null,
230
235
  };
231
236
  this.terminals.set(terminalId, session);
232
237
 
233
- // Handle data output
238
+ // Handle data output — coalesce small chunks to reduce WebSocket message count.
239
+ // On macOS, node-pty emits many tiny chunks (sometimes single bytes) and zsh
240
+ // wraps echoed chars in multi-part ANSI sequences (RPROMPT, syntax highlighting).
241
+ // A longer window on macOS ensures these multi-part sequences arrive as one chunk,
242
+ // which the browser's predictive echo can match correctly.
243
+ const OUTPUT_COALESCE_MS = platform() === 'darwin' ? 16 : 8;
244
+ // High-water mark: flush immediately when buffer exceeds this size
245
+ // to prevent unbounded memory growth during high-output commands (e.g. `yes`)
246
+ const OUTPUT_HIGH_WATER = 64 * 1024; // 64KB
247
+ // Maximum chunk size per WebSocket message to prevent browser overload
248
+ const OUTPUT_CHUNK_SIZE = 64 * 1024;
249
+
250
+ const flushOutputBuffer = () => {
251
+ if (session._outputTimer) {
252
+ clearTimeout(session._outputTimer);
253
+ session._outputTimer = null;
254
+ }
255
+ const buffered = session._outputBuffer;
256
+ session._outputBuffer = '';
257
+ // Chunk large output to prevent single massive WebSocket frames
258
+ for (let i = 0; i < buffered.length; i += OUTPUT_CHUNK_SIZE) {
259
+ this.emit('output', terminalId, buffered.slice(i, i + OUTPUT_CHUNK_SIZE));
260
+ }
261
+ };
262
+
234
263
  ptyProcess.onData((data: string) => {
235
264
  session.lastActivityAt = Date.now();
236
- this.emit('output', terminalId, data);
265
+ session._outputBuffer += data;
266
+ // Flush immediately if buffer exceeds high-water mark
267
+ if (session._outputBuffer.length >= OUTPUT_HIGH_WATER) {
268
+ flushOutputBuffer();
269
+ } else if (!session._outputTimer) {
270
+ session._outputTimer = setTimeout(flushOutputBuffer, OUTPUT_COALESCE_MS);
271
+ }
237
272
  });
238
273
 
239
- // Handle exit
274
+ // Handle exit — flush any buffered output first
240
275
  ptyProcess.onExit(({ exitCode }) => {
276
+ if (session._outputBuffer) {
277
+ flushOutputBuffer();
278
+ } else if (session._outputTimer) {
279
+ clearTimeout(session._outputTimer);
280
+ session._outputTimer = null;
281
+ }
241
282
  this.emit('exit', terminalId, exitCode);
242
283
  this.terminals.delete(terminalId);
243
284
  });
@@ -300,6 +341,15 @@ export class PTYManager extends EventEmitter {
300
341
 
301
342
 
302
343
  try {
344
+ // Flush any coalesced output before closing
345
+ if (session._outputTimer) {
346
+ clearTimeout(session._outputTimer);
347
+ if (session._outputBuffer) {
348
+ this.emit('output', terminalId, session._outputBuffer);
349
+ session._outputBuffer = '';
350
+ }
351
+ session._outputTimer = null;
352
+ }
303
353
  session.pty.kill();
304
354
  this.terminals.delete(terminalId);
305
355
  return true;
@@ -0,0 +1,194 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { AutocompleteService } from './autocomplete.js';
3
+
4
+ // Mock file system operations to avoid hitting real FS
5
+ vi.mock('node:fs', () => ({
6
+ existsSync: vi.fn(() => false),
7
+ readdirSync: vi.fn(() => []),
8
+ statSync: vi.fn(() => ({ isDirectory: () => false })),
9
+ }));
10
+
11
+ vi.mock('./file-utils.js', () => ({
12
+ CACHE_TTL_MS: 5000,
13
+ directoryCache: new Map(),
14
+ getFileType: vi.fn((path: string) => {
15
+ const ext = path.split('.').pop() || '';
16
+ return ext || 'unknown';
17
+ }),
18
+ isIgnored: vi.fn(() => false),
19
+ parseGitignore: vi.fn(() => []),
20
+ scanDirectoryRecursiveWithDepth: vi.fn(() => []),
21
+ }));
22
+
23
+ describe('AutocompleteService', () => {
24
+ // ========== Frecency ==========
25
+
26
+ describe('calculateFrecencyScore', () => {
27
+ it('returns 0 for unknown files', () => {
28
+ const svc = new AutocompleteService();
29
+ expect(svc.calculateFrecencyScore('nonexistent.ts')).toBe(0);
30
+ });
31
+
32
+ it('returns positive score for recently used files', () => {
33
+ const svc = new AutocompleteService();
34
+ svc.recordFileSelection('src/index.ts');
35
+ const score = svc.calculateFrecencyScore('src/index.ts');
36
+ expect(score).toBeGreaterThan(0);
37
+ });
38
+
39
+ it('increases score with more selections', () => {
40
+ const svc = new AutocompleteService();
41
+ svc.recordFileSelection('src/index.ts');
42
+ const score1 = svc.calculateFrecencyScore('src/index.ts');
43
+
44
+ svc.recordFileSelection('src/index.ts');
45
+ svc.recordFileSelection('src/index.ts');
46
+ const score3 = svc.calculateFrecencyScore('src/index.ts');
47
+
48
+ expect(score3).toBeGreaterThan(score1);
49
+ });
50
+
51
+ it('decays score over time', () => {
52
+ const svc = new AutocompleteService();
53
+
54
+ // Record selection at a specific time
55
+ const now = Date.now();
56
+ vi.spyOn(Date, 'now').mockReturnValue(now);
57
+ svc.recordFileSelection('src/index.ts');
58
+ const recentScore = svc.calculateFrecencyScore('src/index.ts');
59
+
60
+ // Move forward 8 days (past the 7-day recency window)
61
+ vi.spyOn(Date, 'now').mockReturnValue(now + 8 * 24 * 60 * 60 * 1000);
62
+ const staleScore = svc.calculateFrecencyScore('src/index.ts');
63
+
64
+ expect(staleScore).toBeLessThan(recentScore);
65
+ vi.restoreAllMocks();
66
+ });
67
+
68
+ it('handles initial frecency data in constructor', () => {
69
+ const svc = new AutocompleteService({
70
+ 'src/main.ts': { count: 5, lastUsed: Date.now() },
71
+ });
72
+ expect(svc.calculateFrecencyScore('src/main.ts')).toBeGreaterThan(0);
73
+ });
74
+ });
75
+
76
+ // ========== recordFileSelection ==========
77
+
78
+ describe('recordFileSelection', () => {
79
+ it('creates new entry for first selection', () => {
80
+ const svc = new AutocompleteService();
81
+ svc.recordFileSelection('new-file.ts');
82
+
83
+ const data = svc.getFrecencyData();
84
+ expect(data['new-file.ts']).toBeDefined();
85
+ expect(data['new-file.ts'].count).toBe(1);
86
+ });
87
+
88
+ it('increments count for existing entry', () => {
89
+ const svc = new AutocompleteService();
90
+ svc.recordFileSelection('file.ts');
91
+ svc.recordFileSelection('file.ts');
92
+ svc.recordFileSelection('file.ts');
93
+
94
+ const data = svc.getFrecencyData();
95
+ expect(data['file.ts'].count).toBe(3);
96
+ });
97
+
98
+ it('updates lastUsed timestamp', () => {
99
+ const svc = new AutocompleteService();
100
+ const before = Date.now();
101
+ svc.recordFileSelection('file.ts');
102
+ const data = svc.getFrecencyData();
103
+ expect(data['file.ts'].lastUsed).toBeGreaterThanOrEqual(before);
104
+ });
105
+ });
106
+
107
+ // ========== setFrecencyData / getFrecencyData ==========
108
+
109
+ describe('setFrecencyData / getFrecencyData', () => {
110
+ it('replaces frecency data', () => {
111
+ const svc = new AutocompleteService();
112
+ svc.recordFileSelection('old.ts');
113
+
114
+ const newData = {
115
+ 'new.ts': { count: 10, lastUsed: Date.now() },
116
+ };
117
+ svc.setFrecencyData(newData);
118
+
119
+ expect(svc.getFrecencyData()).toBe(newData);
120
+ expect(svc.calculateFrecencyScore('old.ts')).toBe(0);
121
+ expect(svc.calculateFrecencyScore('new.ts')).toBeGreaterThan(0);
122
+ });
123
+ });
124
+
125
+ // ========== getFileCompletions ==========
126
+
127
+ describe('getFileCompletions', () => {
128
+ it('returns empty array when no files match', () => {
129
+ const svc = new AutocompleteService();
130
+ const results = svc.getFileCompletions('nonexistent', '/tmp/test');
131
+ expect(results).toEqual([]);
132
+ });
133
+
134
+ it('handles @ symbol prefix', () => {
135
+ const svc = new AutocompleteService();
136
+ // Should not throw when handling @ prefix
137
+ const results = svc.getFileCompletions('@src/index', '/tmp/test');
138
+ expect(Array.isArray(results)).toBe(true);
139
+ });
140
+
141
+ it('returns empty array on error', () => {
142
+ const svc = new AutocompleteService();
143
+ // Invalid working dir should return empty (caught by try/catch)
144
+ const results = svc.getFileCompletions('test', '/nonexistent/path');
145
+ expect(results).toEqual([]);
146
+ });
147
+
148
+ it('limits results to 15', () => {
149
+ // This is tested structurally — the code slices to 15
150
+ const svc = new AutocompleteService();
151
+ const results = svc.getFileCompletions('', '/tmp/test');
152
+ expect(results.length).toBeLessThanOrEqual(15);
153
+ });
154
+ });
155
+
156
+ // ========== Scoring logic (frecency weight formula) ==========
157
+
158
+ describe('frecency scoring formula', () => {
159
+ it('uses log2 for frequency weight', () => {
160
+ const svc = new AutocompleteService();
161
+
162
+ // count=1: log2(2) = 1
163
+ svc.setFrecencyData({ 'a.ts': { count: 1, lastUsed: Date.now() } });
164
+ const score1 = svc.calculateFrecencyScore('a.ts');
165
+
166
+ // count=7: log2(8) = 3
167
+ svc.setFrecencyData({ 'a.ts': { count: 7, lastUsed: Date.now() } });
168
+ const score7 = svc.calculateFrecencyScore('a.ts');
169
+
170
+ // Score should roughly triple (3x) since frequency goes from 1 to 3
171
+ expect(score7 / score1).toBeCloseTo(3, 0);
172
+ });
173
+
174
+ it('recency weight is ~1.0 for very recent files', () => {
175
+ const svc = new AutocompleteService();
176
+ svc.setFrecencyData({ 'a.ts': { count: 1, lastUsed: Date.now() } });
177
+ const score = svc.calculateFrecencyScore('a.ts');
178
+
179
+ // With recencyWeight ≈ 1, score ≈ log2(2) * (0.3 + 0.7*1) * 100 = 100
180
+ expect(score).toBeCloseTo(100, -1);
181
+ });
182
+
183
+ it('recency weight is ~0.3 for files used > 7 days ago', () => {
184
+ const svc = new AutocompleteService();
185
+ const eightDaysAgo = Date.now() - (8 * 24 * 60 * 60 * 1000);
186
+ svc.setFrecencyData({ 'a.ts': { count: 1, lastUsed: eightDaysAgo } });
187
+ const score = svc.calculateFrecencyScore('a.ts');
188
+
189
+ // With recencyWeight = max(0, 1 - 8*24/168) = 0
190
+ // score ≈ log2(2) * (0.3 + 0.7*0) * 100 = 30
191
+ expect(score).toBeCloseTo(30, -1);
192
+ });
193
+ });
194
+ });