mstro-app 0.1.53 → 0.1.56
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/bin/mstro.js +3 -1
- package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker.js +151 -0
- package/dist/server/cli/headless/claude-invoker.js.map +1 -1
- package/dist/server/cli/headless/runner.d.ts.map +1 -1
- package/dist/server/cli/headless/runner.js +7 -1
- package/dist/server/cli/headless/runner.js.map +1 -1
- package/dist/server/cli/headless/stall-assessor.d.ts +30 -0
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -0
- package/dist/server/cli/headless/stall-assessor.js +184 -0
- package/dist/server/cli/headless/stall-assessor.js.map +1 -0
- package/dist/server/cli/headless/types.d.ts +9 -1
- package/dist/server/cli/headless/types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +21 -2
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +65 -5
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/index.js +4 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +32 -0
- package/dist/server/mcp/bouncer-integration.js.map +1 -1
- package/dist/server/services/platform.d.ts.map +1 -1
- package/dist/server/services/platform.js +8 -5
- package/dist/server/services/platform.js.map +1 -1
- package/dist/server/services/settings.d.ts +25 -0
- package/dist/server/services/settings.d.ts.map +1 -0
- package/dist/server/services/settings.js +72 -0
- package/dist/server/services/settings.js.map +1 -0
- package/dist/server/services/websocket/autocomplete.d.ts.map +1 -1
- package/dist/server/services/websocket/autocomplete.js +12 -15
- package/dist/server/services/websocket/autocomplete.js.map +1 -1
- package/dist/server/services/websocket/handler.d.ts +99 -2
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +627 -184
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/session-registry.d.ts +38 -0
- package/dist/server/services/websocket/session-registry.d.ts.map +1 -0
- package/dist/server/services/websocket/session-registry.js +154 -0
- package/dist/server/services/websocket/session-registry.js.map +1 -0
- package/dist/server/services/websocket/types.d.ts +2 -2
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/server/cli/headless/RESEARCH.md +627 -0
- package/server/cli/headless/claude-invoker.ts +192 -1
- package/server/cli/headless/runner.ts +7 -1
- package/server/cli/headless/stall-assessor.ts +245 -0
- package/server/cli/headless/types.ts +9 -1
- package/server/cli/improvisation-session-manager.ts +73 -5
- package/server/index.ts +4 -1
- package/server/mcp/bouncer-integration.ts +32 -0
- package/server/services/platform.ts +8 -5
- package/server/services/settings.ts +89 -0
- package/server/services/websocket/autocomplete.ts +18 -14
- package/server/services/websocket/handler.ts +687 -200
- package/server/services/websocket/session-registry.ts +180 -0
- package/server/services/websocket/types.ts +31 -2
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Session Registry — Persistent tab-to-session mapping
|
|
6
|
+
*
|
|
7
|
+
* Survives WebSocket disconnections and reconnections. When a web client
|
|
8
|
+
* refreshes or a new client connects, the registry allows the handler to
|
|
9
|
+
* reattach tabs to their existing in-memory sessions (or resume from disk).
|
|
10
|
+
*
|
|
11
|
+
* Backed by .mstro/session-registry.json in the working directory.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
15
|
+
import { join } from 'node:path'
|
|
16
|
+
|
|
17
|
+
export interface RegisteredTab {
|
|
18
|
+
sessionId: string
|
|
19
|
+
tabName: string
|
|
20
|
+
createdAt: string
|
|
21
|
+
lastActivityAt: string
|
|
22
|
+
order: number
|
|
23
|
+
hasUnviewedCompletion: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface RegistryData {
|
|
27
|
+
tabs: Record<string, RegisteredTab>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class SessionRegistry {
|
|
31
|
+
private registryPath: string
|
|
32
|
+
private data: RegistryData
|
|
33
|
+
|
|
34
|
+
constructor(workingDir: string) {
|
|
35
|
+
const mstroDir = join(workingDir, '.mstro')
|
|
36
|
+
if (!existsSync(mstroDir)) {
|
|
37
|
+
mkdirSync(mstroDir, { recursive: true })
|
|
38
|
+
}
|
|
39
|
+
this.registryPath = join(mstroDir, 'session-registry.json')
|
|
40
|
+
this.data = this.load()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private load(): RegistryData {
|
|
44
|
+
try {
|
|
45
|
+
if (existsSync(this.registryPath)) {
|
|
46
|
+
const raw: RegistryData = JSON.parse(readFileSync(this.registryPath, 'utf-8'))
|
|
47
|
+
// Backfill `order` for legacy data that lacks it
|
|
48
|
+
const needsOrder = Object.values(raw.tabs).some((t) => t.order === undefined)
|
|
49
|
+
if (needsOrder) {
|
|
50
|
+
const sorted = Object.entries(raw.tabs).sort(([, a], [, b]) =>
|
|
51
|
+
(a.createdAt || '').localeCompare(b.createdAt || '')
|
|
52
|
+
)
|
|
53
|
+
sorted.forEach(([, tab], i) => {
|
|
54
|
+
if (tab.order === undefined) tab.order = i
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
// Backfill `hasUnviewedCompletion` for legacy data
|
|
58
|
+
for (const tab of Object.values(raw.tabs)) {
|
|
59
|
+
if (tab.hasUnviewedCompletion === undefined) tab.hasUnviewedCompletion = false
|
|
60
|
+
}
|
|
61
|
+
return raw
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('[SessionRegistry] Error loading registry:', error)
|
|
65
|
+
}
|
|
66
|
+
return { tabs: {} }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private save(): void {
|
|
70
|
+
try {
|
|
71
|
+
writeFileSync(this.registryPath, JSON.stringify(this.data, null, 2))
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error('[SessionRegistry] Error saving registry:', error)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private getNextOrder(): number {
|
|
78
|
+
let max = -1
|
|
79
|
+
for (const tab of Object.values(this.data.tabs)) {
|
|
80
|
+
if (tab.order > max) max = tab.order
|
|
81
|
+
}
|
|
82
|
+
return max + 1
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
registerTab(tabId: string, sessionId: string, tabName?: string): void {
|
|
86
|
+
const now = new Date().toISOString()
|
|
87
|
+
this.data.tabs[tabId] = {
|
|
88
|
+
sessionId,
|
|
89
|
+
tabName: tabName || `Chat ${this.getNextChatNumber()}`,
|
|
90
|
+
createdAt: this.data.tabs[tabId]?.createdAt || now,
|
|
91
|
+
lastActivityAt: now,
|
|
92
|
+
order: this.data.tabs[tabId]?.order ?? this.getNextOrder(),
|
|
93
|
+
hasUnviewedCompletion: this.data.tabs[tabId]?.hasUnviewedCompletion ?? false,
|
|
94
|
+
}
|
|
95
|
+
this.save()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Find the next available "Chat N" number by scanning existing tab names.
|
|
100
|
+
*/
|
|
101
|
+
private getNextChatNumber(): number {
|
|
102
|
+
const usedNumbers = new Set<number>()
|
|
103
|
+
for (const tab of Object.values(this.data.tabs)) {
|
|
104
|
+
const match = tab.tabName.match(/^Chat (\d+)$/)
|
|
105
|
+
if (match) {
|
|
106
|
+
usedNumbers.add(parseInt(match[1], 10))
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
let n = 1
|
|
110
|
+
while (usedNumbers.has(n)) n++
|
|
111
|
+
return n
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
unregisterTab(tabId: string): void {
|
|
115
|
+
delete this.data.tabs[tabId]
|
|
116
|
+
this.save()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
getTabSession(tabId: string): string | undefined {
|
|
120
|
+
return this.data.tabs[tabId]?.sessionId
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
getTab(tabId: string): RegisteredTab | undefined {
|
|
124
|
+
return this.data.tabs[tabId]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
getAllTabs(): Record<string, RegisteredTab> {
|
|
128
|
+
return { ...this.data.tabs }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
updateTabName(tabId: string, name: string): void {
|
|
132
|
+
if (this.data.tabs[tabId]) {
|
|
133
|
+
this.data.tabs[tabId].tabName = name
|
|
134
|
+
this.save()
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
touchTab(tabId: string): void {
|
|
139
|
+
if (this.data.tabs[tabId]) {
|
|
140
|
+
this.data.tabs[tabId].lastActivityAt = new Date().toISOString()
|
|
141
|
+
this.save()
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Update session ID for a tab (e.g., when "new session" is started)
|
|
147
|
+
*/
|
|
148
|
+
updateTabSession(tabId: string, sessionId: string): void {
|
|
149
|
+
if (this.data.tabs[tabId]) {
|
|
150
|
+
this.data.tabs[tabId].sessionId = sessionId
|
|
151
|
+
this.data.tabs[tabId].lastActivityAt = new Date().toISOString()
|
|
152
|
+
this.save()
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
markTabViewed(tabId: string): void {
|
|
157
|
+
if (this.data.tabs[tabId]) {
|
|
158
|
+
this.data.tabs[tabId].hasUnviewedCompletion = false
|
|
159
|
+
this.save()
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
markTabUnviewed(tabId: string): void {
|
|
164
|
+
if (this.data.tabs[tabId]) {
|
|
165
|
+
this.data.tabs[tabId].hasUnviewedCompletion = true
|
|
166
|
+
this.save()
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Reorder tabs. Accepts an ordered array of tabIds and reassigns order values.
|
|
172
|
+
*/
|
|
173
|
+
reorderTabs(tabOrder: string[]): void {
|
|
174
|
+
for (let i = 0; i < tabOrder.length; i++) {
|
|
175
|
+
const tab = this.data.tabs[tabOrder[i]]
|
|
176
|
+
if (tab) tab.order = i
|
|
177
|
+
}
|
|
178
|
+
this.save()
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -56,6 +56,7 @@ export interface WebSocketMessage {
|
|
|
56
56
|
| 'createDirectory'
|
|
57
57
|
| 'deleteFile'
|
|
58
58
|
| 'renameFile'
|
|
59
|
+
| 'notifyFileOpened'
|
|
59
60
|
// Git message types
|
|
60
61
|
| 'gitStatus'
|
|
61
62
|
| 'gitStage'
|
|
@@ -65,7 +66,18 @@ export interface WebSocketMessage {
|
|
|
65
66
|
| 'gitPush'
|
|
66
67
|
| 'gitLog'
|
|
67
68
|
| 'gitDiscoverRepos'
|
|
68
|
-
| 'gitSetDirectory'
|
|
69
|
+
| 'gitSetDirectory'
|
|
70
|
+
// Session sync message types
|
|
71
|
+
| 'getActiveTabs'
|
|
72
|
+
| 'createTab'
|
|
73
|
+
| 'reorderTabs'
|
|
74
|
+
| 'syncTabMeta'
|
|
75
|
+
| 'syncPromptText'
|
|
76
|
+
| 'removeTab'
|
|
77
|
+
| 'markTabViewed'
|
|
78
|
+
// Settings message types
|
|
79
|
+
| 'getSettings'
|
|
80
|
+
| 'updateSettings';
|
|
69
81
|
tabId?: string;
|
|
70
82
|
terminalId?: string;
|
|
71
83
|
data?: any;
|
|
@@ -109,6 +121,11 @@ export interface WebSocketResponse {
|
|
|
109
121
|
| 'directoryCreated'
|
|
110
122
|
| 'fileDeleted'
|
|
111
123
|
| 'fileRenamed'
|
|
124
|
+
| 'fileOpened'
|
|
125
|
+
| 'fileContentChanged'
|
|
126
|
+
// Terminal sync response types
|
|
127
|
+
| 'terminalCreated'
|
|
128
|
+
| 'terminalClosed'
|
|
112
129
|
// Git response types
|
|
113
130
|
| 'gitStatus'
|
|
114
131
|
| 'gitStaged'
|
|
@@ -119,7 +136,19 @@ export interface WebSocketResponse {
|
|
|
119
136
|
| 'gitLog'
|
|
120
137
|
| 'gitError'
|
|
121
138
|
| 'gitReposDiscovered'
|
|
122
|
-
| 'gitDirectorySet'
|
|
139
|
+
| 'gitDirectorySet'
|
|
140
|
+
// Session sync response types
|
|
141
|
+
| 'activeTabs'
|
|
142
|
+
| 'tabCreated'
|
|
143
|
+
| 'tabRemoved'
|
|
144
|
+
| 'tabRenamed'
|
|
145
|
+
| 'tabsReordered'
|
|
146
|
+
| 'promptTextSync'
|
|
147
|
+
| 'tabViewed'
|
|
148
|
+
| 'tabStateChanged'
|
|
149
|
+
// Settings response types
|
|
150
|
+
| 'settings'
|
|
151
|
+
| 'settingsUpdated';
|
|
123
152
|
tabId?: string;
|
|
124
153
|
terminalId?: string;
|
|
125
154
|
data?: any;
|