@kelceyp/caw-server 0.0.5 → 0.0.9

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,205 @@ ${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}` }],
56529
+ isError: true
56530
+ };
56531
+ }
56532
+ });
56533
+ server.registerTool("get_message_count", {
56534
+ title: "Get Message Count",
56535
+ description: "Get the total number of turns (message pairs) in the conversation",
56536
+ inputSchema: exports_external2.object({
56537
+ pageId: exports_external2.string().describe("Page ID of the ChatGPT tab")
56538
+ })
56539
+ }, async ({ pageId }) => {
56540
+ if (!sessionManager) {
56541
+ return {
56542
+ content: [{ type: "text", text: "Error: SessionManager not initialized" }],
56543
+ isError: true
56544
+ };
56545
+ }
56546
+ const claimCheck = verifyClaimedError(pageId);
56547
+ if (claimCheck.error) {
56548
+ return {
56549
+ content: [{ type: "text", text: `Error: ${claimCheck.message}` }],
56550
+ isError: true
56551
+ };
56552
+ }
56553
+ try {
56554
+ const result = await sessionManager.sendCommand(pageId, "getMessageCount", {});
56555
+ return {
56556
+ content: [{ type: "text", text: `Message count: ${result.count} turns` }]
56557
+ };
56558
+ } catch (error) {
56559
+ return {
56560
+ content: [{ type: "text", text: `Error: ${error.message}` }],
56561
+ isError: true
56562
+ };
56563
+ }
56564
+ });
56565
+ server.registerTool("get_message", {
56566
+ title: "Get Message",
56567
+ description: 'Get a specific message by turn number and role, or by address notation (e.g., "5u", "3a", "7u.5-10")',
56568
+ inputSchema: exports_external2.object({
56569
+ pageId: exports_external2.string().describe("Page ID of the ChatGPT tab"),
56570
+ turn: exports_external2.number().optional().describe("Turn number (1-based)"),
56571
+ role: exports_external2.enum(["user", "assistant"]).optional().describe("Message role"),
56572
+ address: exports_external2.string().optional().describe('Address notation (e.g., "5u", "3a", "7u.5-10")')
56573
+ })
56574
+ }, async ({ pageId, turn, role, address }) => {
56575
+ if (!sessionManager) {
56576
+ return {
56577
+ content: [{ type: "text", text: "Error: SessionManager not initialized" }],
56578
+ isError: true
56579
+ };
56580
+ }
56581
+ const claimCheck = verifyClaimedError(pageId);
56582
+ if (claimCheck.error) {
56583
+ return {
56584
+ content: [{ type: "text", text: `Error: ${claimCheck.message}` }],
56585
+ isError: true
56586
+ };
56587
+ }
56588
+ if (!address && (turn === undefined || !role)) {
56589
+ return {
56590
+ content: [{ type: "text", text: "Error: Either address or (turn + role) is required" }],
56591
+ isError: true
56592
+ };
56593
+ }
56594
+ try {
56595
+ const result = await sessionManager.sendCommand(pageId, "getMessage", { turn, role, address });
56596
+ if (result.error) {
56597
+ return {
56598
+ content: [{ type: "text", text: `Error: ${result.error}` }],
56599
+ isError: true
56600
+ };
56601
+ }
56602
+ if (result.text) {
56603
+ return {
56604
+ content: [{ type: "text", text: result.text }]
56605
+ };
56606
+ } else {
56607
+ return {
56608
+ content: [{ type: "text", text: "Message not found" }]
56609
+ };
56610
+ }
56611
+ } catch (error) {
56612
+ return {
56613
+ content: [{ type: "text", text: `Error: ${error.message}` }],
56614
+ isError: true
56615
+ };
56616
+ }
56617
+ });
56618
+ server.registerTool("get_conversation", {
56619
+ title: "Get Conversation",
56620
+ description: "Get the full conversation history as a structured array of messages",
56621
+ inputSchema: exports_external2.object({
56622
+ pageId: exports_external2.string().describe("Page ID of the ChatGPT tab")
56623
+ })
56624
+ }, async ({ pageId }) => {
56625
+ if (!sessionManager) {
56626
+ return {
56627
+ content: [{ type: "text", text: "Error: SessionManager not initialized" }],
56628
+ isError: true
56629
+ };
56630
+ }
56631
+ const claimCheck = verifyClaimedError(pageId);
56632
+ if (claimCheck.error) {
56633
+ return {
56634
+ content: [{ type: "text", text: `Error: ${claimCheck.message}` }],
56635
+ isError: true
56636
+ };
56637
+ }
56638
+ try {
56639
+ const result = await sessionManager.sendCommand(pageId, "getConversation", {});
56640
+ const messages = result.messages || [];
56641
+ if (messages.length === 0) {
56642
+ return {
56643
+ content: [{ type: "text", text: "No messages in conversation" }]
56644
+ };
56645
+ }
56646
+ const formatted = messages.map((m) => {
56647
+ const roleChar = m.role === "user" ? "u" : "a";
56648
+ const addr = `${m.turn}${roleChar}`;
56649
+ const preview = m.text.length > 100 ? m.text.substring(0, 100) + "..." : m.text;
56650
+ return `[${addr}] ${m.role}: ${preview}`;
56651
+ }).join(`
56652
+
56653
+ `);
56654
+ return {
56655
+ content: [{ type: "text", text: `Conversation (${messages.length} messages):
56656
+
56657
+ ${formatted}` }]
56658
+ };
56659
+ } catch (error) {
56660
+ return {
56661
+ content: [{ type: "text", text: `Error: ${error.message}` }],
56662
+ isError: true
56663
+ };
56664
+ }
56665
+ });
56666
+ server.registerTool("find_message", {
56667
+ title: "Find Message",
56668
+ description: "Search conversation messages using a regex pattern",
56669
+ inputSchema: exports_external2.object({
56670
+ pageId: exports_external2.string().describe("Page ID of the ChatGPT tab"),
56671
+ pattern: exports_external2.string().describe("Regex pattern to search for"),
56672
+ role: exports_external2.enum(["user", "assistant", "all"]).optional().describe("Filter by role (default: all)"),
56673
+ firstMatch: exports_external2.boolean().optional().describe("Stop after first match (default: false)"),
56674
+ caseInsensitive: exports_external2.boolean().optional().describe("Case insensitive search (default: false)")
56675
+ })
56676
+ }, async ({ pageId, pattern, role, firstMatch, caseInsensitive }) => {
56677
+ if (!sessionManager) {
56678
+ return {
56679
+ content: [{ type: "text", text: "Error: SessionManager not initialized" }],
56680
+ isError: true
56681
+ };
56682
+ }
56683
+ const claimCheck = verifyClaimedError(pageId);
56684
+ if (claimCheck.error) {
56685
+ return {
56686
+ content: [{ type: "text", text: `Error: ${claimCheck.message}` }],
56687
+ isError: true
56688
+ };
56689
+ }
56690
+ try {
56691
+ const result = await sessionManager.sendCommand(pageId, "findMessage", {
56692
+ pattern,
56693
+ role: role || "all",
56694
+ firstMatch: firstMatch || false,
56695
+ caseInsensitive: caseInsensitive || false
56696
+ });
56697
+ if (result.error) {
56698
+ return {
56699
+ content: [{ type: "text", text: `Error: ${result.error}` }],
56700
+ isError: true
56701
+ };
56702
+ }
56703
+ const matches = result.matches || [];
56704
+ if (matches.length === 0) {
56705
+ return {
56706
+ content: [{ type: "text", text: "No matches found" }]
56707
+ };
56708
+ }
56709
+ const formatted = matches.map((m) => `• ${m.address} (turn ${m.turn}, ${m.role})`).join(`
56710
+ `);
56711
+ return {
56712
+ content: [{ type: "text", text: `Found ${result.count} match(es):
56713
+ ${formatted}` }]
56714
+ };
56715
+ } catch (error) {
56716
+ return {
56717
+ content: [{ type: "text", text: `Error: ${error.message}` }],
56345
56718
  isError: true
56346
56719
  };
56347
56720
  }
@@ -56351,41 +56724,160 @@ ${result.text}`
56351
56724
  var McpServer_default = createMcpServer;
