@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
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// src/wait.ts
|
|
2
|
+
var WAIT_CARD_TYPES = ["task", "incident", "suggestion", "chat"];
|
|
3
|
+
var WAIT_SCOPES = ["mine", "unclaimed", "all"];
|
|
4
|
+
var WAIT_STATUSES = [
|
|
5
|
+
"Planning",
|
|
6
|
+
"Open",
|
|
7
|
+
"InProgress",
|
|
8
|
+
"ReviewPR",
|
|
9
|
+
"ReviewDev",
|
|
10
|
+
"ReviewLive",
|
|
11
|
+
"Complete",
|
|
12
|
+
"Cancelled"
|
|
13
|
+
];
|
|
14
|
+
var DEFAULT_WAIT_TYPES = ["task", "incident", "suggestion"];
|
|
15
|
+
var DEFAULT_WAIT_SCOPES = ["mine", "unclaimed"];
|
|
16
|
+
var DEFAULT_WAIT_STATUSES = ["Open"];
|
|
17
|
+
function splitList(value) {
|
|
18
|
+
return value.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
|
|
19
|
+
}
|
|
20
|
+
function normalizeToken(value) {
|
|
21
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
22
|
+
}
|
|
23
|
+
function parseWaitTypes(value) {
|
|
24
|
+
if (value === void 0) return [...DEFAULT_WAIT_TYPES];
|
|
25
|
+
const out = [];
|
|
26
|
+
for (const raw of splitList(value)) {
|
|
27
|
+
const token = normalizeToken(raw);
|
|
28
|
+
if (token === "all") return [...WAIT_CARD_TYPES];
|
|
29
|
+
const match = WAIT_CARD_TYPES.find((type) => type === token || `${type}s` === token);
|
|
30
|
+
if (!match) {
|
|
31
|
+
throw new Error(`Unknown card type "${raw}". Valid: ${WAIT_CARD_TYPES.join(", ")}, all.`);
|
|
32
|
+
}
|
|
33
|
+
if (!out.includes(match)) out.push(match);
|
|
34
|
+
}
|
|
35
|
+
if (out.length === 0) return [...DEFAULT_WAIT_TYPES];
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
function parseWaitScopes(value) {
|
|
39
|
+
if (value === void 0) return [...DEFAULT_WAIT_SCOPES];
|
|
40
|
+
const out = [];
|
|
41
|
+
for (const raw of splitList(value)) {
|
|
42
|
+
const token = normalizeToken(raw);
|
|
43
|
+
if (token === "all" || token === "any") return ["all"];
|
|
44
|
+
const match = WAIT_SCOPES.find((scope) => scope === token);
|
|
45
|
+
if (!match) {
|
|
46
|
+
throw new Error(`Unknown scope "${raw}". Valid: ${WAIT_SCOPES.join(", ")}.`);
|
|
47
|
+
}
|
|
48
|
+
if (!out.includes(match)) out.push(match);
|
|
49
|
+
}
|
|
50
|
+
if (out.length === 0) return [...DEFAULT_WAIT_SCOPES];
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
function parseWaitStatuses(value) {
|
|
54
|
+
if (value === void 0) return [...DEFAULT_WAIT_STATUSES];
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const raw of splitList(value)) {
|
|
57
|
+
const token = normalizeToken(raw);
|
|
58
|
+
if (token === "all" || token === "any") return [...WAIT_STATUSES];
|
|
59
|
+
const match = WAIT_STATUSES.find((status) => normalizeToken(status) === token);
|
|
60
|
+
if (!match) {
|
|
61
|
+
throw new Error(`Unknown status "${raw}". Valid: ${WAIT_STATUSES.join(", ")}, all.`);
|
|
62
|
+
}
|
|
63
|
+
if (!out.includes(match)) out.push(match);
|
|
64
|
+
}
|
|
65
|
+
if (out.length === 0) return [...DEFAULT_WAIT_STATUSES];
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
function toWaitCard(item) {
|
|
69
|
+
return {
|
|
70
|
+
id: item.id,
|
|
71
|
+
slug: item.slug ?? null,
|
|
72
|
+
title: item.title ?? null,
|
|
73
|
+
type: item.type ?? null,
|
|
74
|
+
status: item.status ?? null,
|
|
75
|
+
assignedUserId: item.assignedUser?.id ?? null
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function matchesFilter(card, filter) {
|
|
79
|
+
const type = card.type ?? "task";
|
|
80
|
+
if (!filter.types.includes(type)) return false;
|
|
81
|
+
if (card.status === null || !filter.statuses.includes(card.status)) return false;
|
|
82
|
+
return matchesScope(card, filter);
|
|
83
|
+
}
|
|
84
|
+
function matchesScope(card, filter) {
|
|
85
|
+
if (filter.scopes.includes("all")) return true;
|
|
86
|
+
if (filter.scopes.includes("unclaimed") && card.assignedUserId === null) return true;
|
|
87
|
+
if (filter.scopes.includes("mine") && filter.userId !== null && card.assignedUserId === filter.userId) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
var MatchSet = class {
|
|
93
|
+
constructor(filter) {
|
|
94
|
+
this.filter = filter;
|
|
95
|
+
}
|
|
96
|
+
filter;
|
|
97
|
+
matched = /* @__PURE__ */ new Set();
|
|
98
|
+
get size() {
|
|
99
|
+
return this.matched.size;
|
|
100
|
+
}
|
|
101
|
+
has(id) {
|
|
102
|
+
return this.matched.has(id);
|
|
103
|
+
}
|
|
104
|
+
/** Seed from the first snapshot. Pre-existing matches never trigger. */
|
|
105
|
+
seed(cards) {
|
|
106
|
+
this.matched.clear();
|
|
107
|
+
for (const card of cards) {
|
|
108
|
+
if (matchesFilter(card, this.filter)) this.matched.add(card.id);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Re-seed after a `reset` delta or a reconnect gap, returning every card
|
|
113
|
+
* that entered matching state while we were not listening.
|
|
114
|
+
*
|
|
115
|
+
* `complete` says whether the snapshot walk reached the end of the scope.
|
|
116
|
+
* When it did not, ids we no longer see are merged forward rather than
|
|
117
|
+
* dropped — forgetting a still-matching card would re-report it as new on
|
|
118
|
+
* its next delta.
|
|
119
|
+
*/
|
|
120
|
+
reseed(cards, complete = true) {
|
|
121
|
+
const triggers = [];
|
|
122
|
+
const next = new Set(complete ? [] : this.matched);
|
|
123
|
+
for (const card of cards) {
|
|
124
|
+
if (!matchesFilter(card, this.filter)) continue;
|
|
125
|
+
next.add(card.id);
|
|
126
|
+
if (!this.matched.has(card.id)) triggers.push(card);
|
|
127
|
+
}
|
|
128
|
+
this.matched.clear();
|
|
129
|
+
for (const id of next) this.matched.add(id);
|
|
130
|
+
return triggers;
|
|
131
|
+
}
|
|
132
|
+
/** Apply an `added`/`updated` delta. Returns the card only on entry. */
|
|
133
|
+
upsert(card) {
|
|
134
|
+
const matches = matchesFilter(card, this.filter);
|
|
135
|
+
const held = this.matched.has(card.id);
|
|
136
|
+
if (matches && !held) {
|
|
137
|
+
this.matched.add(card.id);
|
|
138
|
+
return card;
|
|
139
|
+
}
|
|
140
|
+
if (!matches && held) this.matched.delete(card.id);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
/** Apply a `removed` delta. A deleted card is never new work. */
|
|
144
|
+
remove(id) {
|
|
145
|
+
this.matched.delete(id);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export {
|
|
150
|
+
WAIT_CARD_TYPES,
|
|
151
|
+
WAIT_SCOPES,
|
|
152
|
+
WAIT_STATUSES,
|
|
153
|
+
DEFAULT_WAIT_TYPES,
|
|
154
|
+
DEFAULT_WAIT_SCOPES,
|
|
155
|
+
DEFAULT_WAIT_STATUSES,
|
|
156
|
+
parseWaitTypes,
|
|
157
|
+
parseWaitScopes,
|
|
158
|
+
parseWaitStatuses,
|
|
159
|
+
toWaitCard,
|
|
160
|
+
matchesFilter,
|
|
161
|
+
MatchSet
|
|
162
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MatchSet,
|
|
3
|
+
toWaitCard
|
|
4
|
+
} from "./chunk-D3Q2QZJA.js";
|
|
5
|
+
|
|
6
|
+
// src/wait-runner.ts
|
|
7
|
+
var SNAPSHOT_PAGE_LIMIT = 500;
|
|
8
|
+
var SNAPSHOT_MAX_PAGES = 20;
|
|
9
|
+
var RESYNC_ATTEMPTS = 4;
|
|
10
|
+
var RESYNC_RETRY_DELAY_MS = 3e3;
|
|
11
|
+
var WaitSession = class {
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.options = options;
|
|
14
|
+
this.tracker = new MatchSet(options.filter);
|
|
15
|
+
}
|
|
16
|
+
options;
|
|
17
|
+
tracker;
|
|
18
|
+
/**
|
|
19
|
+
* Deltas that land before (or during) a snapshot are held, then replayed
|
|
20
|
+
* against the freshly seeded set. Replaying an already-matching card is a
|
|
21
|
+
* no-op, so the overlap is safe.
|
|
22
|
+
*/
|
|
23
|
+
buffered = [];
|
|
24
|
+
seeded = false;
|
|
25
|
+
done = false;
|
|
26
|
+
offDelta;
|
|
27
|
+
offReconnect;
|
|
28
|
+
timer;
|
|
29
|
+
/** Serializes snapshots: a reset arriving mid-resync queues, it does not race. */
|
|
30
|
+
chain = Promise.resolve();
|
|
31
|
+
settle;
|
|
32
|
+
abort;
|
|
33
|
+
run() {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const { source, projectId, timeoutMs, log = () => {
|
|
36
|
+
} } = this.options;
|
|
37
|
+
this.settle = resolve;
|
|
38
|
+
this.abort = reject;
|
|
39
|
+
this.offDelta = source.onCardDelta(projectId, (delta) => this.applyDelta(delta));
|
|
40
|
+
this.offReconnect = source.onReconnect(() => {
|
|
41
|
+
log("Reconnected \u2014 re-snapshotting the board scope.");
|
|
42
|
+
this.schedule(false);
|
|
43
|
+
});
|
|
44
|
+
this.timer = setTimeout(() => this.finish({ reason: "timeout" }), timeoutMs);
|
|
45
|
+
this.schedule(true);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
teardown() {
|
|
49
|
+
this.done = true;
|
|
50
|
+
if (this.timer !== void 0) clearTimeout(this.timer);
|
|
51
|
+
this.offDelta?.();
|
|
52
|
+
this.offReconnect?.();
|
|
53
|
+
}
|
|
54
|
+
finish(result) {
|
|
55
|
+
if (this.done) return;
|
|
56
|
+
this.teardown();
|
|
57
|
+
this.settle?.(result);
|
|
58
|
+
}
|
|
59
|
+
fail(error) {
|
|
60
|
+
if (this.done) return;
|
|
61
|
+
this.teardown();
|
|
62
|
+
this.abort?.(error instanceof Error ? error : new Error(String(error)));
|
|
63
|
+
}
|
|
64
|
+
schedule(initial) {
|
|
65
|
+
this.chain = this.chain.then(() => initial ? this.seedOrResync(true) : this.resyncWithRetry()).catch((err) => this.fail(err));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Re-snapshot after a reset or a reconnect, retrying a transient failure.
|
|
69
|
+
*
|
|
70
|
+
* Giving up here is not a soft failure: deltas buffer while the session is
|
|
71
|
+
* unseeded, so an abandoned resync would leave the wait deaf until its
|
|
72
|
+
* deadline. Rethrow only once every attempt is spent. The initial seed stays
|
|
73
|
+
* fatal by contrast — that failure means a bad project id or revoked access.
|
|
74
|
+
*/
|
|
75
|
+
async resyncWithRetry() {
|
|
76
|
+
const { resyncRetryDelayMs = RESYNC_RETRY_DELAY_MS, log } = this.options;
|
|
77
|
+
for (let attempt = 1; attempt <= RESYNC_ATTEMPTS; attempt++) {
|
|
78
|
+
if (this.done) return;
|
|
79
|
+
try {
|
|
80
|
+
await this.seedOrResync(false);
|
|
81
|
+
return;
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (attempt === RESYNC_ATTEMPTS) throw err;
|
|
84
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
85
|
+
log?.(
|
|
86
|
+
`Re-snapshot attempt ${attempt} failed (${message}) \u2014 retrying in ${Math.round(resyncRetryDelayMs / 1e3)}s.`
|
|
87
|
+
);
|
|
88
|
+
await this.delay(resyncRetryDelayMs);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** A sleep that never holds the process open past the wait itself. */
|
|
93
|
+
delay(ms) {
|
|
94
|
+
return new Promise((resolve) => {
|
|
95
|
+
const timer = setTimeout(resolve, ms);
|
|
96
|
+
timer.unref?.();
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
applyDelta(delta) {
|
|
100
|
+
if (this.done) return;
|
|
101
|
+
if (!this.seeded) {
|
|
102
|
+
this.buffered.push(delta);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
switch (delta.type) {
|
|
106
|
+
case "added":
|
|
107
|
+
case "updated": {
|
|
108
|
+
const entered = this.tracker.upsert(toWaitCard(delta.item));
|
|
109
|
+
if (entered) this.finish({ reason: "event", card: entered });
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case "removed":
|
|
113
|
+
this.tracker.remove(delta.id);
|
|
114
|
+
break;
|
|
115
|
+
case "reset":
|
|
116
|
+
this.options.log?.("Board scope reset \u2014 re-snapshotting.");
|
|
117
|
+
this.schedule(false);
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** Walk the scope to the end, or until the page cap stops us. */
|
|
122
|
+
async snapshotAll() {
|
|
123
|
+
const {
|
|
124
|
+
source,
|
|
125
|
+
projectId,
|
|
126
|
+
pageLimit = SNAPSHOT_PAGE_LIMIT,
|
|
127
|
+
maxPages = SNAPSHOT_MAX_PAGES
|
|
128
|
+
} = this.options;
|
|
129
|
+
const cards = [];
|
|
130
|
+
let cursor = null;
|
|
131
|
+
let complete = false;
|
|
132
|
+
for (let page = 0; page < maxPages; page++) {
|
|
133
|
+
const result = await source.subscribeToCardCollection(projectId, {
|
|
134
|
+
cursor,
|
|
135
|
+
limit: pageLimit
|
|
136
|
+
});
|
|
137
|
+
for (const item of result.items ?? []) cards.push(toWaitCard(item));
|
|
138
|
+
cursor = result.nextCursor ?? null;
|
|
139
|
+
if (cursor === null) {
|
|
140
|
+
complete = true;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { cards, complete };
|
|
145
|
+
}
|
|
146
|
+
async seedOrResync(initial) {
|
|
147
|
+
this.seeded = false;
|
|
148
|
+
const { cards, complete } = await this.snapshotAll();
|
|
149
|
+
if (this.done) return;
|
|
150
|
+
if (initial) {
|
|
151
|
+
this.tracker.seed(cards);
|
|
152
|
+
this.options.log?.(
|
|
153
|
+
`Watching ${cards.length} card(s) \u2014 ${this.tracker.size} already match; waiting for new work.`
|
|
154
|
+
);
|
|
155
|
+
} else {
|
|
156
|
+
const triggers = this.tracker.reseed(cards, complete);
|
|
157
|
+
if (triggers.length > 0) {
|
|
158
|
+
this.finish({ reason: "event", card: triggers[0] });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
this.seeded = true;
|
|
163
|
+
for (const delta of this.buffered.splice(0)) {
|
|
164
|
+
if (this.done) return;
|
|
165
|
+
this.applyDelta(delta);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
function runWait(options) {
|
|
170
|
+
return new WaitSession(options).run();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export {
|
|
174
|
+
runWait
|
|
175
|
+
};
|
|
@@ -49,6 +49,8 @@ function enrichToolError(error, fallback) {
|
|
|
49
49
|
var ConveyorConnection = class {
|
|
50
50
|
socket = null;
|
|
51
51
|
config;
|
|
52
|
+
/** project slug → id, for resolving `<project>/<card>` card paths. */
|
|
53
|
+
projectSlugIds = /* @__PURE__ */ new Map();
|
|
52
54
|
constructor(config) {
|
|
53
55
|
this.config = config;
|
|
54
56
|
}
|
|
@@ -225,7 +227,7 @@ var ConveyorConnection = class {
|
|
|
225
227
|
async updateTask(params) {
|
|
226
228
|
const { projectId, addTags, removeTags, ...rest } = params;
|
|
227
229
|
const resolvedProjectId = this.resolveProjectId(projectId);
|
|
228
|
-
const hasCoreUpdate = rest.title !== void 0 || rest.description !== void 0 || rest.plan !== void 0 || rest.status !== void 0 || rest.risk !== void 0 || rest.storyPointValue !== void 0 || rest.assignedUserId !== void 0 || rest.subProjectId !== void 0;
|
|
230
|
+
const hasCoreUpdate = rest.title !== void 0 || rest.description !== void 0 || rest.plan !== void 0 || rest.status !== void 0 || rest.risk !== void 0 || rest.storyPointValue !== void 0 || rest.assignedUserId !== void 0 || rest.subProjectId !== void 0 || rest.githubBranch !== void 0;
|
|
229
231
|
const removedTags = removeTags ?? [];
|
|
230
232
|
const addedTags = addTags ?? [];
|
|
231
233
|
if (removedTags.length > 0) {
|
|
@@ -317,6 +319,39 @@ var ConveyorConnection = class {
|
|
|
317
319
|
const response = await this.call("listAccessibleProjects", { pageSize: 100 });
|
|
318
320
|
return this.normalizeProjectList(response);
|
|
319
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Resolve a project SLUG (as pasted in a `<project>/<card>` card path) to its
|
|
324
|
+
* id.
|
|
325
|
+
*
|
|
326
|
+
* Cached, because a slug→id mapping effectively never changes and this sits
|
|
327
|
+
* in front of an ordinary card lookup. A miss refetches once before failing,
|
|
328
|
+
* so a project created after the cache warmed still resolves.
|
|
329
|
+
*
|
|
330
|
+
* Throws naming the slug rather than falling back to the configured default
|
|
331
|
+
* project: the caller pasted a specific project, and silently answering about
|
|
332
|
+
* a different one is the worst possible outcome here.
|
|
333
|
+
*/
|
|
334
|
+
async resolveProjectIdBySlug(slug) {
|
|
335
|
+
const cached = this.projectSlugIds.get(slug);
|
|
336
|
+
if (cached) return cached;
|
|
337
|
+
await this.refreshProjectSlugCache();
|
|
338
|
+
const resolved = this.projectSlugIds.get(slug);
|
|
339
|
+
if (!resolved) {
|
|
340
|
+
throw new Error(
|
|
341
|
+
`No accessible Conveyor project has the slug "${slug}". Use list_projects to see the projects this connection can reach.`
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
return resolved;
|
|
345
|
+
}
|
|
346
|
+
async refreshProjectSlugCache() {
|
|
347
|
+
for (const project of await this.listProjects()) {
|
|
348
|
+
if (typeof project !== "object" || project === null) continue;
|
|
349
|
+
const { slug, id } = project;
|
|
350
|
+
if (typeof slug === "string" && typeof id === "string") {
|
|
351
|
+
this.projectSlugIds.set(slug, id);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
320
355
|
// ── Build Management ────────────────────────────────────────────────
|
|
321
356
|
startBuild(taskId, projectId) {
|
|
322
357
|
return this.call("startProjectBuild", {
|
|
@@ -580,6 +615,18 @@ var ConveyorConnection = class {
|
|
|
580
615
|
}
|
|
581
616
|
return result;
|
|
582
617
|
}
|
|
618
|
+
/**
|
|
619
|
+
* Move a card under a new parent, or detach it with an explicit null.
|
|
620
|
+
* `parentTaskId` rides the rest-spread rather than a conditional one so a
|
|
621
|
+
* null survives the call — stripping it would silently no-op a detach.
|
|
622
|
+
*/
|
|
623
|
+
setTaskParent(params) {
|
|
624
|
+
const { projectId, ...rest } = params;
|
|
625
|
+
return this.call("setProjectTaskParent", {
|
|
626
|
+
projectId: this.resolveProjectId(projectId),
|
|
627
|
+
...rest
|
|
628
|
+
});
|
|
629
|
+
}
|
|
583
630
|
updateSubtask(params) {
|
|
584
631
|
const { projectId, ...rest } = params;
|
|
585
632
|
return this.call("updateProjectSubtask", {
|
|
@@ -781,6 +828,50 @@ var ConveyorConnection = class {
|
|
|
781
828
|
socket.off("pty:data", handler);
|
|
782
829
|
};
|
|
783
830
|
}
|
|
831
|
+
// ── Board card collection (conveyor-wait) ───────────────────────────
|
|
832
|
+
/**
|
|
833
|
+
* Fetch one page of a project's live card collection.
|
|
834
|
+
*
|
|
835
|
+
* A cursor-less call also joins the scope room, which is what starts (or
|
|
836
|
+
* restarts, after a reconnect) delta delivery. Cursor-bearing calls are pure
|
|
837
|
+
* paging and join nothing, so always lead with the cursor-less call.
|
|
838
|
+
*/
|
|
839
|
+
subscribeToCardCollection(projectId, opts = {}) {
|
|
840
|
+
return this.callService("taskService", "collection:subscribe", {
|
|
841
|
+
collection: "cardsByProject",
|
|
842
|
+
scopeId: projectId,
|
|
843
|
+
cursor: opts.cursor ?? null,
|
|
844
|
+
limit: opts.limit
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Listen for card collection deltas on a project scope. The event name is
|
|
849
|
+
* the room name by design — no id parsing client-side. Returns an
|
|
850
|
+
* unsubscribe function.
|
|
851
|
+
*/
|
|
852
|
+
onCardDelta(projectId, handler) {
|
|
853
|
+
const socket = this.socket;
|
|
854
|
+
if (!socket) throw new Error("Not connected");
|
|
855
|
+
const event = `taskService:collection:cardsByProject:${projectId}`;
|
|
856
|
+
socket.on(event, handler);
|
|
857
|
+
return () => {
|
|
858
|
+
socket.off(event, handler);
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Run `handler` on every reconnect of the underlying socket. A reconnect
|
|
863
|
+
* drops every room this socket had joined, so long-lived subscribers must
|
|
864
|
+
* re-subscribe and reconcile whatever they missed. Returns an unsubscribe
|
|
865
|
+
* function.
|
|
866
|
+
*/
|
|
867
|
+
onReconnect(handler) {
|
|
868
|
+
const socket = this.socket;
|
|
869
|
+
if (!socket) throw new Error("Not connected");
|
|
870
|
+
socket.on("connect", handler);
|
|
871
|
+
return () => {
|
|
872
|
+
socket.off("connect", handler);
|
|
873
|
+
};
|
|
874
|
+
}
|
|
784
875
|
// ── Connection lifecycle ────────────────────────────────────────────
|
|
785
876
|
disconnect() {
|
|
786
877
|
this.socket?.disconnect();
|
|
@@ -791,4 +882,3 @@ var ConveyorConnection = class {
|
|
|
791
882
|
export {
|
|
792
883
|
ConveyorConnection
|
|
793
884
|
};
|
|
794
|
-
//# sourceMappingURL=chunk-Y6ZJUNDX.js.map
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ConveyorConnection
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-X3EHPZNH.js";
|
|
5
5
|
|
|
6
6
|
// src/cli.ts
|
|
7
7
|
import { createRequire } from "module";
|
|
@@ -215,7 +215,10 @@ var getTaskContract = defineToolContract({
|
|
|
215
215
|
fields: {
|
|
216
216
|
projectId: mcpProjectId,
|
|
217
217
|
taskId: f.string({
|
|
218
|
-
|
|
218
|
+
// The path form is MCP-only on purpose: an in-pod agent is bound to one
|
|
219
|
+
// project, where a bare slug already resolves, so the `agent` half of
|
|
220
|
+
// this contract deliberately says nothing about it.
|
|
221
|
+
desc: "The task ID or slug (the value in a card URL, /cards/<slug>), or a path-form reference '<project-slug>/<card-slug>' from the board's Copy path action"
|
|
219
222
|
})
|
|
220
223
|
}
|
|
221
224
|
}
|
|
@@ -632,10 +635,11 @@ var MCP_STATUS_ENUM = [
|
|
|
632
635
|
"Complete",
|
|
633
636
|
"Cancelled"
|
|
634
637
|
];
|
|
638
|
+
var AGENT_TAGS = `Glossary tag names to label the child with, e.g. ["agent", "pack"]. Names, not ids, matched case-insensitively against this project's tags (use list_tags to see them). A name that matches nothing comes back in the result and never fails the create.`;
|
|
635
639
|
var createSubtaskContract = defineToolContract({
|
|
636
640
|
name: "create_subtask",
|
|
637
641
|
agent: {
|
|
638
|
-
description: "Create a subtask under the
|
|
642
|
+
description: "Create a subtask (a child card) under the CURRENT card. This is how a card becomes a pack: the first child turns this card into the pack parent, and the children build as one unit. Use when breaking the current card into smaller pieces during planning. For a sibling card that lands after this one merges, use create_follow_up_task.",
|
|
639
643
|
fields: {
|
|
640
644
|
title: f.string({ desc: "Subtask title" }),
|
|
641
645
|
description: f.optional(f.string({ desc: cardDescriptionDesc("Brief description") })),
|
|
@@ -643,7 +647,8 @@ var createSubtaskContract = defineToolContract({
|
|
|
643
647
|
ordinal: f.optional(f.number({ desc: "Step/order number (0-based)" })),
|
|
644
648
|
storyPointValue: f.optional(f.number({ desc: SP_DESCRIPTION })),
|
|
645
649
|
followParentStatus: f.optional(f.boolean({ desc: AGENT_FOLLOW_PARENT_STATUS })),
|
|
646
|
-
dependsOn: f.optional(f.array(f.string(), { desc: AGENT_DEPENDS_ON }))
|
|
650
|
+
dependsOn: f.optional(f.array(f.string(), { desc: AGENT_DEPENDS_ON })),
|
|
651
|
+
tags: f.optional(f.array(f.string(), { desc: AGENT_TAGS }))
|
|
647
652
|
}
|
|
648
653
|
},
|
|
649
654
|
mcp: {
|
|
@@ -755,11 +760,46 @@ var listSubtasksContract = defineToolContract({
|
|
|
755
760
|
}
|
|
756
761
|
}
|
|
757
762
|
});
|
|
763
|
+
var SET_PARENT_ORDINAL = "Position among the new parent's children. Defaults to after every existing sibling. A detached card has no siblings to order against, so it only matters when you adopt.";
|
|
764
|
+
var SET_PARENT_WARNINGS = "The move is reported, never refused: a card that already has a pull request, live compute, a release, children of its own, or dependency edges that now cross packs comes back with a warning in the result. Its branch is NOT re-cut against the new parent's feature branch.";
|
|
765
|
+
var setTaskParentContract = defineToolContract({
|
|
766
|
+
name: "set_task_parent",
|
|
767
|
+
agent: {
|
|
768
|
+
description: `Adopt an EXISTING card into this card's pack, or eject one of this card's children. The card keeps its own chat, plan, and history \u2014 it moves under (or out from) the current card, inherits the board, and lands after the current children. To make a NEW child use create_subtask instead. ${SET_PARENT_WARNINGS}`,
|
|
769
|
+
fields: {
|
|
770
|
+
taskId: f.string({
|
|
771
|
+
desc: "Id, slug, or branch name of the card to move. Must live in this project."
|
|
772
|
+
}),
|
|
773
|
+
detach: f.optional(
|
|
774
|
+
f.boolean({
|
|
775
|
+
desc: "Eject the card from this pack instead of adopting it \u2014 it becomes a standalone card and stops following the parent's status. The card must already be a child of the current card."
|
|
776
|
+
})
|
|
777
|
+
),
|
|
778
|
+
ordinal: f.optional(f.number({ desc: SET_PARENT_ORDINAL })),
|
|
779
|
+
followParentStatus: f.optional(f.boolean({ desc: AGENT_FOLLOW_PARENT_STATUS }))
|
|
780
|
+
}
|
|
781
|
+
},
|
|
782
|
+
mcp: {
|
|
783
|
+
description: `Re-parent an existing card: pass a parentTaskId to adopt it into that card's pack, or null to detach it into a standalone card. The card keeps its chat, plan, and history; adopting also moves it to the parent's board and orders it after the parent's existing children. Use create_subtask for a NEW child, and move_card to move a card to another PROJECT. Pass projectId to target a specific project; otherwise the configured default project is used. ${SET_PARENT_WARNINGS}`,
|
|
784
|
+
fields: {
|
|
785
|
+
projectId: mcpProjectId,
|
|
786
|
+
taskId: f.string({ desc: "Id or slug of the card to move" }),
|
|
787
|
+
parentTaskId: f.nullable(
|
|
788
|
+
f.string({
|
|
789
|
+
desc: "Id or slug of the new parent, or null to detach the card from its current parent. Must be in the same project, and can be neither the card itself nor one of its own descendants."
|
|
790
|
+
})
|
|
791
|
+
),
|
|
792
|
+
ordinal: f.optional(f.number({ desc: SET_PARENT_ORDINAL })),
|
|
793
|
+
followParentStatus: f.optional(f.boolean({ desc: MCP_FOLLOW_PARENT_STATUS }))
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
});
|
|
758
797
|
var subtasksContracts = [
|
|
759
798
|
createSubtaskContract,
|
|
760
799
|
updateSubtaskContract,
|
|
761
800
|
deleteSubtaskContract,
|
|
762
|
-
listSubtasksContract
|
|
801
|
+
listSubtasksContract,
|
|
802
|
+
setTaskParentContract
|
|
763
803
|
];
|
|
764
804
|
var mcpTaskIdOrSlug = f.string({ desc: "The task ID or slug" });
|
|
765
805
|
var listTaskFilesContract = defineToolContract({
|
|
@@ -1335,12 +1375,26 @@ function registerListTasks(server2, conn2) {
|
|
|
1335
1375
|
}
|
|
1336
1376
|
);
|
|
1337
1377
|
}
|
|
1378
|
+
function splitCardRef(ref) {
|
|
1379
|
+
const slash = ref.indexOf("/");
|
|
1380
|
+
if (slash <= 0) return { cardSlug: ref };
|
|
1381
|
+
const projectSlug = ref.slice(0, slash);
|
|
1382
|
+
const cardSlug = ref.slice(slash + 1);
|
|
1383
|
+
if (!cardSlug) return { cardSlug: ref };
|
|
1384
|
+
return { projectSlug, cardSlug };
|
|
1385
|
+
}
|
|
1386
|
+
async function resolveCardRef(conn2, ref, projectId2) {
|
|
1387
|
+
const { projectSlug, cardSlug } = splitCardRef(ref);
|
|
1388
|
+
if (!projectSlug) return { cardSlug, projectId: projectId2 };
|
|
1389
|
+
return { cardSlug, projectId: await conn2.resolveProjectIdBySlug(projectSlug) };
|
|
1390
|
+
}
|
|
1338
1391
|
function registerGetTask(server2, conn2) {
|
|
1339
1392
|
registerContractTool(
|
|
1340
1393
|
server2,
|
|
1341
1394
|
getTaskContract,
|
|
1342
1395
|
async (params) => {
|
|
1343
|
-
const
|
|
1396
|
+
const ref = await resolveCardRef(conn2, params.taskId, params.projectId);
|
|
1397
|
+
const task = await conn2.getTask(ref.cardSlug, ref.projectId);
|
|
1344
1398
|
return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
|
|
1345
1399
|
},
|
|
1346
1400
|
{ alwaysLoad: true }
|
|
@@ -1349,13 +1403,16 @@ function registerGetTask(server2, conn2) {
|
|
|
1349
1403
|
function registerGetCardBySlug(server2, conn2) {
|
|
1350
1404
|
server2.tool(
|
|
1351
1405
|
"get_card_by_slug",
|
|
1352
|
-
|
|
1406
|
+
'Get full card details by the slug from a card URL (/cards/<slug>) instead of a task ID. Also accepts the board\'s "Copy path" form, `<project-slug>/<card-slug>` \u2014 an embedded project slug wins over projectId and over the configured default. Pass projectId to target a specific project; otherwise the configured default project is used.',
|
|
1353
1407
|
{
|
|
1354
1408
|
projectId: z5.string().optional().describe("Target Conveyor project ID"),
|
|
1355
|
-
slug: z5.string().describe(
|
|
1409
|
+
slug: z5.string().describe(
|
|
1410
|
+
"The card slug from a Conveyor card URL, e.g. 'ship-it', or a path-form reference 'my-project/ship-it' (the board's Copy path action)"
|
|
1411
|
+
)
|
|
1356
1412
|
},
|
|
1357
1413
|
async (params) => {
|
|
1358
|
-
const
|
|
1414
|
+
const ref = await resolveCardRef(conn2, params.slug, params.projectId);
|
|
1415
|
+
const task = await conn2.getCardBySlug(ref.cardSlug, ref.projectId);
|
|
1359
1416
|
return { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] };
|
|
1360
1417
|
}
|
|
1361
1418
|
);
|
|
@@ -1391,7 +1448,7 @@ function registerCreateTask(server2, conn2) {
|
|
|
1391
1448
|
function registerUpdateTask(server2, conn2) {
|
|
1392
1449
|
server2.tool(
|
|
1393
1450
|
"update_task",
|
|
1394
|
-
"Update task fields: title, description, plan, status, risk, story points, assignment, or tags. Set status to claim a card (InProgress), triage it (Open), or cancel it (Cancelled); for review approvals prefer approve_task / request_changes, which guard against stale-state races. Tags are additive/subtractive \u2014 pass addTags/removeTags with tag names (not a replace-set). Pass projectId to target a specific project; otherwise the configured default project is used. Moving a task beyond Planning auto-fills any missing icon, story points, and agent assignment \u2014 don't spend turns on them; pass storyPointValue only to correct the sizing yourself. For subtasks use update_subtask.",
|
|
1451
|
+
"Update task fields: title, description, plan, status, risk, story points, assignment, githubBranch, or tags. Set status to claim a card (InProgress), triage it (Open), or cancel it (Cancelled); for review approvals prefer approve_task / request_changes, which guard against stale-state races. Tags are additive/subtractive \u2014 pass addTags/removeTags with tag names (not a replace-set). Pass projectId to target a specific project; otherwise the configured default project is used. Moving a task beyond Planning auto-fills any missing icon, story points, and agent assignment \u2014 don't spend turns on them; pass storyPointValue only to correct the sizing yourself. For subtasks use update_subtask.",
|
|
1395
1452
|
{
|
|
1396
1453
|
projectId: z5.string().optional().describe("Target Conveyor project ID"),
|
|
1397
1454
|
taskId: z5.string().describe("The task ID"),
|
|
@@ -1409,6 +1466,9 @@ function registerUpdateTask(server2, conn2) {
|
|
|
1409
1466
|
subProjectId: z5.string().nullable().optional().describe(
|
|
1410
1467
|
"Assign the task to a sub-project board; null moves it back to the parent board. Omit to leave unchanged."
|
|
1411
1468
|
),
|
|
1469
|
+
githubBranch: z5.string().nullable().optional().describe(
|
|
1470
|
+
"Record the task's ACTUAL working branch (e.g. a locally-driven pack's branch cut at claim time, so identification never mints a competing name and pack-child merges settle against reality), or null to detach. The branch must already exist on origin, and a live workspace bound to a different branch rejects the write."
|
|
1471
|
+
),
|
|
1412
1472
|
addTags: z5.array(z5.string()).optional().describe(
|
|
1413
1473
|
'Tag names to add to the card (e.g. ["refactor"]). Additive \u2014 existing tags are kept. Unknown names are rejected; use list_tags to see available tags or manage_tags to create one.'
|
|
1414
1474
|
),
|
|
@@ -1964,11 +2024,28 @@ function registerDeleteSubtask(server2, conn2) {
|
|
|
1964
2024
|
};
|
|
1965
2025
|
});
|
|
1966
2026
|
}
|
|
2027
|
+
function registerSetTaskParent(server2, conn2) {
|
|
2028
|
+
registerContractTool(server2, setTaskParentContract, async (params) => {
|
|
2029
|
+
const result = await conn2.setTaskParent(params);
|
|
2030
|
+
const from = result.previousParentTaskId ?? "none";
|
|
2031
|
+
const to = result.parentSlug ?? result.parentTaskId ?? "none";
|
|
2032
|
+
const warnings = result.warnings.length > 0 ? ` Warnings: ${result.warnings.join(", ")}.` : "";
|
|
2033
|
+
return {
|
|
2034
|
+
content: [
|
|
2035
|
+
{
|
|
2036
|
+
type: "text",
|
|
2037
|
+
text: `Card ${result.slug} re-parented: ${from} -> ${to} (status: ${result.status}).${warnings}`
|
|
2038
|
+
}
|
|
2039
|
+
]
|
|
2040
|
+
};
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
1967
2043
|
function registerSubtaskTools(server2, conn2) {
|
|
1968
2044
|
registerCreateSubtask(server2, conn2);
|
|
1969
2045
|
registerUpdateSubtask(server2, conn2);
|
|
1970
2046
|
registerListSubtasks(server2, conn2);
|
|
1971
2047
|
registerDeleteSubtask(server2, conn2);
|
|
2048
|
+
registerSetTaskParent(server2, conn2);
|
|
1972
2049
|
}
|
|
1973
2050
|
|
|
1974
2051
|
// src/tools/dependencies.ts
|
|
@@ -2591,4 +2668,3 @@ process.on("SIGTERM", () => {
|
|
|
2591
2668
|
conn.disconnect();
|
|
2592
2669
|
process.exit(0);
|
|
2593
2670
|
});
|
|
2594
|
-
//# sourceMappingURL=cli.js.map
|