kaizenai 0.1.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/LICENSE +22 -0
- package/README.md +246 -0
- package/bin/kaizen +15 -0
- package/dist/client/apple-touch-icon.png +0 -0
- package/dist/client/assets/index-D-ORCGrq.js +603 -0
- package/dist/client/assets/index-r28mcHqz.css +32 -0
- package/dist/client/favicon.png +0 -0
- package/dist/client/fonts/body-medium.woff2 +0 -0
- package/dist/client/fonts/body-regular-italic.woff2 +0 -0
- package/dist/client/fonts/body-regular.woff2 +0 -0
- package/dist/client/fonts/body-semibold.woff2 +0 -0
- package/dist/client/index.html +22 -0
- package/dist/client/manifest-dark.webmanifest +24 -0
- package/dist/client/manifest.webmanifest +24 -0
- package/dist/client/pwa-192.png +0 -0
- package/dist/client/pwa-512.png +0 -0
- package/dist/client/pwa-icon.svg +15 -0
- package/dist/client/pwa-splash.png +0 -0
- package/dist/client/pwa-splash.svg +15 -0
- package/package.json +103 -0
- package/src/server/acp-shared.ts +315 -0
- package/src/server/agent.ts +1120 -0
- package/src/server/attachments.ts +133 -0
- package/src/server/backgrounds.ts +74 -0
- package/src/server/cli-runtime.ts +333 -0
- package/src/server/cli-supervisor.ts +81 -0
- package/src/server/cli.ts +68 -0
- package/src/server/codex-app-server-protocol.ts +453 -0
- package/src/server/codex-app-server.ts +1350 -0
- package/src/server/cursor-acp.ts +819 -0
- package/src/server/discovery.ts +322 -0
- package/src/server/event-store.ts +1369 -0
- package/src/server/events.ts +244 -0
- package/src/server/external-open.ts +272 -0
- package/src/server/gemini-acp.ts +844 -0
- package/src/server/gemini-cli.ts +525 -0
- package/src/server/generate-title.ts +36 -0
- package/src/server/git-manager.ts +79 -0
- package/src/server/git-repository.ts +101 -0
- package/src/server/harness-types.ts +20 -0
- package/src/server/keybindings.ts +177 -0
- package/src/server/machine-name.ts +22 -0
- package/src/server/paths.ts +112 -0
- package/src/server/process-utils.ts +22 -0
- package/src/server/project-icon.ts +344 -0
- package/src/server/project-metadata.ts +10 -0
- package/src/server/provider-catalog.ts +85 -0
- package/src/server/provider-settings.ts +155 -0
- package/src/server/quick-response.ts +153 -0
- package/src/server/read-models.ts +275 -0
- package/src/server/recovery.ts +507 -0
- package/src/server/restart.ts +30 -0
- package/src/server/server.ts +244 -0
- package/src/server/terminal-manager.ts +350 -0
- package/src/server/theme-settings.ts +179 -0
- package/src/server/update-manager.ts +230 -0
- package/src/server/usage/base-provider-usage.ts +57 -0
- package/src/server/usage/claude-usage.ts +558 -0
- package/src/server/usage/codex-usage.ts +144 -0
- package/src/server/usage/cursor-browser.ts +120 -0
- package/src/server/usage/cursor-cookies.ts +390 -0
- package/src/server/usage/cursor-usage.ts +490 -0
- package/src/server/usage/gemini-usage.ts +24 -0
- package/src/server/usage/provider-usage.ts +61 -0
- package/src/server/usage/test-helpers.ts +9 -0
- package/src/server/usage/types.ts +54 -0
- package/src/server/usage/utils.ts +325 -0
- package/src/server/ws-router.ts +717 -0
- package/src/shared/branding.ts +83 -0
- package/src/shared/dev-ports.ts +43 -0
- package/src/shared/ports.ts +2 -0
- package/src/shared/protocol.ts +152 -0
- package/src/shared/tools.ts +251 -0
- package/src/shared/types.ts +1028 -0
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
import type { ServerWebSocket } from "bun"
|
|
2
|
+
import { DEFAULT_PROVIDER_SETTINGS, PROTOCOL_VERSION, getSelectableProviders, isProviderSelectable, type AgentProvider } from "../shared/types"
|
|
3
|
+
import type { ClientEnvelope, ServerEnvelope, SubscriptionTopic } from "../shared/protocol"
|
|
4
|
+
import { isClientEnvelope } from "../shared/protocol"
|
|
5
|
+
import type { AgentCoordinator } from "./agent"
|
|
6
|
+
import type { DiscoveredProject } from "./discovery"
|
|
7
|
+
import { EventStore } from "./event-store"
|
|
8
|
+
import { openExternal, openUrl } from "./external-open"
|
|
9
|
+
import { GitManager } from "./git-manager"
|
|
10
|
+
import { KeybindingsManager } from "./keybindings"
|
|
11
|
+
import { ProviderSettingsManager } from "./provider-settings"
|
|
12
|
+
import { ThemeSettingsManager } from "./theme-settings"
|
|
13
|
+
import { listProjectDirectories, requireProjectDirectory, ensureProjectDirectory } from "./paths"
|
|
14
|
+
import { importProjectHistory } from "./recovery"
|
|
15
|
+
import { TerminalManager } from "./terminal-manager"
|
|
16
|
+
import type { UpdateManager } from "./update-manager"
|
|
17
|
+
import { deriveChatSnapshot, deriveLocalProjectsSnapshot, deriveSidebarData } from "./read-models"
|
|
18
|
+
import { refreshClaudeRateLimitFromCli } from "./usage/claude-usage"
|
|
19
|
+
import { importCursorUsageFromCurl, refreshCursorUsage, signInToCursorWithBrowser } from "./usage/cursor-usage"
|
|
20
|
+
|
|
21
|
+
export interface ClientState {
|
|
22
|
+
subscriptions: Map<string, SubscriptionTopic>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const PROVIDER_USAGE_POLL_INTERVAL_MS = 30 * 60 * 1000
|
|
26
|
+
const PROVIDER_USAGE_POLL_MAX_INTERVAL_MS = 31 * 60 * 1000
|
|
27
|
+
|
|
28
|
+
interface CreateWsRouterArgs {
|
|
29
|
+
store: EventStore
|
|
30
|
+
agent: AgentCoordinator
|
|
31
|
+
terminals: TerminalManager
|
|
32
|
+
git: GitManager
|
|
33
|
+
keybindings: KeybindingsManager
|
|
34
|
+
providerSettings?: ProviderSettingsManager
|
|
35
|
+
themeSettings: ThemeSettingsManager
|
|
36
|
+
refreshDiscovery: () => Promise<DiscoveredProject[]>
|
|
37
|
+
getDiscoveredProjects: () => DiscoveredProject[]
|
|
38
|
+
machineDisplayName: string
|
|
39
|
+
updateManager: UpdateManager | null
|
|
40
|
+
providerUsagePollIntervalMs?: number
|
|
41
|
+
refreshProviderUsage?: (provider?: AgentProvider, force?: boolean) => Promise<void>
|
|
42
|
+
openUrlCommand?: typeof openUrl
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function send(ws: ServerWebSocket<ClientState>, message: ServerEnvelope) {
|
|
46
|
+
ws.send(JSON.stringify(message))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createWsRouter({
|
|
50
|
+
store,
|
|
51
|
+
agent,
|
|
52
|
+
terminals,
|
|
53
|
+
git,
|
|
54
|
+
keybindings,
|
|
55
|
+
providerSettings,
|
|
56
|
+
themeSettings,
|
|
57
|
+
refreshDiscovery,
|
|
58
|
+
getDiscoveredProjects,
|
|
59
|
+
machineDisplayName,
|
|
60
|
+
updateManager,
|
|
61
|
+
providerUsagePollIntervalMs,
|
|
62
|
+
refreshProviderUsage = async (provider, force) => {
|
|
63
|
+
const selectableProviders = getSelectableProviders(providerSettings?.getSnapshot().settings ?? DEFAULT_PROVIDER_SETTINGS).map((entry) => entry.id)
|
|
64
|
+
if (((!provider && selectableProviders.includes("claude")) || provider === "claude") && selectableProviders.includes("claude")) {
|
|
65
|
+
await refreshClaudeRateLimitFromCli(store.dataDir, undefined, force).then(() => {})
|
|
66
|
+
}
|
|
67
|
+
if (((!provider && selectableProviders.includes("cursor")) || provider === "cursor") && selectableProviders.includes("cursor")) {
|
|
68
|
+
await refreshCursorUsage(store.dataDir, undefined, force).then(() => {})
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
openUrlCommand = openUrl,
|
|
72
|
+
}: CreateWsRouterArgs) {
|
|
73
|
+
const sockets = new Set<ServerWebSocket<ClientState>>()
|
|
74
|
+
let providerUsageRefreshInFlight: Promise<void> | null = null
|
|
75
|
+
let providerUsagePollTimer: Timer | null = null
|
|
76
|
+
|
|
77
|
+
function createEnvelope(id: string, topic: SubscriptionTopic): ServerEnvelope {
|
|
78
|
+
if (topic.type === "sidebar") {
|
|
79
|
+
return {
|
|
80
|
+
v: PROTOCOL_VERSION,
|
|
81
|
+
type: "snapshot",
|
|
82
|
+
id,
|
|
83
|
+
snapshot: {
|
|
84
|
+
type: "sidebar",
|
|
85
|
+
data: deriveSidebarData(store.state, agent.getActiveStatuses(), agent.getProviderUsage()),
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (topic.type === "local-projects") {
|
|
91
|
+
const discoveredProjects = getDiscoveredProjects()
|
|
92
|
+
const data = deriveLocalProjectsSnapshot(store.state, discoveredProjects, machineDisplayName)
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
v: PROTOCOL_VERSION,
|
|
96
|
+
type: "snapshot",
|
|
97
|
+
id,
|
|
98
|
+
snapshot: {
|
|
99
|
+
type: "local-projects",
|
|
100
|
+
data,
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (topic.type === "keybindings") {
|
|
106
|
+
return {
|
|
107
|
+
v: PROTOCOL_VERSION,
|
|
108
|
+
type: "snapshot",
|
|
109
|
+
id,
|
|
110
|
+
snapshot: {
|
|
111
|
+
type: "keybindings",
|
|
112
|
+
data: keybindings.getSnapshot(),
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (topic.type === "theme-settings") {
|
|
118
|
+
return {
|
|
119
|
+
v: PROTOCOL_VERSION,
|
|
120
|
+
type: "snapshot",
|
|
121
|
+
id,
|
|
122
|
+
snapshot: {
|
|
123
|
+
type: "theme-settings",
|
|
124
|
+
data: themeSettings.getSnapshot(),
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (topic.type === "provider-settings") {
|
|
130
|
+
return {
|
|
131
|
+
v: PROTOCOL_VERSION,
|
|
132
|
+
type: "snapshot",
|
|
133
|
+
id,
|
|
134
|
+
snapshot: {
|
|
135
|
+
type: "provider-settings",
|
|
136
|
+
data: providerSettings?.getSnapshot() ?? {
|
|
137
|
+
settings: DEFAULT_PROVIDER_SETTINGS,
|
|
138
|
+
warning: null,
|
|
139
|
+
filePathDisplay: "",
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (topic.type === "update") {
|
|
146
|
+
return {
|
|
147
|
+
v: PROTOCOL_VERSION,
|
|
148
|
+
type: "snapshot",
|
|
149
|
+
id,
|
|
150
|
+
snapshot: {
|
|
151
|
+
type: "update",
|
|
152
|
+
data: updateManager?.getSnapshot() ?? {
|
|
153
|
+
currentVersion: "unknown",
|
|
154
|
+
latestVersion: null,
|
|
155
|
+
status: "idle",
|
|
156
|
+
updateAvailable: false,
|
|
157
|
+
lastCheckedAt: null,
|
|
158
|
+
error: null,
|
|
159
|
+
installAction: "restart",
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (topic.type === "terminal") {
|
|
166
|
+
return {
|
|
167
|
+
v: PROTOCOL_VERSION,
|
|
168
|
+
type: "snapshot",
|
|
169
|
+
id,
|
|
170
|
+
snapshot: {
|
|
171
|
+
type: "terminal",
|
|
172
|
+
data: terminals.getSnapshot(topic.terminalId),
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
v: PROTOCOL_VERSION,
|
|
179
|
+
type: "snapshot",
|
|
180
|
+
id,
|
|
181
|
+
snapshot: {
|
|
182
|
+
type: "chat",
|
|
183
|
+
data: (() => {
|
|
184
|
+
return deriveChatSnapshot(
|
|
185
|
+
store.state,
|
|
186
|
+
agent.getActiveStatuses(),
|
|
187
|
+
topic.chatId,
|
|
188
|
+
(chatId) => store.getMessages(chatId),
|
|
189
|
+
agent.getChatPendingTool(topic.chatId),
|
|
190
|
+
agent.getLiveUsage(topic.chatId),
|
|
191
|
+
providerSettings?.getSnapshot().settings ?? DEFAULT_PROVIDER_SETTINGS
|
|
192
|
+
)
|
|
193
|
+
})(),
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function pushSnapshots(ws: ServerWebSocket<ClientState>) {
|
|
199
|
+
for (const [id, topic] of ws.data.subscriptions.entries()) {
|
|
200
|
+
send(ws, createEnvelope(id, topic))
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function broadcastSnapshots() {
|
|
205
|
+
for (const ws of sockets) {
|
|
206
|
+
pushSnapshots(ws)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function broadcastSidebarSnapshots() {
|
|
211
|
+
for (const ws of sockets) {
|
|
212
|
+
for (const [id, topic] of ws.data.subscriptions.entries()) {
|
|
213
|
+
if (topic.type !== "sidebar") continue
|
|
214
|
+
send(ws, createEnvelope(id, topic))
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function hasSidebarSubscribers() {
|
|
220
|
+
for (const ws of sockets) {
|
|
221
|
+
for (const topic of ws.data.subscriptions.values()) {
|
|
222
|
+
if (topic.type === "sidebar") return true
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return false
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function pushTerminalSnapshot(terminalId: string) {
|
|
229
|
+
for (const ws of sockets) {
|
|
230
|
+
for (const [id, topic] of ws.data.subscriptions.entries()) {
|
|
231
|
+
if (topic.type !== "terminal" || topic.terminalId !== terminalId) continue
|
|
232
|
+
send(ws, createEnvelope(id, topic))
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function pushTerminalEvent(terminalId: string, event: Extract<ServerEnvelope, { type: "event" }>["event"]) {
|
|
238
|
+
for (const ws of sockets) {
|
|
239
|
+
for (const [id, topic] of ws.data.subscriptions.entries()) {
|
|
240
|
+
if (topic.type !== "terminal" || topic.terminalId !== terminalId) continue
|
|
241
|
+
send(ws, {
|
|
242
|
+
v: PROTOCOL_VERSION,
|
|
243
|
+
type: "event",
|
|
244
|
+
id,
|
|
245
|
+
event,
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const disposeTerminalEvents = terminals.onEvent((event) => {
|
|
252
|
+
pushTerminalEvent(event.terminalId, event)
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
const disposeKeybindingEvents = keybindings.onChange(() => {
|
|
256
|
+
for (const ws of sockets) {
|
|
257
|
+
for (const [id, topic] of ws.data.subscriptions.entries()) {
|
|
258
|
+
if (topic.type !== "keybindings") continue
|
|
259
|
+
send(ws, createEnvelope(id, topic))
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
const disposeThemeSettingsEvents = themeSettings.onChange(() => {
|
|
265
|
+
for (const ws of sockets) {
|
|
266
|
+
for (const [id, topic] of ws.data.subscriptions.entries()) {
|
|
267
|
+
if (topic.type !== "theme-settings") continue
|
|
268
|
+
send(ws, createEnvelope(id, topic))
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
const disposeProviderSettingsEvents = providerSettings?.onChange(() => {
|
|
274
|
+
for (const ws of sockets) {
|
|
275
|
+
for (const [id, topic] of ws.data.subscriptions.entries()) {
|
|
276
|
+
if (topic.type !== "provider-settings") continue
|
|
277
|
+
send(ws, createEnvelope(id, topic))
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
broadcastSnapshots()
|
|
281
|
+
}) ?? (() => {})
|
|
282
|
+
|
|
283
|
+
const disposeUpdateEvents = updateManager?.onChange(() => {
|
|
284
|
+
for (const ws of sockets) {
|
|
285
|
+
for (const [id, topic] of ws.data.subscriptions.entries()) {
|
|
286
|
+
if (topic.type !== "update") continue
|
|
287
|
+
send(ws, createEnvelope(id, topic))
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}) ?? (() => {})
|
|
291
|
+
|
|
292
|
+
async function runProviderUsagePoll() {
|
|
293
|
+
if (!hasSidebarSubscribers()) return
|
|
294
|
+
if (!providerUsageRefreshInFlight) {
|
|
295
|
+
providerUsageRefreshInFlight = refreshProviderUsage().finally(() => {
|
|
296
|
+
providerUsageRefreshInFlight = null
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
await providerUsageRefreshInFlight
|
|
300
|
+
broadcastSidebarSnapshots()
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function nextProviderUsagePollDelayMs() {
|
|
304
|
+
if (typeof providerUsagePollIntervalMs === "number") {
|
|
305
|
+
return providerUsagePollIntervalMs
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return Math.floor(Math.random() * (PROVIDER_USAGE_POLL_MAX_INTERVAL_MS - PROVIDER_USAGE_POLL_INTERVAL_MS + 1))
|
|
309
|
+
+ PROVIDER_USAGE_POLL_INTERVAL_MS
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function scheduleProviderUsagePoll() {
|
|
313
|
+
if (providerUsagePollTimer) {
|
|
314
|
+
clearTimeout(providerUsagePollTimer)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
providerUsagePollTimer = setTimeout(() => {
|
|
318
|
+
void runProviderUsagePoll().finally(() => {
|
|
319
|
+
scheduleProviderUsagePoll()
|
|
320
|
+
})
|
|
321
|
+
}, nextProviderUsagePollDelayMs())
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
scheduleProviderUsagePoll()
|
|
325
|
+
|
|
326
|
+
async function handleCommand(ws: ServerWebSocket<ClientState>, message: Extract<ClientEnvelope, { type: "command" }>) {
|
|
327
|
+
const { command, id } = message
|
|
328
|
+
try {
|
|
329
|
+
switch (command.type) {
|
|
330
|
+
case "system.ping": {
|
|
331
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
332
|
+
return
|
|
333
|
+
}
|
|
334
|
+
case "system.listDirectory": {
|
|
335
|
+
const directory = await listProjectDirectories(command.localPath)
|
|
336
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: directory })
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
case "update.check": {
|
|
340
|
+
const snapshot = updateManager
|
|
341
|
+
? await updateManager.checkForUpdates({ force: command.force })
|
|
342
|
+
: {
|
|
343
|
+
currentVersion: "unknown",
|
|
344
|
+
latestVersion: null,
|
|
345
|
+
status: "error",
|
|
346
|
+
updateAvailable: false,
|
|
347
|
+
lastCheckedAt: Date.now(),
|
|
348
|
+
error: "Update manager unavailable.",
|
|
349
|
+
installAction: "restart",
|
|
350
|
+
}
|
|
351
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
|
|
352
|
+
return
|
|
353
|
+
}
|
|
354
|
+
case "update.install": {
|
|
355
|
+
if (!updateManager) {
|
|
356
|
+
throw new Error("Update manager unavailable.")
|
|
357
|
+
}
|
|
358
|
+
const result = await updateManager.installUpdate()
|
|
359
|
+
send(ws, {
|
|
360
|
+
v: PROTOCOL_VERSION,
|
|
361
|
+
type: "ack",
|
|
362
|
+
id,
|
|
363
|
+
result,
|
|
364
|
+
})
|
|
365
|
+
return
|
|
366
|
+
}
|
|
367
|
+
case "settings.readKeybindings": {
|
|
368
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: keybindings.getSnapshot() })
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
case "settings.writeKeybindings": {
|
|
372
|
+
const snapshot = await keybindings.write(command.bindings)
|
|
373
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
|
|
374
|
+
return
|
|
375
|
+
}
|
|
376
|
+
case "settings.writeThemeSettings": {
|
|
377
|
+
const snapshot = await themeSettings.write(command.settings)
|
|
378
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
|
|
379
|
+
return
|
|
380
|
+
}
|
|
381
|
+
case "settings.writeProviderSettings": {
|
|
382
|
+
if (!providerSettings) throw new Error("Provider settings unavailable.")
|
|
383
|
+
const snapshot = await providerSettings.write(command.settings)
|
|
384
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
case "project.open": {
|
|
388
|
+
await requireProjectDirectory(command.localPath)
|
|
389
|
+
await store.unhideProject(command.localPath)
|
|
390
|
+
const project = await store.openProject(command.localPath)
|
|
391
|
+
const imported = await importProjectHistory({
|
|
392
|
+
store,
|
|
393
|
+
projectId: project.id,
|
|
394
|
+
repoKey: project.repoKey,
|
|
395
|
+
localPath: project.localPath,
|
|
396
|
+
worktreePaths: project.worktreePaths,
|
|
397
|
+
})
|
|
398
|
+
await store.reconcileProjectFeatureState(project.id)
|
|
399
|
+
await refreshDiscovery()
|
|
400
|
+
send(ws, {
|
|
401
|
+
v: PROTOCOL_VERSION,
|
|
402
|
+
type: "ack",
|
|
403
|
+
id,
|
|
404
|
+
result: {
|
|
405
|
+
projectId: project.id,
|
|
406
|
+
chatId: imported.newestChatId,
|
|
407
|
+
importedChats: imported.importedChats,
|
|
408
|
+
},
|
|
409
|
+
})
|
|
410
|
+
break
|
|
411
|
+
}
|
|
412
|
+
case "project.create": {
|
|
413
|
+
await ensureProjectDirectory(command.localPath)
|
|
414
|
+
await store.unhideProject(command.localPath)
|
|
415
|
+
const project = await store.openProject(command.localPath, command.title)
|
|
416
|
+
const imported = await importProjectHistory({
|
|
417
|
+
store,
|
|
418
|
+
projectId: project.id,
|
|
419
|
+
repoKey: project.repoKey,
|
|
420
|
+
localPath: project.localPath,
|
|
421
|
+
worktreePaths: project.worktreePaths,
|
|
422
|
+
})
|
|
423
|
+
await store.reconcileProjectFeatureState(project.id)
|
|
424
|
+
await refreshDiscovery()
|
|
425
|
+
send(ws, {
|
|
426
|
+
v: PROTOCOL_VERSION,
|
|
427
|
+
type: "ack",
|
|
428
|
+
id,
|
|
429
|
+
result: {
|
|
430
|
+
projectId: project.id,
|
|
431
|
+
chatId: imported.newestChatId,
|
|
432
|
+
importedChats: imported.importedChats,
|
|
433
|
+
},
|
|
434
|
+
})
|
|
435
|
+
break
|
|
436
|
+
}
|
|
437
|
+
case "project.remove": {
|
|
438
|
+
const project = store.getProject(command.projectId)
|
|
439
|
+
for (const chat of store.listChatsByProject(command.projectId)) {
|
|
440
|
+
await agent.cancel(chat.id)
|
|
441
|
+
}
|
|
442
|
+
if (project) {
|
|
443
|
+
for (const worktreePath of project.worktreePaths) {
|
|
444
|
+
terminals.closeByCwd(worktreePath)
|
|
445
|
+
}
|
|
446
|
+
await store.hideProject(project.localPath)
|
|
447
|
+
}
|
|
448
|
+
await refreshDiscovery()
|
|
449
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
450
|
+
break
|
|
451
|
+
}
|
|
452
|
+
case "project.setBrowserState": {
|
|
453
|
+
await store.setProjectBrowserState(command.projectId, command.browserState)
|
|
454
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
455
|
+
break
|
|
456
|
+
}
|
|
457
|
+
case "project.setGeneralChatsBrowserState": {
|
|
458
|
+
await store.setProjectGeneralChatsBrowserState(command.projectId, command.browserState)
|
|
459
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
460
|
+
break
|
|
461
|
+
}
|
|
462
|
+
case "project.hide": {
|
|
463
|
+
const existingProject = store.listProjects().find((project) => project.localPath === command.localPath)
|
|
464
|
+
if (existingProject) {
|
|
465
|
+
for (const chat of store.listChatsByProject(existingProject.id)) {
|
|
466
|
+
await agent.cancel(chat.id)
|
|
467
|
+
}
|
|
468
|
+
for (const worktreePath of existingProject.worktreePaths) {
|
|
469
|
+
terminals.closeByCwd(worktreePath)
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
await store.hideProject(command.localPath)
|
|
473
|
+
await refreshDiscovery()
|
|
474
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
475
|
+
break
|
|
476
|
+
}
|
|
477
|
+
case "project.setProjectMetadataDirectoryCommitMode": {
|
|
478
|
+
const localPath = command.projectId
|
|
479
|
+
? store.getProject(command.projectId)?.localPath
|
|
480
|
+
: command.localPath
|
|
481
|
+
if (!localPath) {
|
|
482
|
+
throw new Error("Project not found")
|
|
483
|
+
}
|
|
484
|
+
await git.setProjectMetadataDirectoryCommitMode(localPath, command.commitProjectMetadata)
|
|
485
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
486
|
+
break
|
|
487
|
+
}
|
|
488
|
+
case "system.openExternal": {
|
|
489
|
+
await openExternal(command)
|
|
490
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
491
|
+
break
|
|
492
|
+
}
|
|
493
|
+
case "system.openUrl": {
|
|
494
|
+
await openUrlCommand(command)
|
|
495
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
496
|
+
break
|
|
497
|
+
}
|
|
498
|
+
case "provider.refreshUsage": {
|
|
499
|
+
if (command.provider && !isProviderSelectable(command.provider, providerSettings?.getSnapshot().settings ?? DEFAULT_PROVIDER_SETTINGS)) {
|
|
500
|
+
throw new Error(`${command.provider} is inactive.`)
|
|
501
|
+
}
|
|
502
|
+
await refreshProviderUsage(command.provider, true)
|
|
503
|
+
broadcastSidebarSnapshots()
|
|
504
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
505
|
+
break
|
|
506
|
+
}
|
|
507
|
+
case "provider.browserLogin": {
|
|
508
|
+
if (command.provider !== "cursor") {
|
|
509
|
+
throw new Error(`Browser login is not supported for provider: ${command.provider}`)
|
|
510
|
+
}
|
|
511
|
+
const result = await signInToCursorWithBrowser(store.dataDir)
|
|
512
|
+
broadcastSidebarSnapshots()
|
|
513
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
514
|
+
break
|
|
515
|
+
}
|
|
516
|
+
case "provider.importUsageCurl": {
|
|
517
|
+
if (command.provider !== "cursor") {
|
|
518
|
+
throw new Error(`cURL import is not supported for provider: ${command.provider}`)
|
|
519
|
+
}
|
|
520
|
+
const result = await importCursorUsageFromCurl(store.dataDir, command.curlCommand)
|
|
521
|
+
broadcastSidebarSnapshots()
|
|
522
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
523
|
+
break
|
|
524
|
+
}
|
|
525
|
+
case "feature.create": {
|
|
526
|
+
const feature = await store.createFeature(command.projectId, command.title, command.description ?? "")
|
|
527
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: { featureId: feature.id } })
|
|
528
|
+
break
|
|
529
|
+
}
|
|
530
|
+
case "feature.rename": {
|
|
531
|
+
await store.renameFeature(command.featureId, command.title)
|
|
532
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
533
|
+
break
|
|
534
|
+
}
|
|
535
|
+
case "feature.setBrowserState": {
|
|
536
|
+
await store.setFeatureBrowserState(command.featureId, command.browserState)
|
|
537
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
538
|
+
break
|
|
539
|
+
}
|
|
540
|
+
case "feature.setStage": {
|
|
541
|
+
await store.setFeatureStage(command.featureId, command.stage)
|
|
542
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
543
|
+
break
|
|
544
|
+
}
|
|
545
|
+
case "feature.reorder": {
|
|
546
|
+
await store.reorderFeatures(command.projectId, command.orderedFeatureIds)
|
|
547
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
548
|
+
break
|
|
549
|
+
}
|
|
550
|
+
case "feature.delete": {
|
|
551
|
+
await store.deleteFeature(command.featureId)
|
|
552
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
553
|
+
break
|
|
554
|
+
}
|
|
555
|
+
case "chat.create": {
|
|
556
|
+
// Reuse an existing empty chat for this project+feature if one exists
|
|
557
|
+
const existingChats = store.listChatsByProject(command.projectId)
|
|
558
|
+
const emptyChat = existingChats.find((chat) => {
|
|
559
|
+
if (chat.lastMessageAt != null) return false
|
|
560
|
+
const chatFeatureId = chat.featureId ?? undefined
|
|
561
|
+
return chatFeatureId === (command.featureId ?? undefined)
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
const chat = emptyChat ?? await store.createChat(command.projectId, command.featureId)
|
|
565
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: { chatId: chat.id } })
|
|
566
|
+
break
|
|
567
|
+
}
|
|
568
|
+
case "chat.setFeature": {
|
|
569
|
+
await store.setChatFeature(command.chatId, command.featureId)
|
|
570
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
571
|
+
break
|
|
572
|
+
}
|
|
573
|
+
case "chat.rename": {
|
|
574
|
+
await store.renameChat(command.chatId, command.title)
|
|
575
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
576
|
+
break
|
|
577
|
+
}
|
|
578
|
+
case "chat.delete": {
|
|
579
|
+
await agent.cancel(command.chatId)
|
|
580
|
+
await store.deleteChat(command.chatId)
|
|
581
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
582
|
+
break
|
|
583
|
+
}
|
|
584
|
+
case "chat.send": {
|
|
585
|
+
if (command.provider && !isProviderSelectable(command.provider, providerSettings?.getSnapshot().settings ?? DEFAULT_PROVIDER_SETTINGS)) {
|
|
586
|
+
throw new Error(`${command.provider} is inactive.`)
|
|
587
|
+
}
|
|
588
|
+
const result = await agent.send(command)
|
|
589
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
590
|
+
break
|
|
591
|
+
}
|
|
592
|
+
case "chat.cancel": {
|
|
593
|
+
await agent.cancel(command.chatId)
|
|
594
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
595
|
+
break
|
|
596
|
+
}
|
|
597
|
+
case "chat.respondTool": {
|
|
598
|
+
await agent.respondTool(command)
|
|
599
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
600
|
+
break
|
|
601
|
+
}
|
|
602
|
+
case "terminal.create": {
|
|
603
|
+
const project = store.getProject(command.projectId)
|
|
604
|
+
if (!project) {
|
|
605
|
+
throw new Error("Project not found")
|
|
606
|
+
}
|
|
607
|
+
const snapshot = terminals.createTerminal({
|
|
608
|
+
projectPath: project.localPath,
|
|
609
|
+
terminalId: command.terminalId,
|
|
610
|
+
cols: command.cols,
|
|
611
|
+
rows: command.rows,
|
|
612
|
+
scrollback: command.scrollback,
|
|
613
|
+
})
|
|
614
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
|
|
615
|
+
return
|
|
616
|
+
}
|
|
617
|
+
case "terminal.input": {
|
|
618
|
+
terminals.write(command.terminalId, command.data)
|
|
619
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
620
|
+
return
|
|
621
|
+
}
|
|
622
|
+
case "terminal.resize": {
|
|
623
|
+
terminals.resize(command.terminalId, command.cols, command.rows)
|
|
624
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
625
|
+
return
|
|
626
|
+
}
|
|
627
|
+
case "terminal.close": {
|
|
628
|
+
terminals.close(command.terminalId)
|
|
629
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
630
|
+
pushTerminalSnapshot(command.terminalId)
|
|
631
|
+
return
|
|
632
|
+
}
|
|
633
|
+
case "git.getBranches": {
|
|
634
|
+
const project = store.getProject(command.projectId)
|
|
635
|
+
if (!project) throw new Error("Project not found")
|
|
636
|
+
const result = await git.getBranches(project.localPath)
|
|
637
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
638
|
+
return
|
|
639
|
+
}
|
|
640
|
+
case "git.switchBranch": {
|
|
641
|
+
const project = store.getProject(command.projectId)
|
|
642
|
+
if (!project) throw new Error("Project not found")
|
|
643
|
+
const result = await git.switchBranch(project.localPath, command.branchName)
|
|
644
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
645
|
+
return
|
|
646
|
+
}
|
|
647
|
+
case "git.createBranch": {
|
|
648
|
+
const project = store.getProject(command.projectId)
|
|
649
|
+
if (!project) throw new Error("Project not found")
|
|
650
|
+
const result = await git.createBranch(project.localPath, command.branchName, command.checkout)
|
|
651
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
652
|
+
return
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
broadcastSnapshots()
|
|
657
|
+
} catch (error) {
|
|
658
|
+
const messageText = error instanceof Error ? error.message : String(error)
|
|
659
|
+
send(ws, { v: PROTOCOL_VERSION, type: "error", id, message: messageText })
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return {
|
|
664
|
+
handleOpen(ws: ServerWebSocket<ClientState>) {
|
|
665
|
+
sockets.add(ws)
|
|
666
|
+
},
|
|
667
|
+
handleClose(ws: ServerWebSocket<ClientState>) {
|
|
668
|
+
sockets.delete(ws)
|
|
669
|
+
},
|
|
670
|
+
broadcastSnapshots,
|
|
671
|
+
handleMessage(ws: ServerWebSocket<ClientState>, raw: string | Buffer | ArrayBuffer | Uint8Array) {
|
|
672
|
+
let parsed: unknown
|
|
673
|
+
try {
|
|
674
|
+
parsed = JSON.parse(String(raw))
|
|
675
|
+
} catch {
|
|
676
|
+
send(ws, { v: PROTOCOL_VERSION, type: "error", message: "Invalid JSON" })
|
|
677
|
+
return
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (!isClientEnvelope(parsed)) {
|
|
681
|
+
send(ws, { v: PROTOCOL_VERSION, type: "error", message: "Invalid envelope" })
|
|
682
|
+
return
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (parsed.type === "subscribe") {
|
|
686
|
+
ws.data.subscriptions.set(parsed.id, parsed.topic)
|
|
687
|
+
if (parsed.topic.type === "local-projects") {
|
|
688
|
+
void refreshDiscovery().then(() => {
|
|
689
|
+
if (ws.data.subscriptions.has(parsed.id)) {
|
|
690
|
+
send(ws, createEnvelope(parsed.id, parsed.topic))
|
|
691
|
+
}
|
|
692
|
+
})
|
|
693
|
+
}
|
|
694
|
+
send(ws, createEnvelope(parsed.id, parsed.topic))
|
|
695
|
+
return
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (parsed.type === "unsubscribe") {
|
|
699
|
+
ws.data.subscriptions.delete(parsed.id)
|
|
700
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id: parsed.id })
|
|
701
|
+
return
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
void handleCommand(ws, parsed)
|
|
705
|
+
},
|
|
706
|
+
dispose() {
|
|
707
|
+
if (providerUsagePollTimer) {
|
|
708
|
+
clearTimeout(providerUsagePollTimer)
|
|
709
|
+
}
|
|
710
|
+
disposeTerminalEvents()
|
|
711
|
+
disposeKeybindingEvents()
|
|
712
|
+
disposeProviderSettingsEvents()
|
|
713
|
+
disposeThemeSettingsEvents()
|
|
714
|
+
disposeUpdateEvents()
|
|
715
|
+
},
|
|
716
|
+
}
|
|
717
|
+
}
|