@rallycry/conveyor-mcp 4.3.28 → 4.3.30
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/chunk-D3Q2QZJA.js +162 -0
- package/dist/{chunk-N2XC2PGJ.js → chunk-HIVOGGE3.js} +0 -1
- package/dist/chunk-OPZL4NDT.js +175 -0
- package/dist/{chunk-Y6ZJUNDX.js → chunk-X3EHPZNH.js} +92 -2
- package/dist/cli.js +87 -11
- package/dist/connection-CRkBLz5w.d.ts +911 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -5
- package/dist/tunnel-cli.js +4 -5
- package/dist/tunnel.d.ts +2 -803
- package/dist/tunnel.js +1 -2
- package/dist/wait-cli.d.ts +1 -0
- package/dist/wait-cli.js +313 -0
- package/dist/wait-runner.d.ts +48 -0
- package/dist/wait-runner.js +7 -0
- package/dist/wait.d.ts +108 -0
- package/dist/wait.js +28 -0
- package/package.json +3 -2
- package/dist/chunk-N2XC2PGJ.js.map +0 -1
- package/dist/chunk-Y6ZJUNDX.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/tunnel-cli.js.map +0 -1
- package/dist/tunnel.js.map +0 -1
package/dist/tunnel.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/wait-cli.js
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ConveyorConnection
|
|
4
|
+
} from "./chunk-X3EHPZNH.js";
|
|
5
|
+
import {
|
|
6
|
+
runWait
|
|
7
|
+
} from "./chunk-OPZL4NDT.js";
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_WAIT_SCOPES,
|
|
10
|
+
DEFAULT_WAIT_STATUSES,
|
|
11
|
+
DEFAULT_WAIT_TYPES,
|
|
12
|
+
parseWaitScopes,
|
|
13
|
+
parseWaitStatuses,
|
|
14
|
+
parseWaitTypes
|
|
15
|
+
} from "./chunk-D3Q2QZJA.js";
|
|
16
|
+
|
|
17
|
+
// src/wait-cli.ts
|
|
18
|
+
import { readFileSync } from "fs";
|
|
19
|
+
import { homedir } from "os";
|
|
20
|
+
import { sep } from "path";
|
|
21
|
+
|
|
22
|
+
// src/wait-config.ts
|
|
23
|
+
var TOKEN_KEYS = ["CONVEYOR_USER_TOKEN", "CONVEYOR_PROJECT_TOKEN"];
|
|
24
|
+
var MAX_ANCESTORS = 24;
|
|
25
|
+
function parseJson(raw) {
|
|
26
|
+
if (raw === null) return null;
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(raw);
|
|
29
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
30
|
+
return parsed;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function asRecord(value) {
|
|
36
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
function extractConveyorEnv(config) {
|
|
40
|
+
const root = asRecord(config);
|
|
41
|
+
const servers = asRecord(root?.mcpServers);
|
|
42
|
+
if (!servers) return null;
|
|
43
|
+
for (const [name, raw] of Object.entries(servers)) {
|
|
44
|
+
const server = asRecord(raw);
|
|
45
|
+
if (!server) continue;
|
|
46
|
+
const args2 = Array.isArray(server.args) ? server.args.filter((a) => typeof a === "string") : [];
|
|
47
|
+
const haystack = [name, String(server.command ?? ""), ...args2].join(" ");
|
|
48
|
+
if (!/conveyor/i.test(haystack)) continue;
|
|
49
|
+
const env = asRecord(server.env);
|
|
50
|
+
if (!env) continue;
|
|
51
|
+
const out = {};
|
|
52
|
+
for (const [key, value] of Object.entries(env)) {
|
|
53
|
+
if (typeof value === "string") out[key] = value;
|
|
54
|
+
}
|
|
55
|
+
if (Object.keys(out).length > 0) return out;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
function ancestorDirs(cwd, sep2) {
|
|
60
|
+
const out = [];
|
|
61
|
+
let current = cwd;
|
|
62
|
+
for (let i = 0; i < MAX_ANCESTORS; i++) {
|
|
63
|
+
out.push(current);
|
|
64
|
+
const cut = current.lastIndexOf(sep2);
|
|
65
|
+
if (cut < 0) break;
|
|
66
|
+
const parent = cut === 0 ? sep2 : current.slice(0, cut);
|
|
67
|
+
if (parent === current) break;
|
|
68
|
+
current = parent;
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
function fill(credentials2, env, source) {
|
|
73
|
+
const assign = (field, key) => {
|
|
74
|
+
if (credentials2[field] !== void 0) return;
|
|
75
|
+
const value = env[key];
|
|
76
|
+
if (value) {
|
|
77
|
+
credentials2[field] = value;
|
|
78
|
+
credentials2.sources[field] = source;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
assign("apiUrl", "CONVEYOR_API_URL");
|
|
82
|
+
assign("projectId", "CONVEYOR_PROJECT_ID");
|
|
83
|
+
assign("subProjectId", "CONVEYOR_SUBPROJECT_ID");
|
|
84
|
+
if (credentials2.token === void 0) {
|
|
85
|
+
for (const key of TOKEN_KEYS) {
|
|
86
|
+
const value = env[key];
|
|
87
|
+
if (value) {
|
|
88
|
+
credentials2.token = value;
|
|
89
|
+
credentials2.sources.token = `${source} (${key})`;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function resolveWaitCredentials(deps) {
|
|
96
|
+
const sep2 = deps.sep ?? "/";
|
|
97
|
+
const credentials2 = { sources: {} };
|
|
98
|
+
fill(credentials2, deps.env, "environment");
|
|
99
|
+
const dirs = ancestorDirs(deps.cwd, sep2);
|
|
100
|
+
for (const dir of dirs) {
|
|
101
|
+
const path = dir.endsWith(sep2) ? `${dir}.mcp.json` : `${dir}${sep2}.mcp.json`;
|
|
102
|
+
const env = extractConveyorEnv(parseJson(deps.readFile(path)));
|
|
103
|
+
if (env) fill(credentials2, env, path);
|
|
104
|
+
}
|
|
105
|
+
const claudeConfig = parseJson(
|
|
106
|
+
deps.readFile(
|
|
107
|
+
deps.homeDir.endsWith(sep2) ? `${deps.homeDir}.claude.json` : `${deps.homeDir}${sep2}.claude.json`
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
const projects = asRecord(claudeConfig?.projects);
|
|
111
|
+
if (projects) {
|
|
112
|
+
for (const dir of dirs) {
|
|
113
|
+
const env = extractConveyorEnv(projects[dir]);
|
|
114
|
+
if (env) fill(credentials2, env, `~/.claude.json (${dir})`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return credentials2;
|
|
118
|
+
}
|
|
119
|
+
function describeMissingCredentials(credentials2) {
|
|
120
|
+
const missing = [];
|
|
121
|
+
if (!credentials2.apiUrl) missing.push("CONVEYOR_API_URL");
|
|
122
|
+
if (!credentials2.token) missing.push("CONVEYOR_USER_TOKEN (or legacy CONVEYOR_PROJECT_TOKEN)");
|
|
123
|
+
return `conveyor-wait found no Conveyor credentials. Missing: ${missing.join(", ")}.
|
|
124
|
+
It looked at the environment, every .mcp.json from the working directory up, and ~/.claude.json.
|
|
125
|
+
Fix it by exporting the values, or by adding a "conveyor" entry to .mcp.json. Mint a token in the web app under User Settings \u2192 Connect your coding agent.`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/wait-cli.ts
|
|
129
|
+
var HELP = `conveyor-wait \u2014 block until a card becomes actionable on your board.
|
|
130
|
+
|
|
131
|
+
Exits as soon as a card ENTERS the filtered state, or at the timeout. Run it in
|
|
132
|
+
the background and let its completion be your wake signal instead of polling.
|
|
133
|
+
|
|
134
|
+
Usage:
|
|
135
|
+
conveyor-wait [options]
|
|
136
|
+
|
|
137
|
+
Options:
|
|
138
|
+
--types <list> Card kinds to watch: task, incident, suggestion, chat, all.
|
|
139
|
+
Default: ${DEFAULT_WAIT_TYPES.join(",")}
|
|
140
|
+
--scope <list> Assignment scopes: mine, unclaimed, all.
|
|
141
|
+
Default: ${DEFAULT_WAIT_SCOPES.join(",")}
|
|
142
|
+
--statuses <list> Statuses to watch. Default: ${DEFAULT_WAIT_STATUSES.join(",")}
|
|
143
|
+
--timeout <secs> Give up and exit 0 after this long. Default: 1740 (29m).
|
|
144
|
+
--project <id> Project to watch. Defaults to the configured project.
|
|
145
|
+
-h, --help Show this help.
|
|
146
|
+
|
|
147
|
+
Output:
|
|
148
|
+
One line of JSON on stdout, always. Human-facing progress goes to stderr.
|
|
149
|
+
{"reason":"event","card":{"id":\u2026,"slug":\u2026,"title":\u2026,"type":\u2026,"status":\u2026,"assignedUserId":\u2026}}
|
|
150
|
+
{"reason":"timeout"}
|
|
151
|
+
{"reason":"interrupted"}
|
|
152
|
+
Exit 0 in all three cases \u2014 only a configuration or auth failure exits 1.
|
|
153
|
+
|
|
154
|
+
Credentials:
|
|
155
|
+
Read from CONVEYOR_API_URL, CONVEYOR_USER_TOKEN (or legacy
|
|
156
|
+
CONVEYOR_PROJECT_TOKEN), and CONVEYOR_PROJECT_ID. When those are unset, the
|
|
157
|
+
CLI falls back to any .mcp.json from the working directory up, then to
|
|
158
|
+
~/.claude.json \u2014 so it works from an agent shell that never saw the MCP
|
|
159
|
+
server's environment.
|
|
160
|
+
|
|
161
|
+
The card in the trigger is advisory. Re-read the board before acting on it: by
|
|
162
|
+
the time you wake, someone else may already have claimed it.
|
|
163
|
+
`;
|
|
164
|
+
var DEFAULT_TIMEOUT_SECONDS = 1740;
|
|
165
|
+
var MAX_TIMEOUT_MS = 2147483647;
|
|
166
|
+
var VALUE_FLAGS = {
|
|
167
|
+
"--types": "types",
|
|
168
|
+
"--type": "types",
|
|
169
|
+
"--scope": "scope",
|
|
170
|
+
"--scopes": "scope",
|
|
171
|
+
"--statuses": "statuses",
|
|
172
|
+
"--status": "statuses",
|
|
173
|
+
"--timeout": "timeout",
|
|
174
|
+
"--project": "project",
|
|
175
|
+
"--project-id": "project"
|
|
176
|
+
};
|
|
177
|
+
function parseArgs(argv) {
|
|
178
|
+
const out = {};
|
|
179
|
+
for (let i = 0; i < argv.length; i++) {
|
|
180
|
+
const arg = argv[i];
|
|
181
|
+
if (arg === "-h" || arg === "--help") {
|
|
182
|
+
out.help = true;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const eq = arg.indexOf("=");
|
|
186
|
+
if (arg.startsWith("--") && eq !== -1) {
|
|
187
|
+
const key = VALUE_FLAGS[arg.slice(0, eq)];
|
|
188
|
+
if (key) out[key] = arg.slice(eq + 1);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const valueKey = VALUE_FLAGS[arg];
|
|
192
|
+
if (valueKey) out[valueKey] = argv[++i];
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
var log = (message) => {
|
|
197
|
+
process.stderr.write(`${message}
|
|
198
|
+
`);
|
|
199
|
+
};
|
|
200
|
+
function emitResult(result, done) {
|
|
201
|
+
process.stdout.write(`${JSON.stringify(result)}
|
|
202
|
+
`, () => done());
|
|
203
|
+
}
|
|
204
|
+
var args = parseArgs(process.argv.slice(2));
|
|
205
|
+
if (args.help) {
|
|
206
|
+
process.stderr.write(HELP);
|
|
207
|
+
process.exit(0);
|
|
208
|
+
}
|
|
209
|
+
var filter;
|
|
210
|
+
var timeoutMs;
|
|
211
|
+
try {
|
|
212
|
+
filter = {
|
|
213
|
+
types: parseWaitTypes(args.types),
|
|
214
|
+
scopes: parseWaitScopes(args.scope),
|
|
215
|
+
statuses: parseWaitStatuses(args.statuses)
|
|
216
|
+
};
|
|
217
|
+
const seconds = args.timeout === void 0 ? DEFAULT_TIMEOUT_SECONDS : Number(args.timeout);
|
|
218
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
219
|
+
throw new Error(`--timeout must be a positive number of seconds, got "${args.timeout}".`);
|
|
220
|
+
}
|
|
221
|
+
timeoutMs = Math.min(Math.round(seconds * 1e3), MAX_TIMEOUT_MS);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
log(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
var credentials = resolveWaitCredentials({
|
|
227
|
+
env: process.env,
|
|
228
|
+
cwd: process.cwd(),
|
|
229
|
+
homeDir: homedir(),
|
|
230
|
+
sep,
|
|
231
|
+
readFile: (path) => {
|
|
232
|
+
try {
|
|
233
|
+
return readFileSync(path, "utf8");
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
if (!credentials.apiUrl || !credentials.token) {
|
|
240
|
+
log(describeMissingCredentials(credentials));
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
var conn = new ConveyorConnection({
|
|
244
|
+
apiUrl: credentials.apiUrl,
|
|
245
|
+
projectToken: credentials.token,
|
|
246
|
+
projectId: args.project ?? credentials.projectId,
|
|
247
|
+
subProjectId: credentials.subProjectId
|
|
248
|
+
});
|
|
249
|
+
var exiting = false;
|
|
250
|
+
function shutdown(code) {
|
|
251
|
+
exiting = true;
|
|
252
|
+
conn.disconnect();
|
|
253
|
+
process.exit(code);
|
|
254
|
+
}
|
|
255
|
+
function interrupt() {
|
|
256
|
+
if (exiting) return;
|
|
257
|
+
exiting = true;
|
|
258
|
+
emitResult({ reason: "interrupted" }, () => shutdown(0));
|
|
259
|
+
}
|
|
260
|
+
process.on("SIGINT", interrupt);
|
|
261
|
+
process.on("SIGTERM", interrupt);
|
|
262
|
+
try {
|
|
263
|
+
await conn.connect();
|
|
264
|
+
} catch (err) {
|
|
265
|
+
log(`Failed to connect to Conveyor: ${err instanceof Error ? err.message : String(err)}`);
|
|
266
|
+
shutdown(1);
|
|
267
|
+
}
|
|
268
|
+
var projectId = args.project ?? credentials.projectId;
|
|
269
|
+
if (!projectId) {
|
|
270
|
+
try {
|
|
271
|
+
const projects = await conn.listProjects();
|
|
272
|
+
const ids = projects.map((project) => project.id).filter((id) => typeof id === "string");
|
|
273
|
+
if (ids.length === 1) {
|
|
274
|
+
projectId = ids[0];
|
|
275
|
+
log(`Watching the only accessible project: ${projectId}`);
|
|
276
|
+
}
|
|
277
|
+
} catch {
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (!projectId) {
|
|
281
|
+
log(
|
|
282
|
+
"Error: no project to watch. Set CONVEYOR_PROJECT_ID or pass --project <id>. Your token can reach more than one project, so conveyor-wait cannot pick for you."
|
|
283
|
+
);
|
|
284
|
+
shutdown(1);
|
|
285
|
+
}
|
|
286
|
+
var userId = null;
|
|
287
|
+
try {
|
|
288
|
+
const context = await conn.getConnectionContext(projectId);
|
|
289
|
+
userId = context.account.userId;
|
|
290
|
+
} catch (err) {
|
|
291
|
+
if (filter.scopes.includes("mine")) {
|
|
292
|
+
log(
|
|
293
|
+
`Failed to resolve the connected account, which --scope mine needs: ${err instanceof Error ? err.message : String(err)}`
|
|
294
|
+
);
|
|
295
|
+
shutdown(1);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
log(
|
|
299
|
+
`Waiting up to ${Math.round(timeoutMs / 1e3)}s for a ${filter.statuses.join("/")} ${filter.types.join("/")} card in scope ${filter.scopes.join("+")}.`
|
|
300
|
+
);
|
|
301
|
+
try {
|
|
302
|
+
const result = await runWait({
|
|
303
|
+
source: conn,
|
|
304
|
+
projectId,
|
|
305
|
+
filter: { ...filter, userId },
|
|
306
|
+
timeoutMs,
|
|
307
|
+
log
|
|
308
|
+
});
|
|
309
|
+
emitResult(result, () => shutdown(0));
|
|
310
|
+
} catch (err) {
|
|
311
|
+
log(`Wait failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
312
|
+
shutdown(1);
|
|
313
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { C as CardCollectionPage, a as CardCollectionDelta } from './connection-CRkBLz5w.js';
|
|
2
|
+
import { WaitFilter, WaitResult } from './wait.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `conveyor-wait` runner — the live half of the engine.
|
|
6
|
+
*
|
|
7
|
+
* `wait.ts` decides what "matching" means; this module owns the socket-shaped
|
|
8
|
+
* lifecycle around it: seed from a paged snapshot, apply deltas, re-snapshot
|
|
9
|
+
* after a `reset` or a reconnect, and settle on the first card that enters the
|
|
10
|
+
* filter (or on the deadline).
|
|
11
|
+
*
|
|
12
|
+
* It talks to a narrow `CardCollectionSource` rather than a socket, so the
|
|
13
|
+
* reconnect, reset, and paging paths are unit-testable without a server.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The slice of `ConveyorConnection` the engine needs. `ConveyorConnection`
|
|
18
|
+
* satisfies it structurally; tests pass a fake.
|
|
19
|
+
*/
|
|
20
|
+
interface CardCollectionSource {
|
|
21
|
+
subscribeToCardCollection(projectId: string, opts?: {
|
|
22
|
+
cursor?: string | null;
|
|
23
|
+
limit?: number;
|
|
24
|
+
}): Promise<CardCollectionPage>;
|
|
25
|
+
onCardDelta(projectId: string, handler: (delta: CardCollectionDelta) => void): () => void;
|
|
26
|
+
onReconnect(handler: () => void): () => void;
|
|
27
|
+
}
|
|
28
|
+
interface RunWaitOptions {
|
|
29
|
+
source: CardCollectionSource;
|
|
30
|
+
projectId: string;
|
|
31
|
+
filter: WaitFilter;
|
|
32
|
+
timeoutMs: number;
|
|
33
|
+
pageLimit?: number;
|
|
34
|
+
maxPages?: number;
|
|
35
|
+
/** Backoff between re-snapshot attempts. Overridable so tests stay fast. */
|
|
36
|
+
resyncRetryDelayMs?: number;
|
|
37
|
+
/** Human-facing progress, written to stderr by the CLI. */
|
|
38
|
+
log?: (message: string) => void;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve with the first card that enters matching state, or with
|
|
42
|
+
* `{ reason: "timeout" }` at the deadline. Rejects only when the snapshot
|
|
43
|
+
* itself fails (bad project id, revoked access) — a caller that gets a
|
|
44
|
+
* rejection should exit non-zero rather than keep waiting.
|
|
45
|
+
*/
|
|
46
|
+
declare function runWait(options: RunWaitOptions): Promise<WaitResult>;
|
|
47
|
+
|
|
48
|
+
export { type CardCollectionSource, type RunWaitOptions, runWait };
|
package/dist/wait.d.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { b as CardCollectionItem } from './connection-CRkBLz5w.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `conveyor-wait` engine — block until a card becomes actionable on a board.
|
|
5
|
+
*
|
|
6
|
+
* The API already fans every card write out as a quickdraw collection delta on
|
|
7
|
+
* `taskService`'s `cardsByProject` scope. This module subscribes to that scope,
|
|
8
|
+
* holds the set of card ids that ALREADY match the caller's filter, and reports
|
|
9
|
+
* the first card that *enters* matching state.
|
|
10
|
+
*
|
|
11
|
+
* Two wire facts drive the design and are easy to get wrong:
|
|
12
|
+
* - **Creates arrive as `updated` deltas.** Most card writes go through the
|
|
13
|
+
* service's hand-rolled upsert choke point, which always emits `updated`.
|
|
14
|
+
* "New work" is therefore a set diff, never a delta-type check.
|
|
15
|
+
* - **The snapshot ack is one PAGE, not the scope.** `cardsByProject` omits
|
|
16
|
+
* `ids` (the scope is unbounded), so seeding walks `nextCursor` to the end.
|
|
17
|
+
* When the walk is capped we merge instead of replacing, because dropping an
|
|
18
|
+
* unseen id would make it look newly-matching on the next delta.
|
|
19
|
+
*
|
|
20
|
+
* The live half — snapshot paging, reconnect, and settling — lives in
|
|
21
|
+
* `wait-runner.ts`; this module is the pure filter and set-diff logic it uses.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Card kinds `--types` accepts. Mirrors `CardType` in @project/shared. */
|
|
25
|
+
declare const WAIT_CARD_TYPES: readonly ["task", "incident", "suggestion", "chat"];
|
|
26
|
+
type WaitCardType = (typeof WAIT_CARD_TYPES)[number];
|
|
27
|
+
/** Assignment scopes `--scope` accepts. `all` disables the assignee filter. */
|
|
28
|
+
declare const WAIT_SCOPES: readonly ["mine", "unclaimed", "all"];
|
|
29
|
+
type WaitScope = (typeof WAIT_SCOPES)[number];
|
|
30
|
+
/**
|
|
31
|
+
* Canonical card statuses. Kept in lockstep with the unified status list in
|
|
32
|
+
* @project/shared — conveyor-mcp publishes standalone and does not depend on
|
|
33
|
+
* the shared package, so the vocabulary is duplicated here.
|
|
34
|
+
*/
|
|
35
|
+
declare const WAIT_STATUSES: readonly ["Planning", "Open", "InProgress", "ReviewPR", "ReviewDev", "ReviewLive", "Complete", "Cancelled"];
|
|
36
|
+
/** Chat cards are excluded by default — they are conversations, not queue work. */
|
|
37
|
+
declare const DEFAULT_WAIT_TYPES: WaitCardType[];
|
|
38
|
+
declare const DEFAULT_WAIT_SCOPES: WaitScope[];
|
|
39
|
+
/** `Open` is the claimable lane: identified, planned enough to start, unbuilt. */
|
|
40
|
+
declare const DEFAULT_WAIT_STATUSES: string[];
|
|
41
|
+
interface WaitFilter {
|
|
42
|
+
types: WaitCardType[];
|
|
43
|
+
scopes: WaitScope[];
|
|
44
|
+
statuses: string[];
|
|
45
|
+
/** The connected account, resolved at startup. `mine` never matches without it. */
|
|
46
|
+
userId: string | null;
|
|
47
|
+
}
|
|
48
|
+
/** The card fields conveyor-wait filters on and prints — a subset of `TaskCardDTO`. */
|
|
49
|
+
interface WaitCard {
|
|
50
|
+
id: string;
|
|
51
|
+
slug: string | null;
|
|
52
|
+
title: string | null;
|
|
53
|
+
type: string | null;
|
|
54
|
+
status: string | null;
|
|
55
|
+
assignedUserId: string | null;
|
|
56
|
+
}
|
|
57
|
+
type WaitResult = {
|
|
58
|
+
reason: "event";
|
|
59
|
+
card: WaitCard;
|
|
60
|
+
} | {
|
|
61
|
+
reason: "timeout";
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Parse `--types`. Accepts singular or plural (`incident`/`incidents`) and
|
|
65
|
+
* `all`. Throws on an unknown kind rather than silently never matching.
|
|
66
|
+
*/
|
|
67
|
+
declare function parseWaitTypes(value: string | undefined): WaitCardType[];
|
|
68
|
+
/** Parse `--scope`. `all` short-circuits: it subsumes every other scope. */
|
|
69
|
+
declare function parseWaitScopes(value: string | undefined): WaitScope[];
|
|
70
|
+
/** Parse `--statuses` into canonical casing. Unknown statuses are an error. */
|
|
71
|
+
declare function parseWaitStatuses(value: string | undefined): string[];
|
|
72
|
+
/**
|
|
73
|
+
* Project a collection item down to the fields we filter on. `assignedUserId`
|
|
74
|
+
* is derived from the nested `assignedUser` relation — `TaskCardDTO` has no
|
|
75
|
+
* flat assignee id.
|
|
76
|
+
*/
|
|
77
|
+
declare function toWaitCard(item: CardCollectionItem): WaitCard;
|
|
78
|
+
declare function matchesFilter(card: WaitCard, filter: WaitFilter): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* The held set of already-matching card ids. Everything the CLI reports is a
|
|
81
|
+
* transition INTO this set — a card that was already matching and merely got
|
|
82
|
+
* edited is not new work, and a card that stops matching is not either.
|
|
83
|
+
*/
|
|
84
|
+
declare class MatchSet {
|
|
85
|
+
private readonly filter;
|
|
86
|
+
private readonly matched;
|
|
87
|
+
constructor(filter: WaitFilter);
|
|
88
|
+
get size(): number;
|
|
89
|
+
has(id: string): boolean;
|
|
90
|
+
/** Seed from the first snapshot. Pre-existing matches never trigger. */
|
|
91
|
+
seed(cards: WaitCard[]): void;
|
|
92
|
+
/**
|
|
93
|
+
* Re-seed after a `reset` delta or a reconnect gap, returning every card
|
|
94
|
+
* that entered matching state while we were not listening.
|
|
95
|
+
*
|
|
96
|
+
* `complete` says whether the snapshot walk reached the end of the scope.
|
|
97
|
+
* When it did not, ids we no longer see are merged forward rather than
|
|
98
|
+
* dropped — forgetting a still-matching card would re-report it as new on
|
|
99
|
+
* its next delta.
|
|
100
|
+
*/
|
|
101
|
+
reseed(cards: WaitCard[], complete?: boolean): WaitCard[];
|
|
102
|
+
/** Apply an `added`/`updated` delta. Returns the card only on entry. */
|
|
103
|
+
upsert(card: WaitCard): WaitCard | null;
|
|
104
|
+
/** Apply a `removed` delta. A deleted card is never new work. */
|
|
105
|
+
remove(id: string): void;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export { DEFAULT_WAIT_SCOPES, DEFAULT_WAIT_STATUSES, DEFAULT_WAIT_TYPES, MatchSet, WAIT_CARD_TYPES, WAIT_SCOPES, WAIT_STATUSES, type WaitCard, type WaitCardType, type WaitFilter, type WaitResult, type WaitScope, matchesFilter, parseWaitScopes, parseWaitStatuses, parseWaitTypes, toWaitCard };
|
package/dist/wait.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_WAIT_SCOPES,
|
|
3
|
+
DEFAULT_WAIT_STATUSES,
|
|
4
|
+
DEFAULT_WAIT_TYPES,
|
|
5
|
+
MatchSet,
|
|
6
|
+
WAIT_CARD_TYPES,
|
|
7
|
+
WAIT_SCOPES,
|
|
8
|
+
WAIT_STATUSES,
|
|
9
|
+
matchesFilter,
|
|
10
|
+
parseWaitScopes,
|
|
11
|
+
parseWaitStatuses,
|
|
12
|
+
parseWaitTypes,
|
|
13
|
+
toWaitCard
|
|
14
|
+
} from "./chunk-D3Q2QZJA.js";
|
|
15
|
+
export {
|
|
16
|
+
DEFAULT_WAIT_SCOPES,
|
|
17
|
+
DEFAULT_WAIT_STATUSES,
|
|
18
|
+
DEFAULT_WAIT_TYPES,
|
|
19
|
+
MatchSet,
|
|
20
|
+
WAIT_CARD_TYPES,
|
|
21
|
+
WAIT_SCOPES,
|
|
22
|
+
WAIT_STATUSES,
|
|
23
|
+
matchesFilter,
|
|
24
|
+
parseWaitScopes,
|
|
25
|
+
parseWaitStatuses,
|
|
26
|
+
parseWaitTypes,
|
|
27
|
+
toWaitCard
|
|
28
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rallycry/conveyor-mcp",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.30",
|
|
4
4
|
"description": "Conveyor MCP server for Claude Code PM integration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"bin": {
|
|
13
13
|
"conveyor-mcp": "dist/cli.js",
|
|
14
|
-
"conveyor-tunnel": "dist/tunnel-cli.js"
|
|
14
|
+
"conveyor-tunnel": "dist/tunnel-cli.js",
|
|
15
|
+
"conveyor-wait": "dist/wait-cli.js"
|
|
15
16
|
},
|
|
16
17
|
"files": [
|
|
17
18
|
"dist"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tunnel.ts"],"sourcesContent":["import type { ActivePtySession, PtyAttachSnapshot, PtyDataChunk } from \"./connection.js\";\n\n/** ETX (Ctrl-C) byte. In raw mode this arrives as data rather than SIGINT. */\nconst ETX = 0x03;\n\n/**\n * The slice of {@link ConveyorConnection} the tunnel needs. Defined as a\n * structural interface so tests can inject a mock — and, deliberately, so the\n * relay has NO reference to `stopBuild`. Detaching the local terminal must\n * never stop the cloud session, and the cleanest way to guarantee that is to\n * make stopping unreachable from this layer.\n */\nexport interface TunnelConnection {\n createTask(params: {\n title: string;\n description?: string;\n plan?: string;\n status?: string;\n }): Promise<{ id: string; slug: string }>;\n startBuild(taskId: string): Promise<{ taskId: string; status: string }>;\n getActivePtySession(taskId: string): Promise<ActivePtySession>;\n ptyAttach(sessionId: string): Promise<PtyAttachSnapshot>;\n subscribeToSession(sessionId: string): void;\n ptyInput(sessionId: string, data: string): void;\n ptyResize(sessionId: string, cols: number, rows: number): void;\n onPtyData(handler: (chunk: PtyDataChunk) => void): () => void;\n}\n\n/**\n * Local terminal abstraction. The CLI binds this to the real process TTY; tests\n * bind it to an in-memory fake. All output is raw utf8 (node-pty does not use\n * base64), matching the S2 relay encoding.\n */\nexport interface TunnelTty {\n /** Write raw PTY output to the local terminal. */\n write(data: string): void;\n /** Register a stdin handler. Returns an unsubscribe function. */\n onInput(handler: (data: Buffer) => void): () => void;\n /** Register a resize handler. Returns an unsubscribe function. */\n onResize(handler: (cols: number, rows: number) => void): () => void;\n /** Current local terminal dimensions. */\n size(): { cols: number; rows: number };\n}\n\n/** A live tunnel attachment. */\nexport interface TunnelHandle {\n sessionId: string;\n /**\n * Tear down the local relay (stdin/resize/output listeners). Does NOT stop\n * the cloud build — the session keeps running after detach. Idempotent.\n */\n detach(): void;\n}\n\n/** Resolved (non-null) active PTY session. */\nexport interface ResolvedPtySession {\n sessionId: string;\n cols?: number;\n rows?: number;\n}\n\nconst defaultSleep = (ms: number): Promise<void> =>\n new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n\nexport interface WaitForPtySessionOptions {\n intervalMs?: number;\n timeoutMs?: number;\n log?: (msg: string) => void;\n /** Injectable for tests; defaults to a real timer. */\n sleep?: (ms: number) => Promise<void>;\n}\n\n/**\n * Poll {@link TunnelConnection.getActivePtySession} until the cloud agent has\n * booted its PTY and produced at least one ring frame. `sessionId` is resolved\n * server-side from `taskId`, so the local side never invents or supplies one.\n */\nexport async function waitForPtySession(\n conn: TunnelConnection,\n taskId: string,\n options: WaitForPtySessionOptions = {},\n): Promise<ResolvedPtySession> {\n const intervalMs = options.intervalMs ?? 2000;\n const timeoutMs = options.timeoutMs ?? 5 * 60_000;\n const sleep = options.sleep ?? defaultSleep;\n const deadline = Date.now() + timeoutMs;\n\n for (;;) {\n const active = await conn.getActivePtySession(taskId);\n if (active.sessionId) {\n return { sessionId: active.sessionId, cols: active.cols, rows: active.rows };\n }\n if (Date.now() >= deadline) {\n throw new Error(\n `Timed out after ${timeoutMs}ms waiting for the cloud PTY session to become ready`,\n );\n }\n await sleep(intervalMs);\n }\n}\n\nexport interface AttachTunnelOptions {\n /**\n * Called when the local user requests a detach (Ctrl-C / ETX in the input\n * stream). When provided, the ETX byte is intercepted and NOT forwarded to\n * the cloud PTY; when omitted, all input passes through unmodified.\n */\n onDetachRequest?: () => void;\n}\n\n/**\n * Attach the local TTY to a cloud PTY session, reusing the S2 `pty:*` relay\n * envelope. Replays the ring snapshot then streams live frames, with the same\n * seq-based dedup the web `PtyTerminal` uses so no bytes are dropped or doubled\n * across the attach boundary.\n */\nexport async function attachTunnel(\n conn: TunnelConnection,\n session: ResolvedPtySession,\n tty: TunnelTty,\n options: AttachTunnelOptions = {},\n): Promise<TunnelHandle> {\n const { sessionId } = session;\n let attached = false;\n let lastSeq = -1;\n const liveBuffer: PtyDataChunk[] = [];\n\n const writeChunk = (chunk: { seq: number; data: string }): void => {\n tty.write(chunk.data);\n if (chunk.seq > lastSeq) lastSeq = chunk.seq;\n };\n\n // 1. Register the output handler and join the room BEFORE fetching the\n // snapshot. Frames that arrive during the fetch are buffered, never lost.\n const offData = conn.onPtyData((chunk) => {\n if (chunk.sessionId !== sessionId) return;\n if (!attached) {\n liveBuffer.push(chunk);\n return;\n }\n if (chunk.seq <= lastSeq) return;\n writeChunk(chunk);\n });\n conn.subscribeToSession(sessionId);\n\n // 2. Replay the ring snapshot (skip anything already seen).\n const snapshot = await conn.ptyAttach(sessionId);\n for (const chunk of snapshot.chunks) {\n if (chunk.seq > lastSeq) writeChunk(chunk);\n }\n\n // 3. Flush frames buffered during the fetch, then go live.\n attached = true;\n for (const chunk of liveBuffer) {\n if (chunk.seq > lastSeq) writeChunk(chunk);\n }\n liveBuffer.length = 0;\n\n // 4. Wire local stdin → cloud PTY (raw utf8). Intercept ETX for detach.\n const offInput = tty.onInput((data) => {\n if (options.onDetachRequest) {\n const idx = data.indexOf(ETX);\n if (idx !== -1) {\n if (idx > 0) conn.ptyInput(sessionId, data.subarray(0, idx).toString(\"utf8\"));\n options.onDetachRequest();\n return;\n }\n }\n conn.ptyInput(sessionId, data.toString(\"utf8\"));\n });\n\n // 5. Wire resize → cloud PTY, and push the current size now so the cloud TUI\n // repaints to the local viewport dimensions.\n const offResize = tty.onResize((cols, rows) => {\n conn.ptyResize(sessionId, cols, rows);\n });\n const initial = tty.size();\n conn.ptyResize(sessionId, initial.cols, initial.rows);\n\n let detached = false;\n return {\n sessionId,\n detach() {\n if (detached) return;\n detached = true;\n offInput();\n offResize();\n offData();\n // Intentionally NO stopBuild here: detach != stop. The cloud session\n // continues running so the agent's work is never interrupted by the\n // local terminal disconnecting.\n },\n };\n}\n\nexport interface RunTunnelOptions {\n /** Reuse an existing task instead of creating a new card. */\n taskId?: string;\n /** Card fields (used only when `taskId` is not supplied). */\n title?: string;\n description?: string;\n plan?: string;\n pollIntervalMs?: number;\n pollTimeoutMs?: number;\n log?: (msg: string) => void;\n sleep?: (ms: number) => Promise<void>;\n onDetachRequest?: () => void;\n /**\n * Invoked once the PTY session is ready, immediately before attaching. The\n * CLI uses this to enable raw TTY mode only at the last moment — before this\n * point Ctrl-C should still raise SIGINT so the user can abort the wait.\n */\n onReady?: (session: ResolvedPtySession) => void;\n}\n\nexport interface TunnelSession {\n taskId: string;\n sessionId: string;\n handle: TunnelHandle;\n}\n\n/**\n * End-to-end flow: (optionally) create a cloud card, start the build, wait for\n * the cloud PTY to come up, then attach the local terminal. Returns once the\n * relay is live; the caller keeps the process alive and calls `handle.detach()`.\n */\nexport async function runTunnel(\n conn: TunnelConnection,\n tty: TunnelTty,\n options: RunTunnelOptions = {},\n): Promise<TunnelSession> {\n const log = options.log ?? (() => {});\n\n let taskId = options.taskId;\n if (!taskId) {\n const title = options.title;\n if (!title) throw new Error(\"A task title is required to create a new cloud card\");\n log(\"Creating cloud card…\");\n const task = await conn.createTask({\n title,\n description: options.description,\n plan: options.plan,\n });\n taskId = task.id;\n log(`Created card ${task.slug} (${task.id})`);\n }\n\n log(\"Starting cloud build…\");\n await conn.startBuild(taskId);\n\n log(\"Waiting for the cloud PTY session to become ready…\");\n const session = await waitForPtySession(conn, taskId, {\n intervalMs: options.pollIntervalMs,\n timeoutMs: options.pollTimeoutMs,\n log,\n sleep: options.sleep,\n });\n log(`Cloud PTY session ready: ${session.sessionId}`);\n\n options.onReady?.(session);\n\n const handle = await attachTunnel(conn, session, tty, {\n onDetachRequest: options.onDetachRequest,\n });\n log(\"Attached. Press Ctrl-C to detach (the cloud session keeps running).\");\n\n return { taskId, sessionId: session.sessionId, handle };\n}\n"],"mappings":"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","names":[]}
|