56352
56725
 
56353
56726
  // src/mcp/McpRoutes.js
56354
- var create3 = (app, { sessionManager } = {}) => {
56355
- const auth = Auth_default.create();
56356
- const mcpServer = McpServer_default({ sessionManager });
56727
+ var IDLE_TIMEOUT_MS = 60 * 60 * 1000;
56728
+ var CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
56729
+ var create4 = (app, { sessionManager, store } = {}) => {
56730
+ const auth = Auth_default.create({ store });
56731
+ const mcpSessions = new Map;
56732
+ const cleanupInterval = setInterval(() => {
56733
+ const now = Date.now();
56734
+ for (const [sessionId, session] of mcpSessions) {
56735
+ if (now - session.lastActivity > IDLE_TIMEOUT_MS) {
56736
+ console.log(`Cleaning up idle MCP session: ${sessionId}`);
56737
+ if (sessionManager) {
56738
+ sessionManager.releaseAllForMcpSession(sessionId);
56739
+ }
56740
+ session.transport.close();
56741
+ mcpSessions.delete(sessionId);
56742
+ }
56743
+ }
56744
+ }, CLEANUP_INTERVAL_MS);
56745
+ cleanupInterval.unref();
56746
+ const getSessionIdFromHeaders = (req) => {
56747
+ return req.headers["mcp-session-id"] || req.headers["Mcp-Session-Id"];
56748
+ };
56357
56749
  app.post("/api/mcp", auth.middleware, async (req, res) => {
56750
+ const existingSessionId = getSessionIdFromHeaders(req);
56751
+ const { projectId, token } = req;
56752
+ if (existingSessionId && mcpSessions.has(existingSessionId)) {
56753
+ const session = mcpSessions.get(existingSessionId);
56754
+ if (session.token !== token) {
56755
+ return res.status(401).json({
56756
+ jsonrpc: "2.0",
56757
+ error: { code: -32001, message: "Token mismatch for session" },
56758
+ id: null
56759
+ });
56760
+ }
56761
+ console.log(`Reusing MCP session: ${existingSessionId}`);
56762
+ session.lastActivity = Date.now();
56763
+ res.setHeader("Mcp-Session-Id", existingSessionId);
56764
+ try {
56765
+ await session.transport.handleRequest(req, res, req.body);
56766
+ } catch (error) {
56767
+ console.error("Error handling MCP request:", error);
56768
+ if (!res.headersSent) {
56769
+ res.status(500).json({
56770
+ jsonrpc: "2.0",
56771
+ error: { code: -32603, message: "Internal server error" },
56772
+ id: null
56773
+ });
56774
+ }
56775
+ }
56776
+ return;
56777
+ }
56778
+ console.log(`Creating new MCP session for project: ${projectId}`);
56779
+ let newSessionId = null;
56780
+ const transport = new StreamableHTTPServerTransport({
56781
+ sessionIdGenerator: () => {
56782
+ newSessionId = randomUUID2();
56783
+ return newSessionId;
56784
+ },
56785
+ onsessioninitialized: (sessionId) => {
56786
+ console.log(`MCP session initialized: ${sessionId}`);
56787
+ res.setHeader("Mcp-Session-Id", sessionId);
56788
+ }
56789
+ });
56790
+ const server = McpServer_default({
56791
+ sessionManager,
56792
+ projectId,
56793
+ token,
56794
+ getMcpSessionId: () => newSessionId,
56795
+ mcpSessions
56796
+ });
56358
56797
  try {
56359
- const transport = new StreamableHTTPServerTransport({
56360
- sessionIdGenerator: undefined,
56361
- enableJsonResponse: true
56362
- });
56363
- res.on("close", () => {
56364
- transport.close();
56798
+ await server.connect(transport);
56799
+ const session = {
56800
+ sessionId: newSessionId,
56801
+ transport,
56802
+ server,
56803
+ projectId,
56804
+ token,
56805
+ claimedConnections: new Set,
56806
+ createdAt: Date.now(),
56807
+ lastActivity: Date.now()
56808
+ };
56809
+ res.on("finish", () => {
56810
+ if (newSessionId && !mcpSessions.has(newSessionId)) {
56811
+ mcpSessions.set(newSessionId, session);
56812
+ console.log(`MCP session stored: ${newSessionId}`);
56813
+ }
56365
56814
  });
56366
- await mcpServer.connect(transport);
56367
56815
  await transport.handleRequest(req, res, req.body);
56368
56816
  } catch (error) {
56369
56817
  console.error("Error handling MCP request:", error);
56370
56818
  if (!res.headersSent) {
56371
56819
  res.status(500).json({
56372
56820
  jsonrpc: "2.0",
56373
- error: {
56374
- code: -32603,
56375
- message: "Internal server error"
56376
- },
56821
+ error: { code: -32603, message: "Internal server error" },
56377
56822
  id: null
56378
56823
  });
56379
56824
  }
56380
56825
  }
56381
56826
  });
56827
+ app.get("/api/mcp", auth.middleware, async (req, res) => {
56828
+ const sessionId = req.query.sessionId || getSessionIdFromHeaders(req);
56829
+ if (!sessionId) {
56830
+ return res.status(400).json({
56831
+ error: "Session ID required"
56832
+ });
56833
+ }
56834
+ const session = mcpSessions.get(sessionId);
56835
+ if (!session) {
56836
+ return res.status(404).json({
56837
+ error: "Session not found"
56838
+ });
56839
+ }
56840
+ if (session.token !== req.token) {
56841
+ return res.status(401).json({
56842
+ error: "Token mismatch for session"
56843
+ });
56844
+ }
56845
+ session.lastActivity = Date.now();
56846
+ res.status(200).send("SSE endpoint stub");
56847
+ });
56848
+ app.delete("/api/mcp", auth.middleware, async (req, res) => {
56849
+ const sessionId = req.query.sessionId || getSessionIdFromHeaders(req);
56850
+ if (!sessionId) {
56851
+ return res.status(400).json({
56852
+ error: "Session ID required"
56853
+ });
56854
+ }
56855
+ const session = mcpSessions.get(sessionId);
56856
+ if (!session) {
56857
+ return res.status(404).json({
56858
+ error: "Session not found"
56859
+ });
56860
+ }
56861
+ if (session.token !== req.token) {
56862
+ return res.status(401).json({
56863
+ error: "Token mismatch for session"
56864
+ });
56865
+ }
56866
+ console.log(`Closing MCP session: ${sessionId}`);
56867
+ if (sessionManager) {
56868
+ sessionManager.releaseAllForMcpSession(sessionId);
56869
+ }
56870
+ await session.transport.close();
56871
+ mcpSessions.delete(sessionId);
56872
+ res.status(204).send();
56873
+ });
56382
56874
  return Object.freeze({});
56383
56875
  };
56384
- var McpRoutes_default = { create: create3 };
56876
+ var McpRoutes_default = { create: create4 };
56385
56877
 
56386
56878
  // src/static/StaticRoutes.js
56387
56879
  var import_express = __toESM(require_express2(), 1);
56388
- var create4 = (app, options) => {
56880
+ var create5 = (app, options) => {
56389
56881
  const { staticPath } = options;
56390
56882
  const configure = () => {
56391
56883
  app.use(import_express.default.static(staticPath, {
@@ -56395,10 +56887,10 @@ var create4 = (app, options) => {
56395
56887
  configure();
56396
56888
  return Object.freeze({});
56397
56889
  };
56398
- var StaticRoutes_default = { create: create4 };
56890
+ var StaticRoutes_default = { create: create5 };
56399
56891
 
56400
56892
  // src/test/TestRoutes.js
56401
- var create5 = (app, { sessionManager }) => {
56893
+ var create6 = (app, { sessionManager }) => {
56402
56894
  if (!sessionManager) {
56403
56895
  console.warn("[TestRoutes] SessionManager not provided, skipping test routes");
56404
56896
  return;
@@ -56432,7 +56924,7 @@ var create5 = (app, { sessionManager }) => {
56432
56924
  });
56433
56925
  console.log("[TestRoutes] Test endpoints registered: GET /test/sessions, POST /test/command");
56434
56926
  };
56435
- var TestRoutes_default = { create: create5 };
56927
+ var TestRoutes_default = { create: create6 };
56436
56928
 
56437
56929
  // node_modules/ws/wrapper.mjs
56438
56930
  var import_stream = __toESM(require_stream(), 1);
@@ -56566,7 +57058,7 @@ var MessageValidator_default = {
56566
57058
  };
56567
57059
 
56568
57060
  // src/wss/WebSocketServer.js
56569
- var create6 = ({ server, sessionManager }) => {
57061
+ var create7 = ({ server, sessionManager }) => {
56570
57062
  if (!server) {
56571
57063
  throw new Error("WebSocketServer requires HTTP server instance");
56572
57064
  }
@@ -56637,11 +57129,11 @@ var create6 = ({ server, sessionManager }) => {
56637
57129
  stop
56638
57130
  });
56639
57131
  };
56640
- var WebSocketServer_default = { create: create6 };
57132
+ var WebSocketServer_default = { create: create7 };
56641
57133
 
56642
57134
  // src/Server.js
56643
- var create7 = (options = {}) => {
56644
- const { port = 3000, staticPath, sessionManager } = options;
57135
+ var create8 = (options = {}) => {
57136
+ const { port = 3000, staticPath, sessionManager, store } = options;
56645
57137
  const app = import_express2.default();
56646
57138
  const httpServer = createServer(app);
56647
57139
  app.use(import_cors.default());
@@ -56652,8 +57144,11 @@ var create7 = (options = {}) => {
56652
57144
  timestamp: new Date().toISOString()
56653
57145
  });
56654
57146
  });
56655
- ApiRoutes_default.create(app);
56656
- McpRoutes_default.create(app, { sessionManager });
57147
+ ApiRoutes_default.create(app, { store });
57148
+ if (store) {
57149
+ ProjectRoutes_default.create(app, { store });
57150
+ }
57151
+ McpRoutes_default.create(app, { sessionManager, store });
56657
57152
  StaticRoutes_default.create(app, { staticPath });
56658
57153
  if (sessionManager) {
56659
57154
  TestRoutes_default.create(app, { sessionManager });
@@ -56692,7 +57187,7 @@ var create7 = (options = {}) => {
56692
57187
  sessionManager
56693
57188
  });
56694
57189
  };
56695
- var Server_default = { create: create7 };
57190
+ var Server_default = { create: create8 };
56696
57191
 
56697
57192
  // src/wss/SessionManager.js
56698
57193
  var { SessionState: SessionState2 } = MessageValidator_default;
@@ -56700,12 +57195,13 @@ var DEFAULT_HEARTBEAT_INTERVAL_MS = 30000;
56700
57195
  var DEFAULT_PONG_TIMEOUT_MS = 5000;
56701
57196
  var DEFAULT_MAX_MISSED_PONGS = 3;
56702
57197
  var DEFAULT_COMMAND_TIMEOUT_MS = 30000;
56703
- var create8 = (options = {}) => {
57198
+ var create9 = (options = {}) => {
56704
57199
  const {
56705
57200
  heartbeatIntervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS,
56706
57201
  pongTimeoutMs = DEFAULT_PONG_TIMEOUT_MS,
56707
57202
  maxMissedPongs = DEFAULT_MAX_MISSED_PONGS,
56708
- commandTimeoutMs = DEFAULT_COMMAND_TIMEOUT_MS
57203
+ commandTimeoutMs = DEFAULT_COMMAND_TIMEOUT_MS,
57204
+ onSessionEnded = null
56709
57205
  } = options;
56710
57206
  const sessions = new Map;
56711
57207
  const connectionToPageId = new Map;
@@ -56737,6 +57233,9 @@ var create8 = (options = {}) => {
56737
57233
  if (session.wsConnection) {
56738
57234
  connectionToPageId.delete(session.wsConnection);
56739
57235
  }
57236
+ if (onSessionEnded && session.claimedBy) {
57237
+ onSessionEnded(pageId, session.claimedBy);
57238
+ }
56740
57239
  session.state = SessionState2.REMOVED;
56741
57240
  sessions.delete(pageId);
56742
57241
  };
@@ -56813,7 +57312,8 @@ var create8 = (options = {}) => {
56813
57312
  pongTimeout: null,
56814
57313
  missedPongs: 0,
56815
57314
  pendingCommands: new Map,
56816
- seq: 0
57315
+ seq: 0,
57316
+ claimedBy: null
56817
57317
  };
56818
57318
  sessions.set(pageId, session);
56819
57319
  connectionToPageId.set(ws, pageId);
@@ -56930,9 +57430,55 @@ var create8 = (options = {}) => {
56930
57430
  pageId: session.pageId,
56931
57431
  state: session.state,
56932
57432
  capabilities: session.capabilities,
56933
- version: session.version
57433
+ version: session.version,
57434
+ claimedBy: session.claimedBy
56934
57435
  }));
56935
57436
  };
57437
+ const claimSession = (pageId, mcpSessionId) => {
57438
+ const session = sessions.get(pageId);
57439
+ if (!session) {
57440
+ return { success: false, error: "NOT_FOUND" };
57441
+ }
57442
+ if (session.claimedBy === mcpSessionId) {
57443
+ return { success: true };
57444
+ }
57445
+ if (session.claimedBy !== null) {
57446
+ return { success: false, error: "ALREADY_CLAIMED" };
57447
+ }
57448
+ session.claimedBy = mcpSessionId;
57449
+ console.info(`[SessionManager] Session ${pageId} claimed by MCP session ${mcpSessionId}`);
57450
+ return { success: true };
57451
+ };
57452
+ const releaseSession = (pageId, mcpSessionId) => {
57453
+ const session = sessions.get(pageId);
57454
+ if (!session) {
57455
+ return { success: false, error: "NOT_FOUND" };
57456
+ }
57457
+ if (session.claimedBy === null) {
57458
+ return { success: true };
57459
+ }
57460
+ if (session.claimedBy !== mcpSessionId) {
57461
+ return { success: false, error: "NOT_CLAIMED_BY_YOU" };
57462
+ }
57463
+ session.claimedBy = null;
57464
+ console.info(`[SessionManager] Session ${pageId} released by MCP session ${mcpSessionId}`);
57465
+ return { success: true };
57466
+ };
57467
+ const isClaimedBy = (pageId, mcpSessionId) => {
57468
+ const session = sessions.get(pageId);
57469
+ if (!session) {
57470
+ return false;
57471
+ }
57472
+ return session.claimedBy === mcpSessionId;
57473
+ };
57474
+ const releaseAllForMcpSession = (mcpSessionId) => {
57475
+ for (const session of sessions.values()) {
57476
+ if (session.claimedBy === mcpSessionId) {
57477
+ session.claimedBy = null;
57478
+ console.info(`[SessionManager] Session ${session.pageId} released (MCP session ${mcpSessionId} ended)`);
57479
+ }
57480
+ }
57481
+ };
56936
57482
  return Object.freeze({
56937
57483
  getSession,
56938
57484
  getPageIdForConnection,
@@ -56943,20 +57489,102 @@ var create8 = (options = {}) => {
56943
57489
  handleDisconnect,
56944
57490
  sendCommand,
56945
57491
  endSession,
56946
- shutdown
57492
+ shutdown,
57493
+ claimSession,
57494
+ releaseSession,
57495
+ isClaimedBy,
57496
+ releaseAllForMcpSession
56947
57497
  });
56948
57498
  };
56949
- var SessionManager_default = { create: create8 };
57499
+ var SessionManager_default = { create: create9 };
56950
57500
 
56951
- // src/main.js
57501
+ // src/core/LocalStore.js
57502
+ import { createHash, randomUUID as randomUUID3 } from "crypto";
57503
+ import fs from "fs/promises";
57504
+ import os from "os";
56952
57505
  import path from "path";
57506
+ var DATA_DIR = path.join(os.homedir(), ".caw");
57507
+ var PROJECTS_FILE = path.join(DATA_DIR, "projects.json");
57508
+ var generateToken = (id, name) => {
57509
+ const input = `${id}${name}`;
57510
+ return createHash("sha256").update(input).digest("hex");
57511
+ };
57512
+ var create10 = () => {
57513
+ const readProjects = async () => {
57514
+ try {
57515
+ const data = await fs.readFile(PROJECTS_FILE, "utf8");
57516
+ return JSON.parse(data);
57517
+ } catch (error) {
57518
+ if (error.code === "ENOENT") {
57519
+ return [];
57520
+ }
57521
+ throw error;
57522
+ }
57523
+ };
57524
+ const writeProjects = async (projects) => {
57525
+ await fs.mkdir(DATA_DIR, { recursive: true });
57526
+ const tempFile = `${PROJECTS_FILE}.${randomUUID3()}.tmp`;
57527
+ await fs.writeFile(tempFile, JSON.stringify(projects, null, 2), "utf8");
57528
+ await fs.rename(tempFile, PROJECTS_FILE);
57529
+ };
57530
+ const getNextId = (projects) => {
57531
+ if (projects.length === 0) {
57532
+ return 1;
57533
+ }
57534
+ return Math.max(...projects.map((p) => p.id)) + 1;
57535
+ };
57536
+ const getProjects = async () => {
57537
+ return readProjects();
57538
+ };
57539
+ const getProject = async (id) => {
57540
+ const projects = await readProjects();
57541
+ return projects.find((p) => p.id === id);
57542
+ };
57543
+ const getProjectByToken2 = async (token) => {
57544
+ const projects = await readProjects();
57545
+ return projects.find((p) => p.token === token);
57546
+ };
57547
+ const createProject = async ({ name, path: projectPath }) => {
57548
+ const projects = await readProjects();
57549
+ const id = getNextId(projects);
57550
+ const token = generateToken(id, name);
57551
+ const now = new Date().toISOString();
57552
+ const project = {
57553
+ id,
57554
+ name,
57555
+ path: projectPath,
57556
+ token,
57557
+ createdAt: now,
57558
+ updatedAt: now
57559
+ };
57560
+ projects.push(project);
57561
+ await writeProjects(projects);
57562
+ return project;
57563
+ };
57564
+ return Object.freeze({
57565
+ getProjects,
57566
+ getProject,
57567
+ getProjectByToken: getProjectByToken2,
57568
+ createProject
57569
+ });
57570
+ };
57571
+ var LocalStore_default = { create: create10, generateToken };
57572
+
57573
+ // src/main.js
57574
+ import path2 from "path";
56953
57575
  import { fileURLToPath } from "url";
56954
57576
  var __filename2 = fileURLToPath(import.meta.url);
56955
- var __dirname2 = path.dirname(__filename2);
56956
- var port = parseInt(process.env.PORT || "3000", 10);
56957
- var staticPath = process.env.STATIC_PATH || path.join(__dirname2, "public_html");
57577
+ var __dirname2 = path2.dirname(__filename2);
57578
+ var port = process.env.CAW_PORT;
57579
+ if (!port) {
57580
+ console.error("ERROR: CAW_PORT environment variable is required");
57581
+ console.error("Run via pm2 or set CAW_PORT manually: CAW_PORT=3131 node dist/main.js");
57582
+ process.exit(1);
57583
+ }
57584
+ var staticPath = process.env.STATIC_PATH || path2.join(__dirname2, "public_html");
57585
+ var store = LocalStore_default.create();
56958
57586
  var sessionManager = SessionManager_default.create();
56959
- var server = Server_default.create({ port, staticPath, sessionManager });
57587
+ var server = Server_default.create({ port, staticPath, sessionManager, store });
56960
57588
  server.start();
56961
57589
  var shutdown = async (signal) => {
56962
57590
  console.log(`
@@ -56974,4 +57602,4 @@ ${signal} received, shutting down gracefully...`);
56974
57602
  process.on("SIGINT", () => shutdown("SIGINT"));
56975
57603
  process.on("SIGTERM", () => shutdown("SIGTERM"));
56976
57604
 
56977
- //# debugId=BE2433F55DA5A3FF64756E2164756E21
57605
+ //# debugId=36327466799C28A164756E2164756E21