@rallycry/conveyor-mcp 5.0.3 → 5.0.4

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/README.md CHANGED
@@ -76,6 +76,12 @@ CONVEYOR_PROJECT_ID = "<project-id>"
76
76
  | `CONVEYOR_PROJECT_ID` | no | Default project for unqualified tools. Omit it for multi-project mode. |
77
77
  | `CONVEYOR_SUBPROJECT_ID` | no | Default board. Unqualified `create_task`/`list_tasks`/`search_tasks` target it. |
78
78
 
79
+ ## How the server exits
80
+
81
+ The server's lifetime is its stdin pipe. It exits when stdin reaches EOF, on
82
+ `SIGINT` or `SIGTERM`, and when its parent process changes — so a client that
83
+ force-quits, crashes, or is torn down never leaves a server behind.
84
+
79
85
  ## Why `npx -y …@latest`
80
86
 
81
87
  Every form above launches the server through
@@ -0,0 +1,45 @@
1
+ // src/lifecycle.ts
2
+ function watchForClientExit(onExit, deps = {}) {
3
+ const stdin = deps.stdin ?? process.stdin;
4
+ const signals = deps.signals ?? process;
5
+ const getPpid = deps.getPpid ?? (() => process.ppid);
6
+ const intervalMs = deps.intervalMs ?? 3e4;
7
+ const watchStdin = deps.watchStdin ?? true;
8
+ let fired = false;
9
+ const fire = () => {
10
+ if (fired) return;
11
+ fired = true;
12
+ onExit();
13
+ };
14
+ signals.on("SIGINT", fire);
15
+ signals.on("SIGTERM", fire);
16
+ signals.on("SIGHUP", fire);
17
+ if (watchStdin && stdin.isTTY !== true) {
18
+ if (deps.resumeStdin) stdin.resume?.();
19
+ stdin.on("end", fire);
20
+ stdin.on("close", fire);
21
+ }
22
+ const initialPpid = getPpid();
23
+ const orphanCheck = setInterval(() => {
24
+ if (getPpid() !== initialPpid) fire();
25
+ }, intervalMs);
26
+ orphanCheck.unref?.();
27
+ return fire;
28
+ }
29
+ function installLifecycle(conn, deps = {}) {
30
+ const exit = deps.exit ?? ((code) => process.exit(code));
31
+ let stopping = false;
32
+ function stop(code = 0) {
33
+ if (stopping) return;
34
+ stopping = true;
35
+ conn.disconnect();
36
+ exit(code);
37
+ }
38
+ watchForClientExit(() => stop(), deps);
39
+ return stop;
40
+ }
41
+
42
+ export {
43
+ watchForClientExit,
44
+ installLifecycle
45
+ };
@@ -49,10 +49,14 @@ function enrichToolError(error, fallback) {
49
49
  var ConveyorConnection = class {
50
50
  socket = null;
51
51
  config;
52
+ socketFactory;
52
53
  /** project slug → id, for resolving `<project>/<card>` card paths. */
53
54
  projectSlugIds = /* @__PURE__ */ new Map();
54
- constructor(config) {
55
+ /** Session rooms this connection has joined, replayed after every reconnect. */
56
+ subscribedSessionIds = /* @__PURE__ */ new Set();
57
+ constructor(config, socketFactory = io) {
55
58
  this.config = config;
59
+ this.socketFactory = socketFactory;
56
60
  }
57
61
  get projectId() {
58
62
  return this.resolveProjectId();
@@ -86,12 +90,32 @@ var ConveyorConnection = class {
86
90
  if (Array.isArray(record.data)) return record.data;
87
91
  return [];
88
92
  }
93
+ /**
94
+ * Re-join every room this connection needs.
95
+ *
96
+ * Runs on the FIRST connect and on every reconnect. A socket.io reconnect
97
+ * starts a fresh server-side socket that belongs to no rooms, and Cloud Run
98
+ * cuts each websocket at its 60-minute request limit, so a subscribe sent
99
+ * only once left long-lived MCP connections silently outside the project room
100
+ * for the rest of the day.
101
+ */
102
+ rejoinRooms() {
103
+ const socket = this.socket;
104
+ if (!socket) return;
105
+ socket.emit("projectService:subscribe", { id: this.config.projectId });
106
+ for (const sessionId of this.subscribedSessionIds) {
107
+ socket.emit("agentSessionService:subscribe", {
108
+ entryId: sessionId,
109
+ requiredLevel: "Read"
110
+ });
111
+ }
112
+ }
89
113
  connect() {
90
114
  return new Promise((resolve, reject) => {
91
115
  let settled = false;
92
116
  let attempts = 0;
93
117
  const maxAttempts = 15;
94
- this.socket = io(
118
+ this.socket = this.socketFactory(
95
119
  this.config.apiUrl,
96
120
  buildConveyorSocketOptions({
97
121
  projectToken: this.config.projectToken,
@@ -99,12 +123,15 @@ var ConveyorConnection = class {
99
123
  })
100
124
  );
101
125
  this.socket.on("connect", () => {
126
+ this.rejoinRooms();
102
127
  if (!settled) {
103
128
  settled = true;
104
- this.socket?.emit("projectService:subscribe", { id: this.config.projectId });
105
129
  resolve();
106
130
  }
107
131
  });
132
+ this.socket.on("disconnect", (reason) => {
133
+ if (reason === "io server disconnect") this.socket?.connect();
134
+ });
108
135
  this.socket.on("connect_error", (err) => {
109
136
  const message = err?.message ?? "unknown error";
110
137
  if (!settled && AUTH_ERROR_RE.test(message)) {
@@ -943,6 +970,7 @@ var ConveyorConnection = class {
943
970
  subscribeToSession(sessionId) {
944
971
  const socket = this.socket;
945
972
  if (!socket) throw new Error("Not connected");
973
+ this.subscribedSessionIds.add(sessionId);
946
974
  socket.emit("agentSessionService:subscribe", {
947
975
  entryId: sessionId,
948
976
  requiredLevel: "Read"
@@ -1013,6 +1041,7 @@ var ConveyorConnection = class {
1013
1041
  disconnect() {
1014
1042
  this.socket?.disconnect();
1015
1043
  this.socket = null;
1044
+ this.subscribedSessionIds.clear();
1016
1045
  }
1017
1046
  };
1018
1047
 
package/dist/cli.js CHANGED
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ installLifecycle
4
+ } from "./chunk-5KU3QS6A.js";
2
5
  import {
3
6
  ConveyorConnection
4
- } from "./chunk-XLDG5QEX.js";
7
+ } from "./chunk-EUCQ26F7.js";
5
8
 
6
9
  // src/cli.ts
7
10
  import { createRequire } from "module";
@@ -69,7 +72,7 @@ function registerProjectTools(server2, conn2) {
69
72
  // src/tools/connection.ts
70
73
  import { z as z4 } from "zod";
71
74
 
72
- // ../shared/dist/chunk-42BS7Y35.js
75
+ // ../shared/dist/chunk-VQSAISHG.js
73
76
  import { z as z2 } from "zod";
74
77
  var DEFAULT_SONNET_MODEL = "claude-sonnet-5";
75
78
  var DEFAULT_OPUS_MODEL = "claude-opus-5";
@@ -5497,11 +5500,5 @@ var server = new McpServer({
5497
5500
  registerAllTools(server, conn);
5498
5501
  var transport = new StdioServerTransport();
5499
5502
  await server.connect(transport);
5500
- process.on("SIGINT", () => {
5501
- conn.disconnect();
5502
- process.exit(0);
5503
- });
5504
- process.on("SIGTERM", () => {
5505
- conn.disconnect();
5506
- process.exit(0);
5507
- });
5503
+ var stop = installLifecycle(conn);
5504
+ server.server.onclose = () => stop();
@@ -1,3 +1,4 @@
1
+ import { io, Socket } from 'socket.io-client';
1
2
  import { ListDriveFilesResponse, ReadDriveFileResponse, DriveFileDTO, DeleteDriveFileResponse, ProjectIntegrationsSummary, GaAnalyticsSummaryDTO, ProjectChannelDTO, ReadChannelMessagesResponse, PostChannelMessageResponse, ListMeetingsResponse, MeetingForAgent, ReadMeetingTranscriptResponse, MeetingChecklistItemForAgent } from '@project/shared';
2
3
 
3
4
  interface ConveyorMcpConfig {
@@ -310,12 +311,20 @@ interface PrioritySummary {
310
311
  color: string;
311
312
  description?: string | null;
312
313
  }
314
+ /**
315
+ * Creates the underlying socket. Defaults to socket.io's `io`; tests inject a
316
+ * fake so the reconnect wiring in `connect()` can be driven without a server.
317
+ */
318
+ type ConveyorSocketFactory = (url: string, options: Parameters<typeof io>[1]) => Pick<Socket, "on" | "emit" | "close" | "connect" | "disconnect">;
313
319
  declare class ConveyorConnection {
314
320
  private socket;
315
321
  private config;
322
+ private socketFactory;
316
323
  /** project slug → id, for resolving `<project>/<card>` card paths. */
317
324
  private projectSlugIds;
318
- constructor(config: ConveyorMcpConfig);
325
+ /** Session rooms this connection has joined, replayed after every reconnect. */
326
+ private subscribedSessionIds;
327
+ constructor(config: ConveyorMcpConfig, socketFactory?: ConveyorSocketFactory);
319
328
  get projectId(): string;
320
329
  private resolveProjectId;
321
330
  /** The configured default board (CONVEYOR_SUBPROJECT_ID), if any. */
@@ -329,6 +338,16 @@ declare class ConveyorConnection {
329
338
  */
330
339
  private resolveSubProjectId;
331
340
  private normalizeProjectList;
341
+ /**
342
+ * Re-join every room this connection needs.
343
+ *
344
+ * Runs on the FIRST connect and on every reconnect. A socket.io reconnect
345
+ * starts a fresh server-side socket that belongs to no rooms, and Cloud Run
346
+ * cuts each websocket at its 60-minute request limit, so a subscribe sent
347
+ * only once left long-lived MCP connections silently outside the project room
348
+ * for the rest of the day.
349
+ */
350
+ private rejoinRooms;
332
351
  connect(): Promise<void>;
333
352
  private call;
334
353
  /**
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
- export { A as ActivePtySession, d as ConveyorConnection, e as ConveyorMcpConfig, P as PtyAttachSnapshot, c as PtyDataChunk } from './connection-B7CwszOV.js';
1
+ export { A as ActivePtySession, d as ConveyorConnection, e as ConveyorMcpConfig, P as PtyAttachSnapshot, c as PtyDataChunk } from './connection-jZ8nSRwk.js';
2
2
  export { AttachTunnelOptions, ResolvedPtySession, RunTunnelOptions, TunnelConnection, TunnelHandle, TunnelSession, TunnelTty, WaitForPtySessionOptions, attachTunnel, runTunnel, waitForPtySession } from './tunnel.js';
3
+ import 'socket.io-client';
3
4
  import '@project/shared';
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  } from "./chunk-HIVOGGE3.js";
6
6
  import {
7
7
  ConveyorConnection
8
- } from "./chunk-XLDG5QEX.js";
8
+ } from "./chunk-EUCQ26F7.js";
9
9
  export {
10
10
  ConveyorConnection,
11
11
  attachTunnel,
@@ -2,9 +2,12 @@
2
2
  import {
3
3
  runTunnel
4
4
  } from "./chunk-HIVOGGE3.js";
5
+ import {
6
+ watchForClientExit
7
+ } from "./chunk-5KU3QS6A.js";
5
8
  import {
6
9
  ConveyorConnection
7
- } from "./chunk-XLDG5QEX.js";
10
+ } from "./chunk-EUCQ26F7.js";
8
11
 
9
12
  // src/tunnel-cli.ts
10
13
  var HELP = `conveyor-tunnel \u2014 attach your local terminal to a cloud Claude Code session.
@@ -125,8 +128,7 @@ function cleanupAndExit(code) {
125
128
  conn.disconnect();
126
129
  process.exit(code);
127
130
  }
128
- process.on("SIGINT", () => cleanupAndExit(0));
129
- process.on("SIGTERM", () => cleanupAndExit(0));
131
+ watchForClientExit(() => cleanupAndExit(0), { watchStdin: false });
130
132
  try {
131
133
  await conn.connect();
132
134
  log("Connected to Conveyor API.");
package/dist/tunnel.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { A as ActivePtySession, P as PtyAttachSnapshot, c as PtyDataChunk } from './connection-B7CwszOV.js';
1
+ import { A as ActivePtySession, P as PtyAttachSnapshot, c as PtyDataChunk } from './connection-jZ8nSRwk.js';
2
+ import 'socket.io-client';
2
3
  import '@project/shared';
3
4
 
4
5
  /**
package/dist/wait-cli.js CHANGED
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ watchForClientExit
4
+ } from "./chunk-5KU3QS6A.js";
2
5
  import {
3
6
  ConveyorConnection
4
- } from "./chunk-XLDG5QEX.js";
7
+ } from "./chunk-EUCQ26F7.js";
5
8
  import {
6
9
  runWait
7
10
  } from "./chunk-OPZL4NDT.js";
@@ -257,8 +260,7 @@ function interrupt() {
257
260
  exiting = true;
258
261
  emitResult({ reason: "interrupted" }, () => shutdown(0));
259
262
  }
260
- process.on("SIGINT", interrupt);
261
- process.on("SIGTERM", interrupt);
263
+ watchForClientExit(interrupt, { resumeStdin: true });
262
264
  try {
263
265
  await conn.connect();
264
266
  } catch (err) {
@@ -1,5 +1,6 @@
1
- import { C as CardCollectionPage, a as CardCollectionDelta } from './connection-B7CwszOV.js';
1
+ import { C as CardCollectionPage, a as CardCollectionDelta } from './connection-jZ8nSRwk.js';
2
2
  import { WaitFilter, WaitResult } from './wait.js';
3
+ import 'socket.io-client';
3
4
  import '@project/shared';
4
5
 
5
6
  /**
package/dist/wait.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { b as CardCollectionItem } from './connection-B7CwszOV.js';
1
+ import { b as CardCollectionItem } from './connection-jZ8nSRwk.js';
2
+ import 'socket.io-client';
2
3
  import '@project/shared';
3
4
 
4
5
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rallycry/conveyor-mcp",
3
- "version": "5.0.3",
3
+ "version": "5.0.4",
4
4
  "description": "Conveyor MCP server for Claude Code PM integration",
5
5
  "keywords": [
6
6
  "claude",