@kelceyp/caw-server 0.0.5 → 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/dist/main.js CHANGED
@@ -44596,9 +44596,18 @@ var import_cors = __toESM(require_lib3(), 1);
44596
44596
  import { createServer } from "http";
44597
44597
 
44598
44598
  // src/common/Auth.js
44599
- var HARDCODED_TOKEN = "test-token-123";
44600
- var create = () => {
44601
- const middleware = (req, res, next) => {
44599
+ var getProjectByToken = async (store, token) => {
44600
+ return store.getProjectByToken(token);
44601
+ };
44602
+ var validateToken = async (store, token) => {
44603
+ const project = await getProjectByToken(store, token);
44604
+ if (!project) {
44605
+ throw new Error("Invalid token");
44606
+ }
44607
+ return project.id;
44608
+ };
44609
+ var create = ({ store } = {}) => {
44610
+ const middleware = async (req, res, next) => {
44602
44611
  const authHeader = req.headers.authorization;
44603
44612
  if (!authHeader) {
44604
44613
  return res.status(401).json({
@@ -44606,33 +44615,123 @@ var create = () => {
44606
44615
  message: "Missing Authorization header"
44607
44616
  });
44608
44617
  }
44609
- const token = authHeader.replace(/^Bearer\s+/, "");
44610
- if (token !== HARDCODED_TOKEN) {
44618
+ const token = authHeader.replace(/^Bearer\s+/i, "");
44619
+ if (!store) {
44620
+ return res.status(500).json({
44621
+ error: "Internal server error",
44622
+ message: "Auth store not configured"
44623
+ });
44624
+ }
44625
+ try {
44626
+ const projectId = await validateToken(store, token);
44627
+ req.projectId = projectId;
44628
+ req.token = token;
44629
+ next();
44630
+ } catch (error) {
44611
44631
  return res.status(401).json({
44612
44632
  error: "Unauthorized",
44613
44633
  message: "Invalid token"
44614
44634
  });
44615
44635
  }
44616
- next();
44617
44636
  };
44618
44637
  return Object.freeze({
44619
44638
  middleware
44620
44639
  });
44621
44640
  };
44622
- var Auth_default = { create };
44641
+ var Auth_default = { create, getProjectByToken, validateToken };
44623
44642
 
44624
44643
  // src/api/ApiRoutes.js
44625
- var create2 = (app) => {
44626
- const auth = Auth_default.create();
44644
+ var create2 = (app, { store } = {}) => {
44645
+ const auth = Auth_default.create({ store });
44627
44646
  app.get("/api/hello", auth.middleware, (req, res) => {
44628
44647
  res.json({
44629
- message: "Hello from CAW"
44648
+ message: "Hello from CAW",
44649
+ projectId: req.projectId
44630
44650
  });
44631
44651
  });
44632
44652
  return Object.freeze({});
44633
44653
  };
44634
44654
  var ApiRoutes_default = { create: create2 };
44635
44655
 
44656
+ // src/api/ProjectRoutes.js
44657
+ var create3 = (app, { store }) => {
44658
+ const auth = Auth_default.create({ store });
44659
+ app.get("/api/projects", async (req, res) => {
44660
+ try {
44661
+ const projects = await store.getProjects();
44662
+ res.json(projects);
44663
+ } catch (error) {
44664
+ console.error("[ProjectRoutes] Error listing projects:", error);
44665
+ res.status(500).json({
44666
+ error: "Internal server error",
44667
+ message: "Failed to list projects"
44668
+ });
44669
+ }
44670
+ });
44671
+ app.post("/api/projects", async (req, res) => {
44672
+ const { name, path } = req.body;
44673
+ if (!name || typeof name !== "string") {
44674
+ return res.status(400).json({
44675
+ error: "Bad request",
44676
+ message: "name is required and must be a string"
44677
+ });
44678
+ }
44679
+ if (!path || typeof path !== "string") {
44680
+ return res.status(400).json({
44681
+ error: "Bad request",
44682
+ message: "path is required and must be a string"
44683
+ });
44684
+ }
44685
+ try {
44686
+ const project = await store.createProject({ name, path });
44687
+ res.status(201).json(project);
44688
+ } catch (error) {
44689
+ console.error("[ProjectRoutes] Error creating project:", error);
44690
+ res.status(500).json({
44691
+ error: "Internal server error",
44692
+ message: "Failed to create project"
44693
+ });
44694
+ }
44695
+ });
44696
+ app.get("/api/projects/:projectId", auth.middleware, async (req, res) => {
44697
+ const projectId = parseInt(req.params.projectId, 10);
44698
+ if (isNaN(projectId)) {
44699
+ return res.status(400).json({
44700
+ error: "Bad request",
44701
+ message: "projectId must be an integer"
44702
+ });
44703
+ }
44704
+ if (req.projectId !== projectId) {
44705
+ return res.status(403).json({
44706
+ error: "Forbidden",
44707
+ message: "Token does not match requested project"
44708
+ });
44709
+ }
44710
+ try {
44711
+ const project = await store.getProject(projectId);
44712
+ if (!project) {
44713
+ return res.status(404).json({
44714
+ error: "Not found",
44715
+ message: "Project not found"
44716
+ });
44717
+ }
44718
+ const { token, ...projectWithoutToken } = project;
44719
+ res.json(projectWithoutToken);
44720
+ } catch (error) {
44721
+ console.error("[ProjectRoutes] Error getting project:", error);
44722
+ res.status(500).json({
44723
+ error: "Internal server error",
44724
+ message: "Failed to get project"
44725
+ });
44726
+ }
44727
+ });
44728
+ return Object.freeze({});
44729
+ };
44730
+ var ProjectRoutes_default = { create: create3 };
44731
+
44732
+ // src/mcp/McpRoutes.js
44733
+ import { randomUUID as randomUUID2 } from "crypto";
44734
+
44636
44735
  // node_modules/@modelcontextprotocol/sdk/node_modules/zod/v3/external.js
44637
44736
  var exports_external = {};
44638
44737
  __export(exports_external, {
@@ -56167,11 +56266,33 @@ var EMPTY_COMPLETION_RESULT = {
56167
56266
  }
56168
56267
  };
56169
56268
  // src/mcp/McpServer.js
56170
- var createMcpServer = ({ sessionManager } = {}) => {
56269
+ var createMcpServer = ({ sessionManager, projectId, token, getMcpSessionId, mcpSessions } = {}) => {
56171
56270
  const server = new McpServer({
56172
56271
  name: "caw-server",
56173
56272
  version: "0.0.1"
56174
56273
  });
56274
+ const context = {
56275
+ projectId,
56276
+ token,
56277
+ getMcpSessionId,
56278
+ mcpSessions
56279
+ };
56280
+ const getCurrentMcpSession = () => {
56281
+ const mcpSessionId = context.getMcpSessionId();
56282
+ if (!mcpSessionId || !context.mcpSessions)
56283
+ return null;
56284
+ return context.mcpSessions.get(mcpSessionId);
56285
+ };
56286
+ const verifyClaimedError = (pageId) => {
56287
+ const mcpSessionId = context.getMcpSessionId();
56288
+ if (!sessionManager || !mcpSessionId) {
56289
+ return { error: true, message: "Session context not available" };
56290
+ }
56291
+ if (!sessionManager.isClaimedBy(pageId, mcpSessionId)) {
56292
+ return { error: true, message: "MUST_CLAIM_FIRST: You must claim this session before sending commands" };
56293
+ }
56294
+ return { error: false };
56295
+ };
56175
56296
  server.registerTool("list_sessions", {
56176
56297
  title: "List Active Sessions",
56177
56298
  description: "List all active WebSocket sessions with their pageIds and states",
@@ -56179,36 +56300,128 @@ var createMcpServer = ({ sessionManager } = {}) => {
56179
56300
  }, async () => {
56180
56301
  if (!sessionManager) {
56181
56302
  return {
56182
- content: [
56183
- {
56184
- type: "text",
56185
- text: "Error: SessionManager not initialized"
56186
- }
56187
- ],
56303
+ content: [{ type: "text", text: "Error: SessionManager not initialized" }],
56188
56304
  isError: true
56189
56305
  };
56190
56306
  }
56191
56307
  const sessions = sessionManager.getAllSessions();
56192
56308
  if (sessions.length === 0) {
56193
56309
  return {
56194
- content: [
56195
- {
56196
- type: "text",
56197
- text: "No active sessions"
56198
- }
56199
- ]
56310
+ content: [{ type: "text", text: "No active sessions" }]
56200
56311
  };
56201
56312
  }
56202
- const sessionList = sessions.map((s) => `• ${s.pageId} (${s.state}, capabilities: ${s.capabilities.join(", ")})`).join(`
56313
+ const mcpSessionId = context.getMcpSessionId();
56314
+ const sessionList = sessions.map((s) => {
56315
+ const claimStatus = s.claimedBy === null ? "unclaimed" : s.claimedBy === mcpSessionId ? "claimed by you" : "claimed by another";
56316
+ return `• ${s.pageId} (${s.state}, ${claimStatus})`;
56317
+ }).join(`
56203
56318
  `);
56204
56319
  return {
56205
- content: [
56206
- {
56207
- type: "text",
56208
- text: `Active sessions (${sessions.length}):
56209
- ${sessionList}`
56210
- }
56211
- ]
56320
+ content: [{ type: "text", text: `Active sessions (${sessions.length}):
56321
+ ${sessionList}` }]
56322
+ };
56323
+ });
56324
+ server.registerTool("claim_session", {
56325
+ title: "Claim Session",
56326
+ description: "Claim an unclaimed WebSocket session so you can send commands to it",
56327
+ inputSchema: exports_external2.object({
56328
+ pageId: exports_external2.string().describe("Page ID of the tab to claim")
56329
+ })
56330
+ }, async ({ pageId }) => {
56331
+ if (!sessionManager) {
56332
+ return {
56333
+ content: [{ type: "text", text: "Error: SessionManager not initialized" }],
56334
+ isError: true
56335
+ };
56336
+ }
56337
+ const mcpSessionId = context.getMcpSessionId();
56338
+ if (!mcpSessionId) {
56339
+ return {
56340
+ content: [{ type: "text", text: "Error: MCP session not initialized" }],
56341
+ isError: true
56342
+ };
56343
+ }
56344
+ const result = sessionManager.claimSession(pageId, mcpSessionId);
56345
+ if (!result.success) {
56346
+ return {
56347
+ content: [{ type: "text", text: `Error: ${result.error}` }],
56348
+ isError: true
56349
+ };
56350
+ }
56351
+ const mcpSession = getCurrentMcpSession();
56352
+ if (mcpSession) {
56353
+ mcpSession.claimedConnections.add(pageId);
56354
+ }
56355
+ return {
56356
+ content: [{ type: "text", text: `Successfully claimed session: ${pageId}` }]
56357
+ };
56358
+ });
56359
+ server.registerTool("release_session", {
56360
+ title: "Release Session",
56361
+ description: "Release a claimed WebSocket session so other MCP sessions can claim it",
56362
+ inputSchema: exports_external2.object({
56363
+ pageId: exports_external2.string().describe("Page ID of the tab to release")
56364
+ })
56365
+ }, async ({ pageId }) => {
56366
+ if (!sessionManager) {
56367
+ return {
56368
+ content: [{ type: "text", text: "Error: SessionManager not initialized" }],
56369
+ isError: true
56370
+ };
56371
+ }
56372
+ const mcpSessionId = context.getMcpSessionId();
56373
+ if (!mcpSessionId) {
56374
+ return {
56375
+ content: [{ type: "text", text: "Error: MCP session not initialized" }],
56376
+ isError: true
56377
+ };
56378
+ }
56379
+ const result = sessionManager.releaseSession(pageId, mcpSessionId);
56380
+ if (!result.success) {
56381
+ return {
56382
+ content: [{ type: "text", text: `Error: ${result.error}` }],
56383
+ isError: true
56384
+ };
56385
+ }
56386
+ const mcpSession = getCurrentMcpSession();
56387
+ if (mcpSession) {
56388
+ mcpSession.claimedConnections.delete(pageId);
56389
+ }
56390
+ return {
56391
+ content: [{ type: "text", text: `Successfully released session: ${pageId}` }]
56392
+ };
56393
+ });
56394
+ server.registerTool("list_my_sessions", {
56395
+ title: "List My Sessions",
56396
+ description: "List only the WebSocket sessions claimed by this MCP session",
56397
+ inputSchema: exports_external2.object({})
56398
+ }, async () => {
56399
+ if (!sessionManager) {
56400
+ return {
56401
+ content: [{ type: "text", text: "Error: SessionManager not initialized" }],
56402
+ isError: true
56403
+ };
56404
+ }
56405
+ const mcpSession = getCurrentMcpSession();
56406
+ if (!mcpSession) {
56407
+ return {
56408
+ content: [{ type: "text", text: "Error: MCP session not found" }],
56409
+ isError: true
56410
+ };
56411
+ }
56412
+ const claimedPageIds = Array.from(mcpSession.claimedConnections);
56413
+ if (claimedPageIds.length === 0) {
56414
+ return {
56415
+ content: [{ type: "text", text: "No claimed sessions" }]
56416
+ };
56417
+ }
56418
+ const allSessions = sessionManager.getAllSessions();
56419
+ const mySessions = allSessions.filter((s) => claimedPageIds.includes(s.pageId));
56420
+ const sessionList = mySessions.map((s) => `• ${s.pageId} (${s.state})`).join(`
56421
+ `);
56422
+ return {
56423
+ content: [{ type: "text", text: `Your claimed sessions (${mySessions.length}):
56424
+ ${sessionList}` }]
56212
56425
  };
56213
56426
  });
56214
56427
  server.registerTool("get_state", {
@@ -56220,33 +56433,25 @@ ${sessionList}`
56220
56433
  }, async ({ pageId }) => {
56221
56434
  if (!sessionManager) {
56222
56435
  return {
56223
- content: [
56224
- {
56225
- type: "text",
56226
- text: "Error: SessionManager not initialized"
56227
- }
56228
- ],
56436
+ content: [{ type: "text", text: "Error: SessionManager not initialized" }],
56437
+ isError: true
56438
+ };
56439
+ }
56440
+ const claimCheck = verifyClaimedError(pageId);
56441
+ if (claimCheck.error) {
56442
+ return {
56443
+ content: [{ type: "text", text: `Error: ${claimCheck.message}` }],
56229
56444
  isError: true
56230
56445
  };
56231
56446
  }
56232
56447
  try {
56233
56448
  const result = await sessionManager.sendCommand(pageId, "getState", {});
56234
56449
  return {
56235
- content: [
56236
- {
56237
- type: "text",
56238
- text: `ChatGPT state: ${result.state}`
56239
- }
56240
- ]
56450
+ content: [{ type: "text", text: `ChatGPT state: ${result.state}` }]
56241
56451
  };
56242
56452
  } catch (error) {
56243
56453
  return {
56244
- content: [
56245
- {
56246
- type: "text",
56247
- text: `Error: ${error.message}`
56248
- }
56249
- ],
56454
+ content: [{ type: "text", text: `Error: ${error.message}` }],
56250
56455
  isError: true
56251
56456
  };
56252
56457
  }
@@ -56262,35 +56467,27 @@ ${sessionList}`
56262
56467
  }, async ({ pageId, text, timeout }) => {
56263
56468
  if (!sessionManager) {
56264
56469
  return {
56265
- content: [
56266
- {
56267
- type: "text",
56268
- text: "Error: SessionManager not initialized"
56269
- }
56270
- ],
56470
+ content: [{ type: "text", text: "Error: SessionManager not initialized" }],
56471
+ isError: true
56472
+ };
56473
+ }
56474
+ const claimCheck = verifyClaimedError(pageId);
56475
+ if (claimCheck.error) {
56476
+ return {
56477
+ content: [{ type: "text", text: `Error: ${claimCheck.message}` }],
56271
56478
  isError: true
56272
56479
  };
56273
56480
  }
56274
56481
  try {
56275
56482
  const result = await sessionManager.sendCommand(pageId, "sendMessage", { text, timeout }, timeout || 120000);
56276
56483
  return {
56277
- content: [
56278
- {
56279
- type: "text",
56280
- text: `ChatGPT response (turn ${result.turn}, ${result.duration}ms):
56484
+ content: [{ type: "text", text: `ChatGPT response (turn ${result.turn}, ${result.duration}ms):
56281
56485
 
56282
- ${result.text}`
56283
- }
56284
- ]
56486
+ ${result.text}` }]
56285
56487
  };
56286
56488
  } catch (error) {
56287
56489
  return {
56288
- content: [
56289
- {
56290
- type: "text",
56291
- text: `Error: ${error.message}`
56292
- }
56293
- ],
56490
+ content: [{ type: "text", text: `Error: ${error.message}` }],
56294
56491
  isError: true
56295
56492
  };
56296
56493
  }
@@ -56304,12 +56501,14 @@ ${result.text}`
56304
56501
  }, async ({ pageId }) => {
56305
56502
  if (!sessionManager) {
56306
56503
  return {
56307
- content: [
56308
- {
56309
- type: "text",
56310
- text: "Error: SessionManager not initialized"
56311
- }
56312
- ],
56504
+ content: [{ type: "text", text: "Error: SessionManager not initialized" }],
56505
+ isError: true
56506
+ };
56507
+ }
56508
+ const claimCheck = verifyClaimedError(pageId);
56509
+ if (claimCheck.error) {
56510
+ return {
56511
+ content: [{ type: "text", text: `Error: ${claimCheck.message}` }],
56313
56512
  isError: true
56314
56513
  };
56315
56514
  }
@@ -56317,31 +56516,16 @@ ${result.text}`
56317
56516
  const result = await sessionManager.sendCommand(pageId, "getLastMessage", {});
56318
56517
  if (result.text) {
56319
56518
  return {
56320
- content: [
56321
- {
56322
- type: "text",
56323
- text: result.text
56324
- }
56325
- ]
56519
+ content: [{ type: "text", text: result.text }]
56326
56520
  };
56327
56521
  } else {
56328
56522
  return {
56329
- content: [
56330
- {
56331
- type: "text",
56332
- text: "No message found"
56333
- }
56334
- ]
56523
+ content: [{ type: "text", text: "No message found" }]
56335
56524
  };
56336
56525
  }
56337
56526
  } catch (error) {
56338
56527
  return {
56339
- content: [
56340
- {
56341
- type: "text",
56342
- text: `Error: ${error.message}`
56343
- }
56344
- ],
56528
+ content: [{ type: "text", text: `Error: ${error.message}` }],
56345
56529
  isError: true
56346
56530
  };
56347
56531
  }
@@ -56351,41 +56535,160 @@ ${result.text}`
56351
56535
  var McpServer_default = createMcpServer;
56352
56536
 
56353
56537
  // src/mcp/McpRoutes.js
56354
- var create3 = (app, { sessionManager } = {}) => {
56355
- const auth = Auth_default.create();
56356
- const mcpServer = McpServer_default({ sessionManager });
56538
+ var IDLE_TIMEOUT_MS = 60 * 60 * 1000;
56539
+ var CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
56540
+ var create4 = (app, { sessionManager, store } = {}) => {
56541
+ const auth = Auth_default.create({ store });
56542
+ const mcpSessions = new Map;
56543
+ const cleanupInterval = setInterval(() => {
56544
+ const now = Date.now();
56545
+ for (const [sessionId, session] of mcpSessions) {
56546
+ if (now - session.lastActivity > IDLE_TIMEOUT_MS) {
56547
+ console.log(`Cleaning up idle MCP session: ${sessionId}`);
56548
+ if (sessionManager) {
56549
+ sessionManager.releaseAllForMcpSession(sessionId);
56550
+ }
56551
+ session.transport.close();
56552
+ mcpSessions.delete(sessionId);
56553
+ }
56554
+ }
56555
+ }, CLEANUP_INTERVAL_MS);
56556
+ cleanupInterval.unref();
56557
+ const getSessionIdFromHeaders = (req) => {
56558
+ return req.headers["mcp-session-id"] || req.headers["Mcp-Session-Id"];
56559
+ };
56357
56560
  app.post("/api/mcp", auth.middleware, async (req, res) => {
56561
+ const existingSessionId = getSessionIdFromHeaders(req);
56562
+ const { projectId, token } = req;
56563
+ if (existingSessionId && mcpSessions.has(existingSessionId)) {
56564
+ const session = mcpSessions.get(existingSessionId);
56565
+ if (session.token !== token) {
56566
+ return res.status(401).json({
56567
+ jsonrpc: "2.0",
56568
+ error: { code: -32001, message: "Token mismatch for session" },
56569
+ id: null
56570
+ });
56571
+ }
56572
+ console.log(`Reusing MCP session: ${existingSessionId}`);
56573
+ session.lastActivity = Date.now();
56574
+ res.setHeader("Mcp-Session-Id", existingSessionId);
56575
+ try {
56576
+ await session.transport.handleRequest(req, res, req.body);
56577
+ } catch (error) {
56578
+ console.error("Error handling MCP request:", error);
56579
+ if (!res.headersSent) {
56580
+ res.status(500).json({
56581
+ jsonrpc: "2.0",
56582
+ error: { code: -32603, message: "Internal server error" },
56583
+ id: null
56584
+ });
56585
+ }
56586
+ }
56587
+ return;
56588
+ }
56589
+ console.log(`Creating new MCP session for project: ${projectId}`);
56590
+ let newSessionId = null;
56591
+ const transport = new StreamableHTTPServerTransport({
56592
+ sessionIdGenerator: () => {
56593
+ newSessionId = randomUUID2();
56594
+ return newSessionId;
56595
+ },
56596
+ onsessioninitialized: (sessionId) => {
56597
+ console.log(`MCP session initialized: ${sessionId}`);
56598
+ res.setHeader("Mcp-Session-Id", sessionId);
56599
+ }
56600
+ });
56601
+ const server = McpServer_default({
56602
+ sessionManager,
56603
+ projectId,
56604
+ token,
56605
+ getMcpSessionId: () => newSessionId,
56606
+ mcpSessions
56607
+ });
56358
56608
  try {
56359
- const transport = new StreamableHTTPServerTransport({
56360
- sessionIdGenerator: undefined,
56361
- enableJsonResponse: true
56362
- });
56363
- res.on("close", () => {
56364
- transport.close();
56609
+ await server.connect(transport);
56610
+ const session = {
56611
+ sessionId: newSessionId,
56612
+ transport,
56613
+ server,
56614
+ projectId,
56615
+ token,
56616
+ claimedConnections: new Set,
56617
+ createdAt: Date.now(),
56618
+ lastActivity: Date.now()
56619
+ };
56620
+ res.on("finish", () => {
56621
+ if (newSessionId && !mcpSessions.has(newSessionId)) {
56622
+ mcpSessions.set(newSessionId, session);
56623
+ console.log(`MCP session stored: ${newSessionId}`);
56624
+ }
56365
56625
  });
56366
- await mcpServer.connect(transport);
56367
56626
  await transport.handleRequest(req, res, req.body);
56368
56627
  } catch (error) {
56369
56628
  console.error("Error handling MCP request:", error);
56370
56629
  if (!res.headersSent) {
56371
56630
  res.status(500).json({
56372
56631
  jsonrpc: "2.0",
56373
- error: {
56374
- code: -32603,
56375
- message: "Internal server error"
56376
- },
56632
+ error: { code: -32603, message: "Internal server error" },
56377
56633
  id: null
56378
56634
  });
56379
56635
  }
56380
56636
  }
56381
56637
  });
56638
+ app.get("/api/mcp", auth.middleware, async (req, res) => {
56639
+ const sessionId = req.query.sessionId || getSessionIdFromHeaders(req);
56640
+ if (!sessionId) {
56641
+ return res.status(400).json({
56642
+ error: "Session ID required"
56643
+ });
56644
+ }
56645
+ const session = mcpSessions.get(sessionId);
56646
+ if (!session) {
56647
+ return res.status(404).json({
56648
+ error: "Session not found"
56649
+ });
56650
+ }
56651
+ if (session.token !== req.token) {
56652
+ return res.status(401).json({
56653
+ error: "Token mismatch for session"
56654
+ });
56655
+ }
56656
+ session.lastActivity = Date.now();
56657
+ res.status(200).send("SSE endpoint stub");
56658
+ });
56659
+ app.delete("/api/mcp", auth.middleware, async (req, res) => {
56660
+ const sessionId = req.query.sessionId || getSessionIdFromHeaders(req);
56661
+ if (!sessionId) {
56662
+ return res.status(400).json({
56663
+ error: "Session ID required"
56664
+ });
56665
+ }
56666
+ const session = mcpSessions.get(sessionId);
56667
+ if (!session) {
56668
+ return res.status(404).json({
56669
+ error: "Session not found"
56670
+ });
56671
+ }
56672
+ if (session.token !== req.token) {
56673
+ return res.status(401).json({
56674
+ error: "Token mismatch for session"
56675
+ });
56676
+ }
56677
+ console.log(`Closing MCP session: ${sessionId}`);
56678
+ if (sessionManager) {
56679
+ sessionManager.releaseAllForMcpSession(sessionId);
56680
+ }
56681
+ await session.transport.close();
56682
+ mcpSessions.delete(sessionId);
56683
+ res.status(204).send();
56684
+ });
56382
56685
  return Object.freeze({});
56383
56686
  };
56384
- var McpRoutes_default = { create: create3 };
56687
+ var McpRoutes_default = { create: create4 };
56385
56688
 
56386
56689
  // src/static/StaticRoutes.js
56387
56690
  var import_express = __toESM(require_express2(), 1);
56388
- var create4 = (app, options) => {
56691
+ var create5 = (app, options) => {
56389
56692
  const { staticPath } = options;
56390
56693
  const configure = () => {
56391
56694
  app.use(import_express.default.static(staticPath, {
@@ -56395,10 +56698,10 @@ var create4 = (app, options) => {
56395
56698
  configure();
56396
56699
  return Object.freeze({});
56397
56700
  };
56398
- var StaticRoutes_default = { create: create4 };
56701
+ var StaticRoutes_default = { create: create5 };
56399
56702
 
56400
56703
  // src/test/TestRoutes.js
56401
- var create5 = (app, { sessionManager }) => {
56704
+ var create6 = (app, { sessionManager }) => {
56402
56705
  if (!sessionManager) {
56403
56706
  console.warn("[TestRoutes] SessionManager not provided, skipping test routes");
56404
56707
  return;
@@ -56432,7 +56735,7 @@ var create5 = (app, { sessionManager }) => {
56432
56735
  });
56433
56736
  console.log("[TestRoutes] Test endpoints registered: GET /test/sessions, POST /test/command");
56434
56737
  };
56435
- var TestRoutes_default = { create: create5 };
56738
+ var TestRoutes_default = { create: create6 };
56436
56739
 
56437
56740
  // node_modules/ws/wrapper.mjs
56438
56741
  var import_stream = __toESM(require_stream(), 1);
@@ -56566,7 +56869,7 @@ var MessageValidator_default = {
56566
56869
  };
56567
56870
 
56568
56871
  // src/wss/WebSocketServer.js
56569
- var create6 = ({ server, sessionManager }) => {
56872
+ var create7 = ({ server, sessionManager }) => {
56570
56873
  if (!server) {
56571
56874
  throw new Error("WebSocketServer requires HTTP server instance");
56572
56875
  }
@@ -56637,11 +56940,11 @@ var create6 = ({ server, sessionManager }) => {
56637
56940
  stop
56638
56941
  });
56639
56942
  };
56640
- var WebSocketServer_default = { create: create6 };
56943
+ var WebSocketServer_default = { create: create7 };
56641
56944
 
56642
56945
  // src/Server.js
56643
- var create7 = (options = {}) => {
56644
- const { port = 3000, staticPath, sessionManager } = options;
56946
+ var create8 = (options = {}) => {
56947
+ const { port = 3000, staticPath, sessionManager, store } = options;
56645
56948
  const app = import_express2.default();
56646
56949
  const httpServer = createServer(app);
56647
56950
  app.use(import_cors.default());
@@ -56652,8 +56955,11 @@ var create7 = (options = {}) => {
56652
56955
  timestamp: new Date().toISOString()
56653
56956
  });
56654
56957
  });
56655
- ApiRoutes_default.create(app);
56656
- McpRoutes_default.create(app, { sessionManager });
56958
+ ApiRoutes_default.create(app, { store });
56959
+ if (store) {
56960
+ ProjectRoutes_default.create(app, { store });
56961
+ }
56962
+ McpRoutes_default.create(app, { sessionManager, store });
56657
56963
  StaticRoutes_default.create(app, { staticPath });
56658
56964
  if (sessionManager) {
56659
56965
  TestRoutes_default.create(app, { sessionManager });
@@ -56692,7 +56998,7 @@ var create7 = (options = {}) => {
56692
56998
  sessionManager
56693
56999
  });
56694
57000
  };
56695
- var Server_default = { create: create7 };
57001
+ var Server_default = { create: create8 };
56696
57002
 
56697
57003
  // src/wss/SessionManager.js
56698
57004
  var { SessionState: SessionState2 } = MessageValidator_default;
@@ -56700,12 +57006,13 @@ var DEFAULT_HEARTBEAT_INTERVAL_MS = 30000;
56700
57006
  var DEFAULT_PONG_TIMEOUT_MS = 5000;
56701
57007
  var DEFAULT_MAX_MISSED_PONGS = 3;
56702
57008
  var DEFAULT_COMMAND_TIMEOUT_MS = 30000;
56703
- var create8 = (options = {}) => {
57009
+ var create9 = (options = {}) => {
56704
57010
  const {
56705
57011
  heartbeatIntervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS,
56706
57012
  pongTimeoutMs = DEFAULT_PONG_TIMEOUT_MS,
56707
57013
  maxMissedPongs = DEFAULT_MAX_MISSED_PONGS,
56708
- commandTimeoutMs = DEFAULT_COMMAND_TIMEOUT_MS
57014
+ commandTimeoutMs = DEFAULT_COMMAND_TIMEOUT_MS,
57015
+ onSessionEnded = null
56709
57016
  } = options;
56710
57017
  const sessions = new Map;
56711
57018
  const connectionToPageId = new Map;
@@ -56737,6 +57044,9 @@ var create8 = (options = {}) => {
56737
57044
  if (session.wsConnection) {
56738
57045
  connectionToPageId.delete(session.wsConnection);
56739
57046
  }
57047
+ if (onSessionEnded && session.claimedBy) {
57048
+ onSessionEnded(pageId, session.claimedBy);
57049
+ }
56740
57050
  session.state = SessionState2.REMOVED;
56741
57051
  sessions.delete(pageId);
56742
57052
  };
@@ -56813,7 +57123,8 @@ var create8 = (options = {}) => {
56813
57123
  pongTimeout: null,
56814
57124
  missedPongs: 0,
56815
57125
  pendingCommands: new Map,
56816
- seq: 0
57126
+ seq: 0,
57127
+ claimedBy: null
56817
57128
  };
56818
57129
  sessions.set(pageId, session);
56819
57130
  connectionToPageId.set(ws, pageId);
@@ -56930,9 +57241,55 @@ var create8 = (options = {}) => {
56930
57241
  pageId: session.pageId,
56931
57242
  state: session.state,
56932
57243
  capabilities: session.capabilities,
56933
- version: session.version
57244
+ version: session.version,
57245
+ claimedBy: session.claimedBy
56934
57246
  }));
56935
57247
  };
57248
+ const claimSession = (pageId, mcpSessionId) => {
57249
+ const session = sessions.get(pageId);
57250
+ if (!session) {
57251
+ return { success: false, error: "NOT_FOUND" };
57252
+ }
57253
+ if (session.claimedBy === mcpSessionId) {
57254
+ return { success: true };
57255
+ }
57256
+ if (session.claimedBy !== null) {
57257
+ return { success: false, error: "ALREADY_CLAIMED" };
57258
+ }
57259
+ session.claimedBy = mcpSessionId;
57260
+ console.info(`[SessionManager] Session ${pageId} claimed by MCP session ${mcpSessionId}`);
57261
+ return { success: true };
57262
+ };
57263
+ const releaseSession = (pageId, mcpSessionId) => {
57264
+ const session = sessions.get(pageId);
57265
+ if (!session) {
57266
+ return { success: false, error: "NOT_FOUND" };
57267
+ }
57268
+ if (session.claimedBy === null) {
57269
+ return { success: true };
57270
+ }
57271
+ if (session.claimedBy !== mcpSessionId) {
57272
+ return { success: false, error: "NOT_CLAIMED_BY_YOU" };
57273
+ }
57274
+ session.claimedBy = null;
57275
+ console.info(`[SessionManager] Session ${pageId} released by MCP session ${mcpSessionId}`);
57276
+ return { success: true };
57277
+ };
57278
+ const isClaimedBy = (pageId, mcpSessionId) => {
57279
+ const session = sessions.get(pageId);
57280
+ if (!session) {
57281
+ return false;
57282
+ }
57283
+ return session.claimedBy === mcpSessionId;
57284
+ };
57285
+ const releaseAllForMcpSession = (mcpSessionId) => {
57286
+ for (const session of sessions.values()) {
57287
+ if (session.claimedBy === mcpSessionId) {
57288
+ session.claimedBy = null;
57289
+ console.info(`[SessionManager] Session ${session.pageId} released (MCP session ${mcpSessionId} ended)`);
57290
+ }
57291
+ }
57292
+ };
56936
57293
  return Object.freeze({
56937
57294
  getSession,
56938
57295
  getPageIdForConnection,
@@ -56943,20 +57300,97 @@ var create8 = (options = {}) => {
56943
57300
  handleDisconnect,
56944
57301
  sendCommand,
56945
57302
  endSession,
56946
- shutdown
57303
+ shutdown,
57304
+ claimSession,
57305
+ releaseSession,
57306
+ isClaimedBy,
57307
+ releaseAllForMcpSession
56947
57308
  });
56948
57309
  };
56949
- var SessionManager_default = { create: create8 };
57310
+ var SessionManager_default = { create: create9 };
56950
57311
 
56951
- // src/main.js
57312
+ // src/core/LocalStore.js
57313
+ import { createHash, randomUUID as randomUUID3 } from "crypto";
57314
+ import fs from "fs/promises";
57315
+ import os from "os";
56952
57316
  import path from "path";
57317
+ var DATA_DIR = path.join(os.homedir(), ".caw");
57318
+ var PROJECTS_FILE = path.join(DATA_DIR, "projects.json");
57319
+ var generateToken = (id, name) => {
57320
+ const input = `${id}${name}`;
57321
+ return createHash("sha256").update(input).digest("hex");
57322
+ };
57323
+ var create10 = () => {
57324
+ const readProjects = async () => {
57325
+ try {
57326
+ const data = await fs.readFile(PROJECTS_FILE, "utf8");
57327
+ return JSON.parse(data);
57328
+ } catch (error) {
57329
+ if (error.code === "ENOENT") {
57330
+ return [];
57331
+ }
57332
+ throw error;
57333
+ }
57334
+ };
57335
+ const writeProjects = async (projects) => {
57336
+ await fs.mkdir(DATA_DIR, { recursive: true });
57337
+ const tempFile = `${PROJECTS_FILE}.${randomUUID3()}.tmp`;
57338
+ await fs.writeFile(tempFile, JSON.stringify(projects, null, 2), "utf8");
57339
+ await fs.rename(tempFile, PROJECTS_FILE);
57340
+ };
57341
+ const getNextId = (projects) => {
57342
+ if (projects.length === 0) {
57343
+ return 1;
57344
+ }
57345
+ return Math.max(...projects.map((p) => p.id)) + 1;
57346
+ };
57347
+ const getProjects = async () => {
57348
+ return readProjects();
57349
+ };
57350
+ const getProject = async (id) => {
57351
+ const projects = await readProjects();
57352
+ return projects.find((p) => p.id === id);
57353
+ };
57354
+ const getProjectByToken2 = async (token) => {
57355
+ const projects = await readProjects();
57356
+ return projects.find((p) => p.token === token);
57357
+ };
57358
+ const createProject = async ({ name, path: projectPath }) => {
57359
+ const projects = await readProjects();
57360
+ const id = getNextId(projects);
57361
+ const token = generateToken(id, name);
57362
+ const now = new Date().toISOString();
57363
+ const project = {
57364
+ id,
57365
+ name,
57366
+ path: projectPath,
57367
+ token,
57368
+ createdAt: now,
57369
+ updatedAt: now
57370
+ };
57371
+ projects.push(project);
57372
+ await writeProjects(projects);
57373
+ return project;
57374
+ };
57375
+ return Object.freeze({
57376
+ getProjects,
57377
+ getProject,
57378
+ getProjectByToken: getProjectByToken2,
57379
+ createProject
57380
+ });
57381
+ };
57382
+ var LocalStore_default = { create: create10, generateToken };
57383
+
57384
+ // src/main.js
57385
+ import path2 from "path";
56953
57386
  import { fileURLToPath } from "url";
56954
57387
  var __filename2 = fileURLToPath(import.meta.url);
56955
- var __dirname2 = path.dirname(__filename2);
57388
+ var __dirname2 = path2.dirname(__filename2);
56956
57389
  var port = parseInt(process.env.PORT || "3000", 10);
56957
- var staticPath = process.env.STATIC_PATH || path.join(__dirname2, "public_html");
57390
+ var staticPath = process.env.STATIC_PATH || path2.join(__dirname2, "public_html");
57391
+ var store = LocalStore_default.create();
56958
57392
  var sessionManager = SessionManager_default.create();
56959
- var server = Server_default.create({ port, staticPath, sessionManager });
57393
+ var server = Server_default.create({ port, staticPath, sessionManager, store });
56960
57394
  server.start();
56961
57395
  var shutdown = async (signal) => {
56962
57396
  console.log(`
@@ -56974,4 +57408,4 @@ ${signal} received, shutting down gracefully...`);
56974
57408
  process.on("SIGINT", () => shutdown("SIGINT"));
56975
57409
  process.on("SIGTERM", () => shutdown("SIGTERM"));
56976
57410
 
56977
- //# debugId=BE2433F55DA5A3FF64756E2164756E21
57411
+ //# debugId=043C8B45A50E012E64756E2164756E21