@rallycry/conveyor-mcp 5.0.17 → 5.0.19

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.
@@ -4,10 +4,26 @@ import {
4
4
  } from "./chunk-D3Q2QZJA.js";
5
5
 
6
6
  // src/wait-runner.ts
7
+ var STATE_PAGE_LIMIT = 1e3;
8
+ var STATE_MAX_PAGES = 50;
7
9
  var SNAPSHOT_PAGE_LIMIT = 500;
8
10
  var SNAPSHOT_MAX_PAGES = 20;
9
11
  var RESYNC_ATTEMPTS = 4;
10
12
  var RESYNC_RETRY_DELAY_MS = 3e3;
13
+ function isLeanSeedUnsupported(error) {
14
+ const message = error instanceof Error ? error.message : String(error);
15
+ return /^Request timed out: listCardStates$|^Validation error|unknown method/i.test(message);
16
+ }
17
+ function toWaitCardFromState(row) {
18
+ return {
19
+ id: row.id,
20
+ slug: row.slug ?? null,
21
+ title: row.title ?? null,
22
+ type: row.type ?? null,
23
+ status: row.status ?? null,
24
+ assignedUserId: row.assignedUserId ?? null
25
+ };
26
+ }
11
27
  var WaitSession = class {
12
28
  constructor(options) {
13
29
  this.options = options;
@@ -23,6 +39,14 @@ var WaitSession = class {
23
39
  buffered = [];
24
40
  seeded = false;
25
41
  done = false;
42
+ /** Flips off once the API shows it lacks `listCardStates`. */
43
+ leanSeed = true;
44
+ /**
45
+ * Set once a `listCardStates` call has answered. From then on the method
46
+ * exists, so a failure is transient and goes to the resync retry — it never
47
+ * downgrades the session to the walk.
48
+ */
49
+ leanSeedConfirmed = false;
26
50
  offDelta;
27
51
  offReconnect;
28
52
  timer;
@@ -118,8 +142,57 @@ var WaitSession = class {
118
142
  break;
119
143
  }
120
144
  }
121
- /** Walk the scope to the end, or until the page cap stops us. */
145
+ /** The lean seed while the API has `listCardStates`, else the collection walk. */
122
146
  async snapshotAll() {
147
+ if (this.leanSeed) {
148
+ const lean = await this.leanSnapshot();
149
+ if (lean) return lean;
150
+ }
151
+ if (this.done) return { cards: [], complete: false, pages: 0 };
152
+ return this.walkCollection();
153
+ }
154
+ fetchStates(cursor) {
155
+ const { source, projectId, filter, statePageLimit = STATE_PAGE_LIMIT } = this.options;
156
+ return source.listCardStates(projectId, {
157
+ statuses: filter.statuses,
158
+ types: filter.types,
159
+ cursor,
160
+ limit: statePageLimit
161
+ });
162
+ }
163
+ /**
164
+ * Join the scope room, then page the cards in the watched statuses and
165
+ * types. Returns null when the first `listCardStates` call of the session
166
+ * fails the way an older API does: the session then walks the collection
167
+ * for the rest of its life.
168
+ */
169
+ async leanSnapshot() {
170
+ const { source, projectId, stateMaxPages = STATE_MAX_PAGES, log } = this.options;
171
+ await source.subscribeToCardCollection(projectId, { limit: 1 });
172
+ let page;
173
+ try {
174
+ page = await this.fetchStates(null);
175
+ } catch (error) {
176
+ if (this.leanSeedConfirmed || !isLeanSeedUnsupported(error)) throw error;
177
+ this.leanSeed = false;
178
+ if (!this.done) {
179
+ const message = error instanceof Error ? error.message : String(error);
180
+ log?.(`listCardStates did not answer (${message}) \u2014 walking the board instead.`);
181
+ }
182
+ return null;
183
+ }
184
+ this.leanSeedConfirmed = true;
185
+ const cards = page.items.map(toWaitCardFromState);
186
+ let pages = 1;
187
+ while (page.nextCursor !== null && pages < stateMaxPages) {
188
+ page = await this.fetchStates(page.nextCursor);
189
+ pages++;
190
+ cards.push(...page.items.map(toWaitCardFromState));
191
+ }
192
+ return { cards, complete: page.nextCursor === null, pages };
193
+ }
194
+ /** The fallback: walk the collection to the end, or until the page cap stops us. */
195
+ async walkCollection() {
123
196
  const {
124
197
  source,
125
198
  projectId,
@@ -129,11 +202,13 @@ var WaitSession = class {
129
202
  const cards = [];
130
203
  let cursor = null;
131
204
  let complete = false;
132
- for (let page = 0; page < maxPages; page++) {
205
+ let pages = 0;
206
+ while (pages < maxPages) {
133
207
  const result = await source.subscribeToCardCollection(projectId, {
134
208
  cursor,
135
209
  limit: pageLimit
136
210
  });
211
+ pages++;
137
212
  for (const item of result.items ?? []) cards.push(toWaitCard(item));
138
213
  cursor = result.nextCursor ?? null;
139
214
  if (cursor === null) {
@@ -141,16 +216,16 @@ var WaitSession = class {
141
216
  break;
142
217
  }
143
218
  }
144
- return { cards, complete };
219
+ return { cards, complete, pages };
145
220
  }
146
221
  async seedOrResync(initial) {
147
222
  this.seeded = false;
148
- const { cards, complete } = await this.snapshotAll();
223
+ const { cards, complete, pages } = await this.snapshotAll();
149
224
  if (this.done) return;
150
225
  if (initial) {
151
226
  this.tracker.seed(cards);
152
227
  this.options.log?.(
153
- `Watching ${cards.length} card(s) \u2014 ${this.tracker.size} already match; waiting for new work.`
228
+ `Seeded ${cards.length} card(s) from ${pages} page(s) \u2014 ${this.tracker.size} already match; waiting for new work.`
154
229
  );
155
230
  } else {
156
231
  const triggers = this.tracker.reseed(cards, complete);
@@ -1,4 +1,4 @@
1
- // ../shared/dist/chunk-SFSQ7NKP.js
1
+ // ../shared/dist/chunk-DUZLPI2K.js
2
2
  import { z } from "zod";
3
3
  import { z as z2 } from "zod";
4
4
  var DEFAULT_SONNET_MODEL = "claude-sonnet-5-5";
@@ -53,7 +53,10 @@ var actionsPrebakeRegistrySchema = z.string().trim().min(1).regex(
53
53
  function uniqueSortedArray(item, minimum = 0) {
54
54
  return z.array(item).min(minimum).superRefine((values, ctx) => {
55
55
  if (new Set(values).size !== values.length) {
56
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Duplicate values are not allowed" });
56
+ ctx.addIssue({
57
+ code: z.ZodIssueCode.custom,
58
+ message: "Duplicate values are not allowed"
59
+ });
57
60
  }
58
61
  }).transform((values) => [...values].sort());
59
62
  }
@@ -500,9 +503,9 @@ var searchFilterFields = {
500
503
  desc: "Only return cards with no assignee (mutually exclusive with assigneeId)"
501
504
  })
502
505
  ),
503
- limit: f.optional(f.number({ desc: "Max results to return (default 20)" }))
506
+ limit: f.optional(f.number({ desc: "Max results to return (default 20, at most 200)" }))
504
507
  };
505
- var searchResultNote = "Every result lists the tags it carries, so you can see why it matched. Results are relevance-ordered: highest priority first then newest; suggestions-only queries rank by upvote score. Returns summaries \u2014 plan omitted, description truncated; use get_task for full details.";
508
+ var searchResultNote = "Every result lists the tags it carries, so you can see why it matched. Searches every status, Complete and Cancelled included, unless statusFilters narrows it. Returns at most 200 results. Results are relevance-ordered: highest priority first then newest; suggestions-only queries rank by upvote score. Returns summaries \u2014 plan omitted, description truncated; use get_task for full details.";
506
509
  var searchTasksContract = defineToolContract({
507
510
  name: "search_tasks",
508
511
  agent: {
@@ -1104,6 +1104,20 @@ var ConveyorConnection = class {
1104
1104
  limit: opts.limit
1105
1105
  });
1106
1106
  }
1107
+ /**
1108
+ * A lean, status-filtered page of a project's cards, ordered by id — the
1109
+ * conveyor-wait seed. An API from before this method never acks the call,
1110
+ * so callers on an older server see a timeout (see `wait-runner.ts`).
1111
+ */
1112
+ listCardStates(projectId, opts) {
1113
+ return this.call("listCardStates", {
1114
+ projectId,
1115
+ statuses: opts.statuses,
1116
+ types: opts.types,
1117
+ cursor: opts.cursor ?? null,
1118
+ limit: opts.limit
1119
+ });
1120
+ }
1107
1121
  /**
1108
1122
  * Listen for card collection deltas on a project scope. The event name is
1109
1123
  * the room name by design — no id parsing client-side. Returns an
package/dist/cli.js CHANGED
@@ -81,13 +81,13 @@ import {
81
81
  uploadAttachmentContract,
82
82
  uploadSiteAssetsContract,
83
83
  voteDecisionContract
84
- } from "./chunk-IRVUONC3.js";
84
+ } from "./chunk-VFZOYTDH.js";
85
85
  import {
86
86
  installLifecycle
87
87
  } from "./chunk-5KU3QS6A.js";
88
88
  import {
89
89
  ConveyorConnection
90
- } from "./chunk-6JLKJUYV.js";
90
+ } from "./chunk-XLNIBIX4.js";
91
91
 
92
92
  // src/cli.ts
93
93
  import { createRequire } from "module";
@@ -552,7 +552,7 @@ function summarizeTask(task) {
552
552
  const { plan, description, ...rest } = task;
553
553
  const summary = { ...rest };
554
554
  summary.description = typeof description === "string" && description.length > DESCRIPTION_PREVIEW_CHARS ? `${description.slice(0, DESCRIPTION_PREVIEW_CHARS)}\u2026 [truncated \u2014 use get_task for the full description]` : description ?? null;
555
- summary.hasPlan = typeof plan === "string" && plan.length > 0;
555
+ summary.hasPlan = typeof plan === "string" ? plan.length > 0 : rest.hasPlan === true;
556
556
  return summary;
557
557
  }
558
558
 
@@ -579,10 +579,10 @@ function registerListTasks(server2, conn2) {
579
579
  registerHotTool(
580
580
  server2,
581
581
  "list_tasks",
582
- "List project cards, optionally filtered by card type, status, or assignment (a specific assignee, or unassigned tasks). Defaults to type=task \u2014 pass typeFilters to list incidents, suggestions or decisions. Results are relevance-ordered: highest priority first then newest; suggestions-only queries rank by upvote score. Pass projectId to target a specific project; otherwise the configured default project is used. Returns summaries \u2014 plan omitted, description truncated; use get_task for full details.",
582
+ "List project cards, optionally filtered by card type, status, or assignment (a specific assignee, or unassigned tasks). Defaults to type=task \u2014 pass typeFilters to list incidents, suggestions or decisions. Without a status, Complete and Cancelled cards are left out \u2014 pass status to list one of them, or use search_tasks, which covers every status. Returns at most 200 cards. Results are relevance-ordered: highest priority first then newest; suggestions-only queries rank by upvote score. Pass projectId to target a specific project; otherwise the configured default project is used. Returns summaries \u2014 plan omitted, description truncated; use get_task for full details.",
583
583
  {
584
584
  projectId: z5.string().optional().describe("Target Conveyor project ID"),
585
- status: z5.enum(STATUS_ENUM).optional().describe("Filter by task status"),
585
+ status: z5.enum(STATUS_ENUM).optional().describe("Filter by task status. Omit for every status except Complete and Cancelled."),
586
586
  typeFilters: z5.array(z5.enum(CARD_TYPE_ENUM)).optional().describe(
587
587
  'Card types to include, e.g. ["incident"] or ["task", "incident"]. Omit for tasks only.'
588
588
  ),
@@ -590,7 +590,7 @@ function registerListTasks(server2, conn2) {
590
590
  unassigned: z5.boolean().optional().describe("Only return tasks with no assignee (mutually exclusive with assigneeId)"),
591
591
  subProjectId: BOARD_FILTER,
592
592
  onHold: z5.boolean().optional().describe("true = only cards on hold, false = exclude them, omit = both"),
593
- limit: z5.number().optional().describe("Max tasks to return (default 50)")
593
+ limit: z5.number().optional().describe("Max tasks to return (default 50, at most 200)")
594
594
  },
595
595
  async (params) => {
596
596
  const tasks = await conn2.listTasks(params);
@@ -2272,6 +2272,13 @@ var ListProjectTasksRequestSchema = z72.object({
2272
2272
  }).refine((p) => !(p.unassigned && p.assigneeId), {
2273
2273
  message: "Pass either assigneeId or unassigned, not both"
2274
2274
  });
2275
+ var ListCardStatesRequestSchema = z72.object({
2276
+ projectId: z72.string(),
2277
+ statuses: z72.array(z72.string()).min(1).max(32),
2278
+ types: z72.array(z72.string()).optional(),
2279
+ cursor: z72.string().nullable().optional(),
2280
+ limit: z72.number().int().positive().optional().default(500)
2281
+ });
2275
2282
  var GetProjectTaskRequestSchema = z72.object({
2276
2283
  projectId: z72.string(),
2277
2284
  taskId: z72.string()
@@ -130,6 +130,23 @@ interface CardCollectionPage {
130
130
  nextCursor: string | null;
131
131
  totalCount: number;
132
132
  }
133
+ /**
134
+ * One `listCardStates` row: the six fields conveyor-wait filters on and
135
+ * prints, with a flat assignee id. Mirrors `CardStateDTO` in @project/shared.
136
+ */
137
+ interface CardStateRow {
138
+ id: string;
139
+ slug: string | null;
140
+ title: string | null;
141
+ type: string | null;
142
+ status: string | null;
143
+ assignedUserId: string | null;
144
+ }
145
+ /** One page of `listCardStates`, ordered by id. */
146
+ interface CardStatePage {
147
+ items: CardStateRow[];
148
+ nextCursor: string | null;
149
+ }
133
150
  /**
134
151
  * A quickdraw collection change on a card scope. `added` vs `updated` is
135
152
  * cosmetic — most card writes go through a hand-rolled upsert that always
@@ -1099,6 +1116,17 @@ declare class ConveyorConnection {
1099
1116
  cursor?: string | null;
1100
1117
  limit?: number;
1101
1118
  }): Promise<CardCollectionPage>;
1119
+ /**
1120
+ * A lean, status-filtered page of a project's cards, ordered by id — the
1121
+ * conveyor-wait seed. An API from before this method never acks the call,
1122
+ * so callers on an older server see a timeout (see `wait-runner.ts`).
1123
+ */
1124
+ listCardStates(projectId: string, opts: {
1125
+ statuses: string[];
1126
+ types?: string[];
1127
+ cursor?: string | null;
1128
+ limit?: number;
1129
+ }): Promise<CardStatePage>;
1102
1130
  /**
1103
1131
  * Listen for card collection deltas on a project scope. The event name is
1104
1132
  * the room name by design — no id parsing client-side. Returns an
@@ -1115,4 +1143,4 @@ declare class ConveyorConnection {
1115
1143
  disconnect(): void;
1116
1144
  }
1117
1145
 
1118
- export { type ActivePtySession as A, type CardCollectionPage as C, type PtyAttachSnapshot as P, type CardCollectionDelta as a, type CardCollectionItem as b, type PtyDataChunk as c, ConveyorConnection as d, type ConveyorMcpConfig as e };
1146
+ export { type ActivePtySession as A, type CardCollectionPage as C, type PtyAttachSnapshot as P, type CardStatePage as a, type CardCollectionDelta as b, type CardCollectionItem as c, type PtyDataChunk as d, ConveyorConnection as e, type ConveyorMcpConfig as f };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { A as ActivePtySession, d as ConveyorConnection, e as ConveyorMcpConfig, P as PtyAttachSnapshot, c as PtyDataChunk } from './connection-Ctg-QM1s.js';
1
+ export { A as ActivePtySession, e as ConveyorConnection, f as ConveyorMcpConfig, P as PtyAttachSnapshot, d as PtyDataChunk } from './connection-C2qTFync.js';
2
2
  export { AttachTunnelOptions, ResolvedPtySession, RunTunnelOptions, TunnelConnection, TunnelHandle, TunnelSession, TunnelTty, WaitForPtySessionOptions, attachTunnel, runTunnel, waitForPtySession } from './tunnel.js';
3
3
  import 'socket.io-client';
4
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-6JLKJUYV.js";
8
+ } from "./chunk-XLNIBIX4.js";
9
9
  export {
10
10
  ConveyorConnection,
11
11
  attachTunnel,
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  transferSiteAsset
4
- } from "./chunk-IRVUONC3.js";
4
+ } from "./chunk-VFZOYTDH.js";
5
5
  import {
6
6
  ConveyorConnection
7
- } from "./chunk-6JLKJUYV.js";
7
+ } from "./chunk-XLNIBIX4.js";
8
8
 
9
9
  // src/site-assets-cli.ts
10
10
  import crypto from "crypto";
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-5KU3QS6A.js";
8
8
  import {
9
9
  ConveyorConnection
10
- } from "./chunk-6JLKJUYV.js";
10
+ } from "./chunk-XLNIBIX4.js";
11
11
 
12
12
  // src/tunnel-cli.ts
13
13
  var HELP = `conveyor-tunnel \u2014 attach your local terminal to a cloud Claude Code session.
package/dist/tunnel.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { A as ActivePtySession, P as PtyAttachSnapshot, c as PtyDataChunk } from './connection-Ctg-QM1s.js';
1
+ import { A as ActivePtySession, P as PtyAttachSnapshot, d as PtyDataChunk } from './connection-C2qTFync.js';
2
2
  import 'socket.io-client';
3
3
  import '@project/shared';
4
4
 
package/dist/wait-cli.js CHANGED
@@ -4,10 +4,10 @@ import {
4
4
  } from "./chunk-5KU3QS6A.js";
5
5
  import {
6
6
  ConveyorConnection
7
- } from "./chunk-6JLKJUYV.js";
7
+ } from "./chunk-XLNIBIX4.js";
8
8
  import {
9
9
  runWait
10
- } from "./chunk-OPZL4NDT.js";
10
+ } from "./chunk-J7LQ5DAT.js";
11
11
  import {
12
12
  DEFAULT_WAIT_SCOPES,
13
13
  DEFAULT_WAIT_STATUSES,
@@ -1,4 +1,4 @@
1
- import { C as CardCollectionPage, a as CardCollectionDelta } from './connection-Ctg-QM1s.js';
1
+ import { C as CardCollectionPage, a as CardStatePage, b as CardCollectionDelta } from './connection-C2qTFync.js';
2
2
  import { WaitFilter, WaitResult } from './wait.js';
3
3
  import 'socket.io-client';
4
4
  import '@project/shared';
@@ -7,9 +7,16 @@ import '@project/shared';
7
7
  * `conveyor-wait` runner — the live half of the engine.
8
8
  *
9
9
  * `wait.ts` decides what "matching" means; this module owns the socket-shaped
10
- * lifecycle around it: seed from a paged snapshot, apply deltas, re-snapshot
11
- * after a `reset` or a reconnect, and settle on the first card that enters the
12
- * filter (or on the deadline).
10
+ * lifecycle around it: seed, apply deltas, re-seed after a `reset` or a
11
+ * reconnect, and settle on the first card that enters the filter (or on the
12
+ * deadline).
13
+ *
14
+ * A seed is two calls, not a walk of the board. A cursor-less collection
15
+ * subscribe with `limit: 1` joins the scope room, which is what makes deltas
16
+ * flow; its one item is ignored. `listCardStates` then pages just the cards in
17
+ * the watched statuses and types — six fields each, usually one page. An API
18
+ * from before `listCardStates` gets today's walk of the collection instead
19
+ * (conveyor-mcp publishes ahead of the API release).
13
20
  *
14
21
  * It talks to a narrow `CardCollectionSource` rather than a socket, so the
15
22
  * reconnect, reset, and paging paths are unit-testable without a server.
@@ -24,6 +31,12 @@ interface CardCollectionSource {
24
31
  cursor?: string | null;
25
32
  limit?: number;
26
33
  }): Promise<CardCollectionPage>;
34
+ listCardStates(projectId: string, opts: {
35
+ statuses: string[];
36
+ types?: string[];
37
+ cursor?: string | null;
38
+ limit?: number;
39
+ }): Promise<CardStatePage>;
27
40
  onCardDelta(projectId: string, handler: (delta: CardCollectionDelta) => void): () => void;
28
41
  onReconnect(handler: () => void): () => void;
29
42
  }
@@ -32,7 +45,13 @@ interface RunWaitOptions {
32
45
  projectId: string;
33
46
  filter: WaitFilter;
34
47
  timeoutMs: number;
48
+ /** Rows per `listCardStates` page. */
49
+ statePageLimit?: number;
50
+ /** Page cap for one `listCardStates` seed. */
51
+ stateMaxPages?: number;
52
+ /** Items per page of the fallback collection walk. */
35
53
  pageLimit?: number;
54
+ /** Page cap for the fallback collection walk. */
36
55
  maxPages?: number;
37
56
  /** Backoff between re-snapshot attempts. Overridable so tests stay fast. */
38
57
  resyncRetryDelayMs?: number;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runWait
3
- } from "./chunk-OPZL4NDT.js";
3
+ } from "./chunk-J7LQ5DAT.js";
4
4
  import "./chunk-D3Q2QZJA.js";
5
5
  export {
6
6
  runWait
package/dist/wait.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { b as CardCollectionItem } from './connection-Ctg-QM1s.js';
1
+ import { c as CardCollectionItem } from './connection-C2qTFync.js';
2
2
  import 'socket.io-client';
3
3
  import '@project/shared';
4
4
 
@@ -14,10 +14,14 @@ import '@project/shared';
14
14
  * - **Creates arrive as `updated` deltas.** Most card writes go through the
15
15
  * service's hand-rolled upsert choke point, which always emits `updated`.
16
16
  * "New work" is therefore a set diff, never a delta-type check.
17
- * - **The snapshot ack is one PAGE, not the scope.** `cardsByProject` omits
18
- * `ids` (the scope is unbounded), so seeding walks `nextCursor` to the end.
19
- * When the walk is capped we merge instead of replacing, because dropping an
20
- * unseen id would make it look newly-matching on the next delta.
17
+ * - **The seed is not the collection snapshot.** A snapshot ack is one page
18
+ * of full cards, and a large project's scope runs to thousands of them. The
19
+ * seed instead pages `listCardStates`: only the watched statuses and types,
20
+ * six fields a row. The collection subscribe (with `limit: 1`) only joins
21
+ * the delta room. An older API without `listCardStates` falls back to
22
+ * walking the collection's `nextCursor` to the end. When either walk is
23
+ * capped we merge instead of replacing, because dropping an unseen id would
24
+ * make it look newly-matching on the next delta.
21
25
  *
22
26
  * The live half — snapshot paging, reconnect, and settling — lives in
23
27
  * `wait-runner.ts`; this module is the pure filter and set-diff logic it uses.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rallycry/conveyor-mcp",
3
- "version": "5.0.17",
3
+ "version": "5.0.19",
4
4
  "description": "Conveyor MCP server for Claude Code PM integration",
5
5
  "keywords": [
6
6
  "claude",
@@ -31,7 +31,7 @@
31
31
  "dev": "tsup --watch --no-dts --no-clean",
32
32
  "lint": "oxlint -c ../../.oxlintrc.json src",
33
33
  "lint:fix": "oxlint -c ../../.oxlintrc.json --fix src",
34
- "typecheck": "tsgo --noEmit",
34
+ "typecheck": "bash ../../scripts/typecheck.sh",
35
35
  "test": "vitest run --passWithNoTests",
36
36
  "test:unit": "vitest run --passWithNoTests"
37
37
  },