@rallycry/conveyor-mcp 2.1.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.
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ConveyorConnection
4
+ } from "./chunk-UIDBFQNZ.js";
5
+ import {
6
+ runTunnel
7
+ } from "./chunk-N2XC2PGJ.js";
8
+
9
+ // src/tunnel-cli.ts
10
+ var HELP = `conveyor-tunnel \u2014 attach your local terminal to a cloud Claude Code session.
11
+
12
+ Usage:
13
+ conveyor-tunnel [title...] Create a card, build it, and attach.
14
+ conveyor-tunnel --task <taskId> Attach to an existing task's cloud session.
15
+
16
+ Options:
17
+ --task, --task-id <id> Attach to an existing task instead of creating one.
18
+ --title <text> Card title (defaults to positional args).
19
+ --description, -d <text> Card description (new card only).
20
+ --plan <text> Card plan (new card only).
21
+ -h, --help Show this help.
22
+
23
+ Environment:
24
+ CONVEYOR_API_URL Conveyor API base URL (required).
25
+ CONVEYOR_PROJECT_TOKEN Project runner token (required).
26
+ CONVEYOR_PROJECT_ID Project id (required).
27
+
28
+ While attached, press Ctrl-C to detach. The cloud session keeps running \u2014
29
+ detaching never stops the build.
30
+ `;
31
+ var VALUE_FLAGS = {
32
+ "--task": "taskId",
33
+ "--task-id": "taskId",
34
+ "--title": "title",
35
+ "--description": "description",
36
+ "-d": "description",
37
+ "--plan": "plan"
38
+ };
39
+ function parseArgs(argv) {
40
+ const out = {};
41
+ const positionals = [];
42
+ for (let i = 0; i < argv.length; i++) {
43
+ const arg = argv[i];
44
+ if (arg === "-h" || arg === "--help") {
45
+ out.help = true;
46
+ continue;
47
+ }
48
+ const eq = arg.indexOf("=");
49
+ if (arg.startsWith("--") && eq !== -1) {
50
+ const key = VALUE_FLAGS[arg.slice(0, eq)];
51
+ if (key) out[key] = arg.slice(eq + 1);
52
+ continue;
53
+ }
54
+ const valueKey = VALUE_FLAGS[arg];
55
+ if (valueKey) {
56
+ out[valueKey] = argv[++i];
57
+ continue;
58
+ }
59
+ if (!arg.startsWith("-")) positionals.push(arg);
60
+ }
61
+ if (!out.title && positionals.length > 0) out.title = positionals.join(" ");
62
+ return out;
63
+ }
64
+ var log = (msg) => {
65
+ process.stderr.write(`${msg}
66
+ `);
67
+ };
68
+ var args = parseArgs(process.argv.slice(2));
69
+ if (args.help) {
70
+ process.stderr.write(HELP);
71
+ process.exit(0);
72
+ }
73
+ var apiUrl = process.env.CONVEYOR_API_URL;
74
+ var projectToken = process.env.CONVEYOR_PROJECT_TOKEN;
75
+ var projectId = process.env.CONVEYOR_PROJECT_ID;
76
+ if (!apiUrl || !projectToken || !projectId) {
77
+ log(
78
+ "Error: CONVEYOR_API_URL, CONVEYOR_PROJECT_TOKEN, and CONVEYOR_PROJECT_ID environment variables are required."
79
+ );
80
+ process.exit(1);
81
+ }
82
+ if (!args.taskId && !args.title) {
83
+ log("Error: provide a card title or --task <taskId>. See --help.");
84
+ process.exit(1);
85
+ }
86
+ var conn = new ConveyorConnection({ apiUrl, projectToken, projectId });
87
+ var stdinIsTty = process.stdin.isTTY === true;
88
+ var tty = {
89
+ write(data) {
90
+ process.stdout.write(data);
91
+ },
92
+ onInput(handler) {
93
+ process.stdin.on("data", handler);
94
+ return () => {
95
+ process.stdin.off("data", handler);
96
+ };
97
+ },
98
+ onResize(handler) {
99
+ const fn = () => handler(process.stdout.columns ?? 80, process.stdout.rows ?? 24);
100
+ process.stdout.on("resize", fn);
101
+ return () => {
102
+ process.stdout.off("resize", fn);
103
+ };
104
+ },
105
+ size() {
106
+ return { cols: process.stdout.columns ?? 80, rows: process.stdout.rows ?? 24 };
107
+ }
108
+ };
109
+ var handle;
110
+ var cleanedUp = false;
111
+ function cleanupAndExit(code) {
112
+ if (cleanedUp) return;
113
+ cleanedUp = true;
114
+ try {
115
+ handle?.detach();
116
+ } catch {
117
+ }
118
+ if (stdinIsTty) {
119
+ try {
120
+ process.stdin.setRawMode(false);
121
+ } catch {
122
+ }
123
+ }
124
+ process.stdin.pause();
125
+ conn.disconnect();
126
+ process.exit(code);
127
+ }
128
+ process.on("SIGINT", () => cleanupAndExit(0));
129
+ process.on("SIGTERM", () => cleanupAndExit(0));
130
+ try {
131
+ await conn.connect();
132
+ log("Connected to Conveyor API.");
133
+ } catch (err) {
134
+ log(`Failed to connect to Conveyor API: ${err instanceof Error ? err.message : String(err)}`);
135
+ conn.disconnect();
136
+ process.exit(1);
137
+ }
138
+ try {
139
+ const session = await runTunnel(conn, tty, {
140
+ taskId: args.taskId,
141
+ title: args.title,
142
+ description: args.description,
143
+ plan: args.plan,
144
+ log,
145
+ onReady: () => {
146
+ if (stdinIsTty) {
147
+ process.stdin.setRawMode(true);
148
+ }
149
+ process.stdin.resume();
150
+ },
151
+ onDetachRequest: () => {
152
+ log("\nDetaching \u2014 the cloud session keeps running.");
153
+ cleanupAndExit(0);
154
+ }
155
+ });
156
+ handle = session.handle;
157
+ } catch (err) {
158
+ log(`Tunnel failed: ${err instanceof Error ? err.message : String(err)}`);
159
+ cleanupAndExit(1);
160
+ }
161
+ //# sourceMappingURL=tunnel-cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tunnel-cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { ConveyorConnection } from \"./connection.js\";\nimport { runTunnel, type TunnelHandle, type TunnelTty } from \"./tunnel.js\";\n\nconst HELP = `conveyor-tunnel — attach your local terminal to a cloud Claude Code session.\n\nUsage:\n conveyor-tunnel [title...] Create a card, build it, and attach.\n conveyor-tunnel --task <taskId> Attach to an existing task's cloud session.\n\nOptions:\n --task, --task-id <id> Attach to an existing task instead of creating one.\n --title <text> Card title (defaults to positional args).\n --description, -d <text> Card description (new card only).\n --plan <text> Card plan (new card only).\n -h, --help Show this help.\n\nEnvironment:\n CONVEYOR_API_URL Conveyor API base URL (required).\n CONVEYOR_PROJECT_TOKEN Project runner token (required).\n CONVEYOR_PROJECT_ID Project id (required).\n\nWhile attached, press Ctrl-C to detach. The cloud session keeps running —\ndetaching never stops the build.\n`;\n\ninterface ParsedArgs {\n taskId?: string;\n title?: string;\n description?: string;\n plan?: string;\n help?: boolean;\n}\n\ntype StringArgKey = \"taskId\" | \"title\" | \"description\" | \"plan\";\n\nconst VALUE_FLAGS: Record<string, StringArgKey> = {\n \"--task\": \"taskId\",\n \"--task-id\": \"taskId\",\n \"--title\": \"title\",\n \"--description\": \"description\",\n \"-d\": \"description\",\n \"--plan\": \"plan\",\n};\n\nfunction parseArgs(argv: string[]): ParsedArgs {\n const out: ParsedArgs = {};\n const positionals: string[] = [];\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n if (arg === \"-h\" || arg === \"--help\") {\n out.help = true;\n continue;\n }\n const eq = arg.indexOf(\"=\");\n if (arg.startsWith(\"--\") && eq !== -1) {\n const key = VALUE_FLAGS[arg.slice(0, eq)];\n if (key) out[key] = arg.slice(eq + 1);\n continue;\n }\n const valueKey = VALUE_FLAGS[arg];\n if (valueKey) {\n out[valueKey] = argv[++i];\n continue;\n }\n if (!arg.startsWith(\"-\")) positionals.push(arg);\n }\n if (!out.title && positionals.length > 0) out.title = positionals.join(\" \");\n return out;\n}\n\nconst log = (msg: string): void => {\n // stdout is reserved for raw PTY passthrough — all human-facing output is stderr.\n process.stderr.write(`${msg}\\n`);\n};\n\nconst args = parseArgs(process.argv.slice(2));\n\nif (args.help) {\n process.stderr.write(HELP);\n process.exit(0);\n}\n\nconst apiUrl = process.env.CONVEYOR_API_URL;\nconst projectToken = process.env.CONVEYOR_PROJECT_TOKEN;\nconst projectId = process.env.CONVEYOR_PROJECT_ID;\n\nif (!apiUrl || !projectToken || !projectId) {\n log(\n \"Error: CONVEYOR_API_URL, CONVEYOR_PROJECT_TOKEN, and CONVEYOR_PROJECT_ID environment variables are required.\",\n );\n process.exit(1);\n}\n\nif (!args.taskId && !args.title) {\n log(\"Error: provide a card title or --task <taskId>. See --help.\");\n process.exit(1);\n}\n\nconst conn = new ConveyorConnection({ apiUrl, projectToken, projectId });\n\nconst stdinIsTty = process.stdin.isTTY === true;\n\nconst tty: TunnelTty = {\n write(data) {\n process.stdout.write(data);\n },\n onInput(handler) {\n process.stdin.on(\"data\", handler);\n return () => {\n process.stdin.off(\"data\", handler);\n };\n },\n onResize(handler) {\n const fn = (): void => handler(process.stdout.columns ?? 80, process.stdout.rows ?? 24);\n process.stdout.on(\"resize\", fn);\n return () => {\n process.stdout.off(\"resize\", fn);\n };\n },\n size() {\n return { cols: process.stdout.columns ?? 80, rows: process.stdout.rows ?? 24 };\n },\n};\n\nlet handle: TunnelHandle | undefined;\nlet cleanedUp = false;\n\nfunction cleanupAndExit(code: number): void {\n if (cleanedUp) return;\n cleanedUp = true;\n // Detach the relay — this tears down listeners but, by design, never stops\n // the cloud build. The session continues running after we disconnect.\n try {\n handle?.detach();\n } catch {\n // best-effort teardown\n }\n if (stdinIsTty) {\n try {\n process.stdin.setRawMode(false);\n } catch {\n // best-effort restore\n }\n }\n process.stdin.pause();\n conn.disconnect();\n process.exit(code);\n}\n\n// Signal fallbacks for non-raw stdin (e.g. piped). In raw mode Ctrl-C arrives\n// as ETX in the input stream and is handled by onDetachRequest instead. Either\n// path only detaches — it must never stop the cloud session.\nprocess.on(\"SIGINT\", () => cleanupAndExit(0));\nprocess.on(\"SIGTERM\", () => cleanupAndExit(0));\n\ntry {\n await conn.connect();\n log(\"Connected to Conveyor API.\");\n} catch (err) {\n log(`Failed to connect to Conveyor API: ${err instanceof Error ? err.message : String(err)}`);\n conn.disconnect();\n process.exit(1);\n}\n\ntry {\n const session = await runTunnel(conn, tty, {\n taskId: args.taskId,\n title: args.title,\n description: args.description,\n plan: args.plan,\n log,\n onReady: () => {\n // Enter raw mode only now — before the PTY is ready, Ctrl-C must still\n // raise SIGINT so the user can abort the wait. In raw mode, Ctrl-C\n // instead arrives as ETX and is intercepted as a detach.\n if (stdinIsTty) {\n process.stdin.setRawMode(true);\n }\n process.stdin.resume();\n },\n onDetachRequest: () => {\n log(\"\\nDetaching — the cloud session keeps running.\");\n cleanupAndExit(0);\n },\n });\n handle = session.handle;\n} catch (err) {\n log(`Tunnel failed: ${err instanceof Error ? err.message : String(err)}`);\n cleanupAndExit(1);\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,322 @@
1
+ interface ConveyorMcpConfig {
2
+ apiUrl: string;
3
+ projectToken: string;
4
+ projectId: string;
5
+ }
6
+ /** A single PTY output frame broadcast on the `pty:data` room event. */
7
+ interface PtyDataChunk {
8
+ sessionId: string;
9
+ seq: number;
10
+ data: string;
11
+ cols?: number;
12
+ rows?: number;
13
+ }
14
+ /** Ring-buffer snapshot returned by `ptyAttach` for catch-up replay. */
15
+ interface PtyAttachSnapshot {
16
+ sessionId: string;
17
+ chunks: {
18
+ seq: number;
19
+ data: string;
20
+ }[];
21
+ cols: number;
22
+ rows: number;
23
+ totalBytes: number;
24
+ }
25
+ /**
26
+ * Result of `getActivePtySession`. `sessionId` is null until the cloud agent
27
+ * has booted a PTY and produced at least one ring frame (readiness signal).
28
+ */
29
+ interface ActivePtySession {
30
+ sessionId: string | null;
31
+ cols?: number;
32
+ rows?: number;
33
+ }
34
+ declare class ConveyorConnection {
35
+ private socket;
36
+ private config;
37
+ constructor(config: ConveyorMcpConfig);
38
+ get projectId(): string;
39
+ connect(): Promise<void>;
40
+ private call;
41
+ /**
42
+ * Fire-and-forget emit (no ack). The quickdraw-core server method handler
43
+ * invokes the ack callback via optional chaining, so omitting it still runs
44
+ * the full auth/schema/ACL pipeline — it just skips the response round-trip.
45
+ * Used for high-frequency PTY input/resize so each keystroke does not arm a
46
+ * 15s timeout timer.
47
+ */
48
+ private emit;
49
+ listTasks(params: {
50
+ status?: string;
51
+ assigneeId?: string;
52
+ limit?: number;
53
+ }): Promise<unknown[]>;
54
+ getTask(taskId: string): Promise<unknown>;
55
+ searchTasks(params: {
56
+ tagNames?: string[];
57
+ searchQuery?: string;
58
+ statusFilters?: string[];
59
+ limit?: number;
60
+ }): Promise<unknown[]>;
61
+ createTask(params: {
62
+ title: string;
63
+ description?: string;
64
+ plan?: string;
65
+ status?: string;
66
+ }): Promise<{
67
+ id: string;
68
+ slug: string;
69
+ }>;
70
+ updateTask(params: {
71
+ taskId: string;
72
+ title?: string;
73
+ description?: string;
74
+ plan?: string;
75
+ status?: string;
76
+ assignedUserId?: string | null;
77
+ }): Promise<{
78
+ id: string;
79
+ status: string;
80
+ }>;
81
+ startBuild(taskId: string): Promise<{
82
+ taskId: string;
83
+ status: string;
84
+ }>;
85
+ stopBuild(taskId: string): Promise<{
86
+ taskId: string;
87
+ stopped: boolean;
88
+ }>;
89
+ getBuildStatus(taskId: string): Promise<{
90
+ session: {
91
+ status: string | null;
92
+ agentRunnerStatus: string | null;
93
+ } | null;
94
+ }>;
95
+ getTaskChat(taskId: string, limit?: number): Promise<unknown[]>;
96
+ postToTaskChat(taskId: string, content: string): Promise<{
97
+ messageId: string;
98
+ }>;
99
+ getTaskCli(taskId: string, limit?: number, source?: string): Promise<{
100
+ type: string;
101
+ data: Record<string, unknown>;
102
+ timestamp: string;
103
+ }[]>;
104
+ listTags(): Promise<{
105
+ id: string;
106
+ name: string;
107
+ color: string;
108
+ }[]>;
109
+ getProjectSummary(): Promise<unknown>;
110
+ approveTask(taskId: string): Promise<{
111
+ status: string;
112
+ }>;
113
+ requestChanges(taskId: string, feedback: string): Promise<void>;
114
+ approveAndMergePR(childTaskId: string): Promise<{
115
+ merged: boolean;
116
+ childTaskId: string;
117
+ prNumber: number;
118
+ }>;
119
+ listTaskFiles(taskId: string): Promise<unknown[]>;
120
+ getAttachment(taskId: string, fileId: string): Promise<unknown>;
121
+ createPullRequest(params: {
122
+ taskId: string;
123
+ title: string;
124
+ body: string;
125
+ head?: string;
126
+ base?: string;
127
+ }): Promise<{
128
+ prNumber: number;
129
+ prUrl: string;
130
+ }>;
131
+ createSubtask(params: {
132
+ parentTaskId: string;
133
+ title: string;
134
+ description?: string;
135
+ plan?: string;
136
+ ordinal?: number;
137
+ storyPointValue?: number;
138
+ }): Promise<{
139
+ id: string;
140
+ slug: string;
141
+ }>;
142
+ updateSubtask(params: {
143
+ subtaskId: string;
144
+ title?: string;
145
+ description?: string;
146
+ plan?: string;
147
+ status?: string;
148
+ ordinal?: number;
149
+ storyPointValue?: number;
150
+ }): Promise<{
151
+ id: string;
152
+ status: string;
153
+ }>;
154
+ listSubtasks(taskId: string): Promise<unknown[]>;
155
+ deleteSubtask(subtaskId: string): Promise<{
156
+ deleted: boolean;
157
+ }>;
158
+ getDependencies(taskId: string): Promise<unknown[]>;
159
+ addDependency(params: {
160
+ taskId: string;
161
+ dependsOnSlugOrId: string;
162
+ }): Promise<{
163
+ success: boolean;
164
+ }>;
165
+ removeDependency(params: {
166
+ taskId: string;
167
+ dependsOnSlugOrId: string;
168
+ }): Promise<{
169
+ success: boolean;
170
+ }>;
171
+ createSuggestion(params: {
172
+ title: string;
173
+ description?: string;
174
+ tagNames?: string[];
175
+ }): Promise<{
176
+ id: string;
177
+ merged: boolean;
178
+ mergedIntoId?: string;
179
+ }>;
180
+ /**
181
+ * Poll target: returns the active cloud PTY session for a task once its ring
182
+ * buffer has frames. `sessionId` is resolved server-side from `taskId` (never
183
+ * accepted from the wire), preserving the one-active-session-per-task invariant.
184
+ */
185
+ getActivePtySession(taskId: string): Promise<ActivePtySession>;
186
+ /** Fetch the ring-buffer snapshot for catch-up replay on (re)attach. */
187
+ ptyAttach(sessionId: string): Promise<PtyAttachSnapshot>;
188
+ /**
189
+ * Join the session room so `pty:data` frames are delivered. Uses the standard
190
+ * quickdraw-core subscribe envelope; "Read" is sufficient for output streaming.
191
+ */
192
+ subscribeToSession(sessionId: string): void;
193
+ /** Relay a stdin chunk to the cloud PTY (raw utf8, fire-and-forget). */
194
+ ptyInput(sessionId: string, data: string): void;
195
+ /** Relay a terminal resize to the cloud PTY (fire-and-forget). */
196
+ ptyResize(sessionId: string, cols: number, rows: number): void;
197
+ /** Subscribe to raw PTY output frames. Returns an unsubscribe function. */
198
+ onPtyData(handler: (chunk: PtyDataChunk) => void): () => void;
199
+ disconnect(): void;
200
+ }
201
+
202
+ /**
203
+ * The slice of {@link ConveyorConnection} the tunnel needs. Defined as a
204
+ * structural interface so tests can inject a mock — and, deliberately, so the
205
+ * relay has NO reference to `stopBuild`. Detaching the local terminal must
206
+ * never stop the cloud session, and the cleanest way to guarantee that is to
207
+ * make stopping unreachable from this layer.
208
+ */
209
+ interface TunnelConnection {
210
+ createTask(params: {
211
+ title: string;
212
+ description?: string;
213
+ plan?: string;
214
+ status?: string;
215
+ }): Promise<{
216
+ id: string;
217
+ slug: string;
218
+ }>;
219
+ startBuild(taskId: string): Promise<{
220
+ taskId: string;
221
+ status: string;
222
+ }>;
223
+ getActivePtySession(taskId: string): Promise<ActivePtySession>;
224
+ ptyAttach(sessionId: string): Promise<PtyAttachSnapshot>;
225
+ subscribeToSession(sessionId: string): void;
226
+ ptyInput(sessionId: string, data: string): void;
227
+ ptyResize(sessionId: string, cols: number, rows: number): void;
228
+ onPtyData(handler: (chunk: PtyDataChunk) => void): () => void;
229
+ }
230
+ /**
231
+ * Local terminal abstraction. The CLI binds this to the real process TTY; tests
232
+ * bind it to an in-memory fake. All output is raw utf8 (node-pty does not use
233
+ * base64), matching the S2 relay encoding.
234
+ */
235
+ interface TunnelTty {
236
+ /** Write raw PTY output to the local terminal. */
237
+ write(data: string): void;
238
+ /** Register a stdin handler. Returns an unsubscribe function. */
239
+ onInput(handler: (data: Buffer) => void): () => void;
240
+ /** Register a resize handler. Returns an unsubscribe function. */
241
+ onResize(handler: (cols: number, rows: number) => void): () => void;
242
+ /** Current local terminal dimensions. */
243
+ size(): {
244
+ cols: number;
245
+ rows: number;
246
+ };
247
+ }
248
+ /** A live tunnel attachment. */
249
+ interface TunnelHandle {
250
+ sessionId: string;
251
+ /**
252
+ * Tear down the local relay (stdin/resize/output listeners). Does NOT stop
253
+ * the cloud build — the session keeps running after detach. Idempotent.
254
+ */
255
+ detach(): void;
256
+ }
257
+ /** Resolved (non-null) active PTY session. */
258
+ interface ResolvedPtySession {
259
+ sessionId: string;
260
+ cols?: number;
261
+ rows?: number;
262
+ }
263
+ interface WaitForPtySessionOptions {
264
+ intervalMs?: number;
265
+ timeoutMs?: number;
266
+ log?: (msg: string) => void;
267
+ /** Injectable for tests; defaults to a real timer. */
268
+ sleep?: (ms: number) => Promise<void>;
269
+ }
270
+ /**
271
+ * Poll {@link TunnelConnection.getActivePtySession} until the cloud agent has
272
+ * booted its PTY and produced at least one ring frame. `sessionId` is resolved
273
+ * server-side from `taskId`, so the local side never invents or supplies one.
274
+ */
275
+ declare function waitForPtySession(conn: TunnelConnection, taskId: string, options?: WaitForPtySessionOptions): Promise<ResolvedPtySession>;
276
+ interface AttachTunnelOptions {
277
+ /**
278
+ * Called when the local user requests a detach (Ctrl-C / ETX in the input
279
+ * stream). When provided, the ETX byte is intercepted and NOT forwarded to
280
+ * the cloud PTY; when omitted, all input passes through unmodified.
281
+ */
282
+ onDetachRequest?: () => void;
283
+ }
284
+ /**
285
+ * Attach the local TTY to a cloud PTY session, reusing the S2 `pty:*` relay
286
+ * envelope. Replays the ring snapshot then streams live frames, with the same
287
+ * seq-based dedup the web `PtyTerminal` uses so no bytes are dropped or doubled
288
+ * across the attach boundary.
289
+ */
290
+ declare function attachTunnel(conn: TunnelConnection, session: ResolvedPtySession, tty: TunnelTty, options?: AttachTunnelOptions): Promise<TunnelHandle>;
291
+ interface RunTunnelOptions {
292
+ /** Reuse an existing task instead of creating a new card. */
293
+ taskId?: string;
294
+ /** Card fields (used only when `taskId` is not supplied). */
295
+ title?: string;
296
+ description?: string;
297
+ plan?: string;
298
+ pollIntervalMs?: number;
299
+ pollTimeoutMs?: number;
300
+ log?: (msg: string) => void;
301
+ sleep?: (ms: number) => Promise<void>;
302
+ onDetachRequest?: () => void;
303
+ /**
304
+ * Invoked once the PTY session is ready, immediately before attaching. The
305
+ * CLI uses this to enable raw TTY mode only at the last moment — before this
306
+ * point Ctrl-C should still raise SIGINT so the user can abort the wait.
307
+ */
308
+ onReady?: (session: ResolvedPtySession) => void;
309
+ }
310
+ interface TunnelSession {
311
+ taskId: string;
312
+ sessionId: string;
313
+ handle: TunnelHandle;
314
+ }
315
+ /**
316
+ * End-to-end flow: (optionally) create a cloud card, start the build, wait for
317
+ * the cloud PTY to come up, then attach the local terminal. Returns once the
318
+ * relay is live; the caller keeps the process alive and calls `handle.detach()`.
319
+ */
320
+ declare function runTunnel(conn: TunnelConnection, tty: TunnelTty, options?: RunTunnelOptions): Promise<TunnelSession>;
321
+
322
+ export { type ActivePtySession as A, type AttachTunnelOptions, ConveyorConnection as C, type PtyAttachSnapshot as P, type ResolvedPtySession, type RunTunnelOptions, type TunnelConnection, type TunnelHandle, type TunnelSession, type TunnelTty, type WaitForPtySessionOptions, type ConveyorMcpConfig as a, attachTunnel, type PtyDataChunk as b, runTunnel, waitForPtySession };
package/dist/tunnel.js ADDED
@@ -0,0 +1,11 @@
1
+ import {
2
+ attachTunnel,
3
+ runTunnel,
4
+ waitForPtySession
5
+ } from "./chunk-N2XC2PGJ.js";
6
+ export {
7
+ attachTunnel,
8
+ runTunnel,
9
+ waitForPtySession
10
+ };
11
+ //# sourceMappingURL=tunnel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@rallycry/conveyor-mcp",
3
+ "version": "2.1.1",
4
+ "description": "Conveyor MCP server for Claude Code PM integration",
5
+ "keywords": [
6
+ "claude",
7
+ "conveyor",
8
+ "mcp",
9
+ "project-management"
10
+ ],
11
+ "license": "MIT",
12
+ "bin": {
13
+ "conveyor-mcp": "dist/cli.js",
14
+ "conveyor-tunnel": "dist/tunnel-cli.js"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "type": "module",
20
+ "main": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "scripts": {
26
+ "prepublishOnly": "tsup",
27
+ "build": "tsup",
28
+ "build:js": "tsup --no-dts --no-clean",
29
+ "dev": "tsup --watch",
30
+ "lint": "oxlint -c ../../.oxlintrc.json src",
31
+ "lint:fix": "oxlint -c ../../.oxlintrc.json --fix src",
32
+ "typecheck": "tsgo --noEmit",
33
+ "test": "vitest run --passWithNoTests",
34
+ "test:unit": "vitest run --passWithNoTests"
35
+ },
36
+ "dependencies": {
37
+ "@modelcontextprotocol/sdk": "^1.12.1",
38
+ "socket.io-client": "^4.7.4",
39
+ "zod": "^3.25.76"
40
+ },
41
+ "devDependencies": {
42
+ "tsup": "^8.0.0",
43
+ "typescript": "^5.3.0",
44
+ "vitest": "^4.0.17"
45
+ }
46
+ }