@jacexh/claude-web-console 0.2.1

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 (65) hide show
  1. package/README.md +102 -0
  2. package/bin/cli.js +64 -0
  3. package/claude-web-console.service +26 -0
  4. package/client/dist/assets/_baseUniq-Cnoz3HXL.js +1 -0
  5. package/client/dist/assets/arc-Z_2trbN8.js +1 -0
  6. package/client/dist/assets/architectureDiagram-Q4EWVU46-BkJc1Y5A.js +36 -0
  7. package/client/dist/assets/blockDiagram-DXYQGD6D-DajeZur_.js +132 -0
  8. package/client/dist/assets/c4Diagram-AHTNJAMY-DPrSiSIe.js +10 -0
  9. package/client/dist/assets/channel-xmJgF8AT.js +1 -0
  10. package/client/dist/assets/chunk-4BX2VUAB-Ap8VOuEx.js +1 -0
  11. package/client/dist/assets/chunk-4TB4RGXK-7WUmxI0T.js +206 -0
  12. package/client/dist/assets/chunk-55IACEB6-gyKf_szG.js +1 -0
  13. package/client/dist/assets/chunk-EDXVE4YY-Dqsdf8RE.js +1 -0
  14. package/client/dist/assets/chunk-FMBD7UC4-CFwejjYg.js +15 -0
  15. package/client/dist/assets/chunk-OYMX7WX6-DjhMJFGF.js +231 -0
  16. package/client/dist/assets/chunk-QZHKN3VN-BZl3lFpB.js +1 -0
  17. package/client/dist/assets/chunk-YZCP3GAM-Bgp9VGoJ.js +1 -0
  18. package/client/dist/assets/classDiagram-6PBFFD2Q-CUsL9OeO.js +1 -0
  19. package/client/dist/assets/classDiagram-v2-HSJHXN6E-CUsL9OeO.js +1 -0
  20. package/client/dist/assets/clone-YPoNuUWg.js +1 -0
  21. package/client/dist/assets/cose-bilkent-S5V4N54A-BhMgNxzB.js +1 -0
  22. package/client/dist/assets/cytoscape.esm-BQaXIfA_.js +331 -0
  23. package/client/dist/assets/dagre-KV5264BT-Im4xvruI.js +4 -0
  24. package/client/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  25. package/client/dist/assets/diagram-5BDNPKRD-C_LUH1t_.js +10 -0
  26. package/client/dist/assets/diagram-G4DWMVQ6-ovGPIIZa.js +24 -0
  27. package/client/dist/assets/diagram-MMDJMWI5-Cc_S6QYf.js +43 -0
  28. package/client/dist/assets/diagram-TYMM5635-DNtJ4zRJ.js +24 -0
  29. package/client/dist/assets/erDiagram-SMLLAGMA-mjVqYF51.js +85 -0
  30. package/client/dist/assets/flowDiagram-DWJPFMVM-gfeU3vbm.js +162 -0
  31. package/client/dist/assets/ganttDiagram-T4ZO3ILL-DHzKAwPu.js +292 -0
  32. package/client/dist/assets/gitGraphDiagram-UUTBAWPF-NmVr9v2T.js +106 -0
  33. package/client/dist/assets/graph-DWZBkQul.js +1 -0
  34. package/client/dist/assets/index-Bs3edPh7.js +329 -0
  35. package/client/dist/assets/index-BvGI0U64.css +1 -0
  36. package/client/dist/assets/infoDiagram-42DDH7IO-uZQs2FWg.js +2 -0
  37. package/client/dist/assets/init-Gi6I4Gst.js +1 -0
  38. package/client/dist/assets/ishikawaDiagram-UXIWVN3A-CVqWYfyh.js +70 -0
  39. package/client/dist/assets/journeyDiagram-VCZTEJTY-BM-S2Uek.js +139 -0
  40. package/client/dist/assets/kanban-definition-6JOO6SKY-DtfrMWsW.js +89 -0
  41. package/client/dist/assets/katex-DkKDou_j.js +257 -0
  42. package/client/dist/assets/layout-dcq7fgvs.js +1 -0
  43. package/client/dist/assets/linear-B-FGkSzh.js +1 -0
  44. package/client/dist/assets/mermaid.core-DDFJ1A5Z.js +309 -0
  45. package/client/dist/assets/min-DD4RQeGA.js +1 -0
  46. package/client/dist/assets/mindmap-definition-QFDTVHPH-B1E5NbCo.js +96 -0
  47. package/client/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  48. package/client/dist/assets/pieDiagram-DEJITSTG-Ciel1XRV.js +30 -0
  49. package/client/dist/assets/quadrantDiagram-34T5L4WZ-D1jxP0fH.js +7 -0
  50. package/client/dist/assets/requirementDiagram-MS252O5E-CTXKAocB.js +84 -0
  51. package/client/dist/assets/sankeyDiagram-XADWPNL6-Dj-i0BnN.js +10 -0
  52. package/client/dist/assets/sequenceDiagram-FGHM5R23-kCI4qbes.js +157 -0
  53. package/client/dist/assets/stateDiagram-FHFEXIEX-CiE-lplM.js +1 -0
  54. package/client/dist/assets/stateDiagram-v2-QKLJ7IA2-ZPI5JmoG.js +1 -0
  55. package/client/dist/assets/timeline-definition-GMOUNBTQ-MoAQpvF_.js +120 -0
  56. package/client/dist/assets/vennDiagram-DHZGUBPP-BqTjHn6B.js +34 -0
  57. package/client/dist/assets/wardley-RL74JXVD-C3OshQuH.js +162 -0
  58. package/client/dist/assets/wardleyDiagram-NUSXRM2D-Dl-s9oxJ.js +20 -0
  59. package/client/dist/assets/xychartDiagram-5P7HB3ND-BqyQEiVY.js +7 -0
  60. package/client/dist/index.html +16 -0
  61. package/package.json +57 -0
  62. package/server/src/index.ts +52 -0
  63. package/server/src/session-manager.ts +765 -0
  64. package/server/src/types.ts +295 -0
  65. package/server/src/ws-handler.ts +355 -0
@@ -0,0 +1,295 @@
1
+ // === Client → Server Messages ===
2
+
3
+ export interface CreateSessionMessage {
4
+ type: 'create_session'
5
+ options?: {
6
+ model?: string
7
+ cwd?: string
8
+ permissionMode?: string
9
+ executableArgs?: string[]
10
+ env?: Record<string, string>
11
+ }
12
+ }
13
+
14
+ export interface SendMessageMessage {
15
+ type: 'send_message'
16
+ sessionId: string
17
+ content: string
18
+ }
19
+
20
+ export interface SwitchSessionMessage {
21
+ type: 'switch_session'
22
+ sessionId: string
23
+ }
24
+
25
+ export interface PermissionDecisionMessage {
26
+ type: 'permission_decision'
27
+ toolUseId: string
28
+ approved: boolean
29
+ reason?: string
30
+ alwaysAllow?: boolean
31
+ }
32
+
33
+ export interface ListSessionsMessage {
34
+ type: 'list_sessions'
35
+ }
36
+
37
+ export interface ListFilesMessage {
38
+ type: 'list_files'
39
+ prefix: string
40
+ sessionId?: string
41
+ }
42
+
43
+ export interface GetDefaultCwdMessage {
44
+ type: 'get_default_cwd'
45
+ }
46
+
47
+ export interface ListCommandsMessage {
48
+ type: 'list_commands'
49
+ sessionId: string
50
+ }
51
+
52
+ export interface ResumeSessionMessage {
53
+ type: 'resume_session'
54
+ sessionId: string
55
+ }
56
+
57
+ export interface CloseSessionMessage {
58
+ type: 'close_session'
59
+ sessionId: string
60
+ }
61
+
62
+ export interface SetModelMessage {
63
+ type: 'set_model'
64
+ sessionId: string
65
+ model: string
66
+ }
67
+
68
+ export interface ListModelsMessage {
69
+ type: 'list_models'
70
+ sessionId: string
71
+ }
72
+
73
+ export interface RenameSessionMessage {
74
+ type: 'rename_session'
75
+ sessionId: string
76
+ title: string
77
+ }
78
+
79
+ export interface ForkSessionMessage {
80
+ type: 'fork_session'
81
+ sessionId: string
82
+ upToMessageId: string
83
+ }
84
+
85
+ export type EffortLevel = 'low' | 'medium' | 'high' | 'max'
86
+
87
+ export interface SetEffortLevelMessage {
88
+ type: 'set_effort_level'
89
+ sessionId: string
90
+ level: EffortLevel
91
+ }
92
+
93
+ export interface GetSubagentMessagesMessage {
94
+ type: 'get_subagent_messages'
95
+ sessionId: string
96
+ agentId: string
97
+ }
98
+
99
+ export interface ElicitationResponseMessage {
100
+ type: 'elicitation_response'
101
+ id: string
102
+ action: 'accept' | 'decline' | 'cancel'
103
+ content?: Record<string, unknown>
104
+ }
105
+
106
+ export interface GetSessionSettingsMessage {
107
+ type: 'get_session_settings'
108
+ sessionId: string
109
+ }
110
+
111
+ export type ClientMessage =
112
+ | CreateSessionMessage
113
+ | SendMessageMessage
114
+ | SwitchSessionMessage
115
+ | PermissionDecisionMessage
116
+ | ListSessionsMessage
117
+ | ListFilesMessage
118
+ | GetDefaultCwdMessage
119
+ | ListCommandsMessage
120
+ | ResumeSessionMessage
121
+ | CloseSessionMessage
122
+ | SetModelMessage
123
+ | ListModelsMessage
124
+ | RenameSessionMessage
125
+ | ForkSessionMessage
126
+ | SetEffortLevelMessage
127
+ | GetSubagentMessagesMessage
128
+ | ElicitationResponseMessage
129
+ | GetSessionSettingsMessage
130
+
131
+ // === Server → Client Messages ===
132
+
133
+ export interface SessionCreatedMessage {
134
+ type: 'session_created'
135
+ sessionId: string
136
+ }
137
+
138
+ export interface SessionListMessage {
139
+ type: 'session_list'
140
+ sessions: SessionInfo[]
141
+ }
142
+
143
+ export interface SessionInfo {
144
+ sessionId: string
145
+ summary: string
146
+ lastModified: number
147
+ status: 'idle' | 'running'
148
+ }
149
+
150
+ export interface SdkEventMessage {
151
+ type: 'sdk_message'
152
+ sessionId: string
153
+ message: unknown // SDK message, typed on client side
154
+ }
155
+
156
+ export interface PermissionRequestMessage {
157
+ type: 'permission_request'
158
+ sessionId: string
159
+ toolUseId: string
160
+ toolName: string
161
+ input: Record<string, unknown>
162
+ agentId?: string
163
+ title?: string
164
+ description?: string
165
+ hasSuggestions?: boolean
166
+ }
167
+
168
+ export interface ErrorMessage {
169
+ type: 'error'
170
+ message: string
171
+ }
172
+
173
+ export interface SessionEndMessage {
174
+ type: 'session_end'
175
+ sessionId: string
176
+ }
177
+
178
+ export interface SessionHistoryMessage {
179
+ type: 'session_history'
180
+ sessionId: string
181
+ messages: unknown[]
182
+ }
183
+
184
+ export interface SessionIdResolvedMessage {
185
+ type: 'session_id_resolved'
186
+ tempId: string
187
+ sessionId: string
188
+ }
189
+
190
+ export interface FileListMessage {
191
+ type: 'file_list'
192
+ files: FileEntry[]
193
+ }
194
+
195
+ export interface FileEntry {
196
+ name: string
197
+ path: string
198
+ isDir: boolean
199
+ }
200
+
201
+ export interface DefaultCwdMessage {
202
+ type: 'default_cwd'
203
+ cwd: string
204
+ }
205
+
206
+ export interface CommandListMessage {
207
+ type: 'command_list'
208
+ commands: { name: string; description: string }[]
209
+ }
210
+
211
+ export interface PermissionDecidedMessage {
212
+ type: 'permission_decided'
213
+ toolUseId: string
214
+ approved: boolean
215
+ }
216
+
217
+ export interface SessionResumedMessage {
218
+ type: 'session_resumed'
219
+ sessionId: string
220
+ }
221
+
222
+ export interface ModelInfo {
223
+ value: string
224
+ displayName: string
225
+ description: string
226
+ }
227
+
228
+ export interface ModelListMessage {
229
+ type: 'model_list'
230
+ sessionId: string
231
+ models: ModelInfo[]
232
+ currentModel?: string
233
+ }
234
+
235
+ export interface SessionRenamedMessage {
236
+ type: 'session_renamed'
237
+ sessionId: string
238
+ title: string
239
+ }
240
+
241
+ export interface SessionForkedMessage {
242
+ type: 'session_forked'
243
+ sessionId: string
244
+ newSessionId: string
245
+ }
246
+
247
+ export interface EffortLevelChangedMessage {
248
+ type: 'effort_level_changed'
249
+ sessionId: string
250
+ level: EffortLevel
251
+ }
252
+
253
+ export interface SubagentMessagesMessage {
254
+ type: 'subagent_messages'
255
+ agentId: string
256
+ messages: unknown[]
257
+ }
258
+
259
+ export interface ElicitationRequestMessage {
260
+ type: 'elicitation_request'
261
+ id: string
262
+ serverName: string
263
+ message: string
264
+ mode?: string
265
+ requestedSchema?: Record<string, unknown>
266
+ url?: string
267
+ }
268
+
269
+ export interface SessionSettingsMessage {
270
+ type: 'session_settings'
271
+ sessionId: string
272
+ settings: Record<string, unknown>
273
+ }
274
+
275
+ export type ServerMessage =
276
+ | SessionCreatedMessage
277
+ | SessionListMessage
278
+ | SdkEventMessage
279
+ | PermissionRequestMessage
280
+ | ErrorMessage
281
+ | SessionEndMessage
282
+ | SessionHistoryMessage
283
+ | SessionIdResolvedMessage
284
+ | FileListMessage
285
+ | DefaultCwdMessage
286
+ | CommandListMessage
287
+ | PermissionDecidedMessage
288
+ | SessionResumedMessage
289
+ | ModelListMessage
290
+ | SessionRenamedMessage
291
+ | SessionForkedMessage
292
+ | EffortLevelChangedMessage
293
+ | SubagentMessagesMessage
294
+ | ElicitationRequestMessage
295
+ | SessionSettingsMessage
@@ -0,0 +1,355 @@
1
+ import type { WebSocket } from '@fastify/websocket'
2
+ import { randomUUID } from 'node:crypto'
3
+ import { readdir, mkdir } from 'node:fs/promises'
4
+ import { join, resolve, relative, basename, dirname } from 'node:path'
5
+ import type { SessionManager, PermissionMeta, SessionListener } from './session-manager.js'
6
+ import type { ClientMessage, ServerMessage, FileEntry, EffortLevel } from './types.js'
7
+
8
+ export function createWsHandler(sessionManager: SessionManager) {
9
+ return async function handleConnection(socket: WebSocket): Promise<void> {
10
+ const listenerId = randomUUID()
11
+ let alive = true
12
+
13
+ // --- Heartbeat: 30s ping interval ---
14
+ const pingInterval = setInterval(() => {
15
+ if (!alive) {
16
+ socket.terminate()
17
+ return
18
+ }
19
+ alive = false
20
+ socket.ping()
21
+ }, 30_000)
22
+
23
+ socket.on('pong', () => {
24
+ alive = true
25
+ })
26
+
27
+ function send(msg: ServerMessage): void {
28
+ if (socket.readyState === socket.OPEN) {
29
+ socket.send(JSON.stringify(msg))
30
+ }
31
+ }
32
+
33
+ function makeListener(sessionId: string): SessionListener {
34
+ return {
35
+ id: listenerId,
36
+ onMessage(sid, message) {
37
+ const msg = message as Record<string, unknown>
38
+
39
+ // Handle synthetic messages from SessionManager
40
+ if (msg.type === 'permission_decided') {
41
+ send({
42
+ type: 'permission_decided',
43
+ toolUseId: msg.toolUseId as string,
44
+ approved: msg.approved as boolean,
45
+ })
46
+ return
47
+ }
48
+
49
+ if (msg.type === 'session_id_resolved') {
50
+ send({
51
+ type: 'session_id_resolved',
52
+ tempId: msg.tempId as string,
53
+ sessionId: msg.sessionId as string,
54
+ })
55
+ return
56
+ }
57
+
58
+ if (msg.type === 'session_resumed') {
59
+ send({
60
+ type: 'session_resumed',
61
+ sessionId: msg.sessionId as string,
62
+ })
63
+ return
64
+ }
65
+
66
+ if (msg.type === 'session_renamed') {
67
+ send({
68
+ type: 'session_renamed',
69
+ sessionId: msg.sessionId as string,
70
+ title: msg.title as string,
71
+ })
72
+ return
73
+ }
74
+
75
+ if (msg.type === 'session_forked') {
76
+ send({
77
+ type: 'session_forked',
78
+ sessionId: msg.sessionId as string,
79
+ newSessionId: msg.newSessionId as string,
80
+ })
81
+ return
82
+ }
83
+
84
+ if (msg.type === 'effort_level_changed') {
85
+ send({
86
+ type: 'effort_level_changed',
87
+ sessionId: msg.sessionId as string,
88
+ level: msg.level as EffortLevel,
89
+ })
90
+ return
91
+ }
92
+
93
+ if (msg.type === 'elicitation_request') {
94
+ send({
95
+ type: 'elicitation_request',
96
+ id: msg.id as string,
97
+ serverName: msg.serverName as string,
98
+ message: msg.message as string,
99
+ mode: msg.mode as string | undefined,
100
+ requestedSchema: msg.requestedSchema as Record<string, unknown> | undefined,
101
+ url: msg.url as string | undefined,
102
+ })
103
+ return
104
+ }
105
+
106
+ // Auto-push command list on init or commands_updated
107
+ if ((msg.type === 'system' && msg.subtype === 'init') || msg.type === 'commands_updated') {
108
+ sessionManager.getCommands(sid).then((commands) => {
109
+ if (commands.length > 0) {
110
+ send({ type: 'command_list', commands })
111
+ }
112
+ }).catch(() => {})
113
+ if (msg.type === 'commands_updated') return // don't forward synthetic message
114
+ }
115
+
116
+ // Push model list when available
117
+ if (msg.type === 'models_updated') {
118
+ send({
119
+ type: 'model_list',
120
+ sessionId: sid,
121
+ models: msg.models as { value: string; displayName: string; description: string }[],
122
+ currentModel: msg.currentModel as string | undefined,
123
+ })
124
+ return // don't forward synthetic message
125
+ }
126
+
127
+ send({ type: 'sdk_message', sessionId: sid, message })
128
+ },
129
+ onPermissionRequest(sid, toolUseId, toolName, input, meta) {
130
+ send({ type: 'permission_request', sessionId: sid, toolUseId, toolName, input, ...meta })
131
+ },
132
+ onEnd(sid) {
133
+ send({ type: 'session_end', sessionId: sid })
134
+ },
135
+ }
136
+ }
137
+
138
+ // --- Cleanup on close/error ---
139
+ function cleanup(): void {
140
+ clearInterval(pingInterval)
141
+ sessionManager.unsubscribeAll(listenerId)
142
+ }
143
+
144
+ socket.on('close', cleanup)
145
+ socket.on('error', cleanup)
146
+
147
+ // --- Message handling ---
148
+ socket.on('message', async (raw: Buffer) => {
149
+ alive = true
150
+
151
+ let msg: ClientMessage
152
+ try {
153
+ msg = JSON.parse(raw.toString()) as ClientMessage
154
+ } catch {
155
+ send({ type: 'error', message: 'Invalid JSON' })
156
+ return
157
+ }
158
+
159
+ try {
160
+ switch (msg.type) {
161
+ case 'create_session': {
162
+ // Ensure cwd exists before creating session
163
+ const cwd = msg.options?.cwd
164
+ if (cwd) {
165
+ await mkdir(cwd, { recursive: true })
166
+ }
167
+
168
+ const tempId = await sessionManager.createSession(msg.options)
169
+ sessionManager.subscribe(tempId, makeListener(tempId))
170
+ send({ type: 'session_created', sessionId: tempId })
171
+ break
172
+ }
173
+
174
+ case 'resume_session': {
175
+ // Subscribe BEFORE resume so this connection receives the broadcast too
176
+ sessionManager.subscribe(msg.sessionId, makeListener(msg.sessionId))
177
+ await sessionManager.resumeSession(msg.sessionId)
178
+ break
179
+ }
180
+
181
+ case 'send_message': {
182
+ // Subscribe for live updates (idempotent via listener dedup)
183
+ sessionManager.subscribe(msg.sessionId, makeListener(msg.sessionId))
184
+ await sessionManager.sendMessage(msg.sessionId, msg.content)
185
+ console.log('[ws] Message sent to session', msg.sessionId)
186
+ break
187
+ }
188
+
189
+ case 'switch_session': {
190
+ // Load historical messages
191
+ const history = await sessionManager.getHistory(msg.sessionId)
192
+ send({ type: 'session_history', sessionId: msg.sessionId, messages: history })
193
+ // Subscribe for live updates if session is running
194
+ sessionManager.subscribe(msg.sessionId, makeListener(msg.sessionId))
195
+ break
196
+ }
197
+
198
+ case 'close_session': {
199
+ sessionManager.closeSession(msg.sessionId)
200
+ // Listeners get session_end via broadcast inside closeSession
201
+ break
202
+ }
203
+
204
+ case 'permission_decision': {
205
+ sessionManager.resolvePermission(
206
+ msg.toolUseId,
207
+ msg.approved,
208
+ msg.reason,
209
+ msg.alwaysAllow,
210
+ )
211
+ break
212
+ }
213
+
214
+ case 'list_sessions': {
215
+ const sessions = await sessionManager.listSessions()
216
+ send({ type: 'session_list', sessions })
217
+ break
218
+ }
219
+
220
+ case 'list_files': {
221
+ const cwd = sessionManager.getCwd(msg.sessionId)
222
+ const prefix = msg.prefix || ''
223
+ const files = await listFiles(cwd, prefix)
224
+ send({ type: 'file_list', files })
225
+ break
226
+ }
227
+
228
+ case 'get_default_cwd': {
229
+ const cwd = sessionManager.getCwd()
230
+ send({ type: 'default_cwd', cwd })
231
+ break
232
+ }
233
+
234
+ case 'list_commands': {
235
+ const commands = await sessionManager.getCommands(msg.sessionId)
236
+ send({ type: 'command_list', commands })
237
+ break
238
+ }
239
+
240
+ case 'set_model': {
241
+ await sessionManager.setModel(msg.sessionId, msg.model)
242
+ break
243
+ }
244
+
245
+ case 'list_models': {
246
+ const models = await sessionManager.getSupportedModels(msg.sessionId)
247
+ if (models.length > 0) {
248
+ send({ type: 'model_list', sessionId: msg.sessionId, models })
249
+ }
250
+ break
251
+ }
252
+
253
+ case 'rename_session': {
254
+ await sessionManager.renameSession(msg.sessionId, msg.title)
255
+ break
256
+ }
257
+
258
+ case 'fork_session': {
259
+ await sessionManager.forkSession(msg.sessionId, msg.upToMessageId)
260
+ break
261
+ }
262
+
263
+ case 'set_effort_level': {
264
+ await sessionManager.setEffortLevel(msg.sessionId, msg.level)
265
+ break
266
+ }
267
+
268
+ case 'get_subagent_messages': {
269
+ const messages = await sessionManager.getSubagentMessages(msg.sessionId, msg.agentId)
270
+ send({ type: 'subagent_messages', sessionId: msg.sessionId, agentId: msg.agentId, messages })
271
+ break
272
+ }
273
+
274
+ case 'elicitation_response': {
275
+ sessionManager.resolveElicitation(msg.id, msg.action, msg.content as Record<string, unknown> | undefined)
276
+ break
277
+ }
278
+
279
+ case 'get_session_settings': {
280
+ const settings = await sessionManager.getSessionSettings(msg.sessionId)
281
+ send({ type: 'session_settings', sessionId: msg.sessionId, settings })
282
+ break
283
+ }
284
+ }
285
+ } catch (err) {
286
+ send({ type: 'error', message: err instanceof Error ? err.message : 'Unknown error' })
287
+ }
288
+ })
289
+
290
+ // Send initial session list on connect
291
+ try {
292
+ const sessions = await sessionManager.listSessions()
293
+ send({ type: 'session_list', sessions })
294
+ } catch {
295
+ // Non-fatal: proceed without initial session list
296
+ }
297
+ }
298
+ }
299
+
300
+ const IGNORED = new Set(['.git', 'node_modules', '.next', 'dist', '__pycache__', '.venv', 'venv', '.cache'])
301
+ const MAX_FILES = 50
302
+
303
+ async function listFiles(cwd: string, prefix: string): Promise<FileEntry[]> {
304
+ // Determine which directory to read based on the prefix
305
+ // e.g. prefix="src/comp" → dir="src", namePrefix="comp"
306
+ const targetPath = resolve(cwd, prefix)
307
+ let dir: string
308
+ let namePrefix: string
309
+
310
+ try {
311
+ const entries = await readdir(targetPath, { withFileTypes: true })
312
+ // prefix points to a directory, list its contents
313
+ dir = targetPath
314
+ namePrefix = ''
315
+ return entriesToFiles(entries, dir, cwd, namePrefix)
316
+ } catch {
317
+ // prefix is partial — list the parent directory and filter
318
+ dir = dirname(targetPath)
319
+ namePrefix = basename(targetPath).toLowerCase()
320
+ }
321
+
322
+ try {
323
+ const entries = await readdir(dir, { withFileTypes: true })
324
+ return entriesToFiles(entries, dir, cwd, namePrefix)
325
+ } catch {
326
+ return []
327
+ }
328
+ }
329
+
330
+ function entriesToFiles(
331
+ entries: import('node:fs').Dirent[],
332
+ dir: string,
333
+ cwd: string,
334
+ namePrefix: string,
335
+ ): FileEntry[] {
336
+ const results: FileEntry[] = []
337
+ for (const entry of entries) {
338
+ if (entry.name.startsWith('.') && !namePrefix.startsWith('.')) continue
339
+ if (IGNORED.has(entry.name)) continue
340
+ if (namePrefix && !entry.name.toLowerCase().startsWith(namePrefix)) continue
341
+ const fullPath = join(dir, entry.name)
342
+ results.push({
343
+ name: entry.name,
344
+ path: fullPath + (entry.isDirectory() ? '/' : ''),
345
+ isDir: entry.isDirectory(),
346
+ })
347
+ if (results.length >= MAX_FILES) break
348
+ }
349
+ // Directories first, then files, alphabetical
350
+ results.sort((a, b) => {
351
+ if (a.isDir !== b.isDir) return a.isDir ? -1 : 1
352
+ return a.name.localeCompare(b.name)
353
+ })
354
+ return results
355
+ }