magic-spells 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +90 -2
- package/bin/magic.js +12 -0
- package/dist/chunk-JZ7XESF4.js +1829 -0
- package/dist/chunk-PQFOO5A7.js +1057 -0
- package/dist/main.js +1111 -0
- package/dist/operations-GGMLIYEW.js +9 -0
- package/dist/src-DJYPPJFM.js +16 -0
- package/dist/src-KJGFPRQP.js +17 -0
- package/dist/src-L2I546M7.js +1071 -0
- package/package.json +33 -5
|
@@ -0,0 +1,1057 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MagicError,
|
|
3
|
+
mapHttpError,
|
|
4
|
+
readStr,
|
|
5
|
+
rec,
|
|
6
|
+
toMagicError
|
|
7
|
+
} from "./chunk-JZ7XESF4.js";
|
|
8
|
+
|
|
9
|
+
// ../pyramid/src/operations/index.ts
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
|
|
12
|
+
// ../pyramid/src/errors.ts
|
|
13
|
+
var PRODUCT = "pyramid";
|
|
14
|
+
var REGENERATE_KEY_HINT = "Regenerate your key in Pyramid \u2192 Settings \u2192 API Keys.";
|
|
15
|
+
var NETWORK_HINT = "Check MAGIC_PYRAMID_URL and your network connection.";
|
|
16
|
+
var NOT_FOUND_KINDS = ["project", "task", "status", "stage", "label", "user", "field"];
|
|
17
|
+
function pyramidError(code, message, opts = {}) {
|
|
18
|
+
const [mapped, extra] = translate(code);
|
|
19
|
+
const details = extra || opts.details ? { ...opts.details, ...extra } : void 0;
|
|
20
|
+
return new MagicError(mapped, message, {
|
|
21
|
+
...opts,
|
|
22
|
+
product: PRODUCT,
|
|
23
|
+
...details ? { details } : {}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function translate(code) {
|
|
27
|
+
const notFound = /^(\w+)_not_found$/.exec(code);
|
|
28
|
+
if (notFound && code !== "not_found") return ["not_found", { kind: notFound[1] }];
|
|
29
|
+
const ambiguous = /^ambiguous_(\w+)_name$/.exec(code);
|
|
30
|
+
if (ambiguous) return ["ambiguous_name", { kind: ambiguous[1] }];
|
|
31
|
+
switch (code) {
|
|
32
|
+
case "status_not_in_stage":
|
|
33
|
+
case "invalid_field_value":
|
|
34
|
+
case "reply_depth_exceeded":
|
|
35
|
+
case "task_archived":
|
|
36
|
+
return ["validation_failed", { reason: code }];
|
|
37
|
+
default:
|
|
38
|
+
return [code];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function mapPyramidHttpError(status, body) {
|
|
42
|
+
return mapHttpError(status, body, {
|
|
43
|
+
product: PRODUCT,
|
|
44
|
+
title: "Pyramid",
|
|
45
|
+
authHint: REGENERATE_KEY_HINT,
|
|
46
|
+
notFoundKinds: NOT_FOUND_KINDS
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function toPyramidError(e) {
|
|
50
|
+
return toMagicError(e, { product: PRODUCT, networkHint: NETWORK_HINT });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ../pyramid/src/hydrate.ts
|
|
54
|
+
function hydrateTaskSummary(raw, workflow) {
|
|
55
|
+
const r = obj(raw);
|
|
56
|
+
const statusId = statusIdOf(r);
|
|
57
|
+
return {
|
|
58
|
+
id: str(r.id),
|
|
59
|
+
key: str(r.key),
|
|
60
|
+
title: str(r.title),
|
|
61
|
+
description: nullableStr(r.description),
|
|
62
|
+
status: statusRef(workflow, statusId),
|
|
63
|
+
stage: stageRefForStatus(workflow, statusId, r),
|
|
64
|
+
owner: ownerStub(workflow, r.owner, r.owner_id),
|
|
65
|
+
reviewer: ownerStub(workflow, r.reviewer, r.reviewer_id),
|
|
66
|
+
labels: labelNames(workflow, r.labels ?? r.label_ids),
|
|
67
|
+
archived: r.archived_at != null || r.archived === true,
|
|
68
|
+
updated_at: str(r.updated_at)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function hydrateTaskDetail(raw, workflow) {
|
|
72
|
+
const r = obj(raw);
|
|
73
|
+
const detail = hydrateTaskSummary(raw, workflow);
|
|
74
|
+
detail.description = nullableStr(r.description);
|
|
75
|
+
if (Array.isArray(r.field_values)) {
|
|
76
|
+
detail.field_values = r.field_values.map((fv) => {
|
|
77
|
+
const f = obj(fv);
|
|
78
|
+
return {
|
|
79
|
+
// Join a custom-field id to its name where possible (no bare UUID).
|
|
80
|
+
field: fieldName(workflow, f.field ?? f.field_id ?? f.key),
|
|
81
|
+
value: f.value
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (r.estimates && typeof r.estimates === "object") {
|
|
86
|
+
const e = obj(r.estimates);
|
|
87
|
+
detail.estimates = {
|
|
88
|
+
total_hours: num(e.total_hours),
|
|
89
|
+
by_stage: byStageNames(workflow, e.by_stage)
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (Array.isArray(r.comments)) {
|
|
93
|
+
detail.comments = r.comments.map((c) => hydrateComment(c, workflow));
|
|
94
|
+
}
|
|
95
|
+
if (Array.isArray(r.references)) {
|
|
96
|
+
detail.references = r.references.map(hydrateReference);
|
|
97
|
+
}
|
|
98
|
+
if (Array.isArray(r.followers)) {
|
|
99
|
+
detail.followers = r.followers.map((f) => userStubFromAny(workflow, f)).filter((u) => u !== null);
|
|
100
|
+
}
|
|
101
|
+
if (Array.isArray(r.dependencies)) {
|
|
102
|
+
detail.dependencies = r.dependencies.map((d) => {
|
|
103
|
+
const dep = obj(d);
|
|
104
|
+
return { id: str(dep.id), key: str(dep.key), type: str(dep.type) };
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return detail;
|
|
108
|
+
}
|
|
109
|
+
function hydrateComment(raw, workflow) {
|
|
110
|
+
const c = obj(raw);
|
|
111
|
+
const replies = (Array.isArray(c.replies) ? c.replies : []).map(
|
|
112
|
+
(r) => hydrateReply(r, workflow)
|
|
113
|
+
);
|
|
114
|
+
return {
|
|
115
|
+
id: str(c.id),
|
|
116
|
+
task_id: str(c.task_id),
|
|
117
|
+
stage: stageRef(workflow, str(c.stage_id)),
|
|
118
|
+
author: authorStub(workflow, c.author, c.author_id),
|
|
119
|
+
content: str(c.content),
|
|
120
|
+
mentions: mentionStubs(workflow, c.mentions),
|
|
121
|
+
replies,
|
|
122
|
+
created_at: str(c.created_at)
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function hydrateReply(raw, workflow) {
|
|
126
|
+
const c = obj(raw);
|
|
127
|
+
return {
|
|
128
|
+
id: str(c.id),
|
|
129
|
+
task_id: str(c.task_id),
|
|
130
|
+
stage: stageRef(workflow, str(c.stage_id)),
|
|
131
|
+
author: authorStub(workflow, c.author, c.author_id),
|
|
132
|
+
content: str(c.content),
|
|
133
|
+
mentions: mentionStubs(workflow, c.mentions),
|
|
134
|
+
created_at: str(c.created_at)
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
var PERSON_VALUED_EVENTS = /* @__PURE__ */ new Set(["owner_changed", "reviewer_changed"]);
|
|
138
|
+
function hydrateTimelineEvent(raw, workflow) {
|
|
139
|
+
const e = obj(raw);
|
|
140
|
+
const eventType = str(e.event_type);
|
|
141
|
+
const value = (v) => {
|
|
142
|
+
if (!PERSON_VALUED_EVENTS.has(eventType)) return v ?? null;
|
|
143
|
+
if (typeof v !== "string" || v.length === 0) return v ?? null;
|
|
144
|
+
const stub = userStubFromAny(workflow, v);
|
|
145
|
+
return stub && stub.display_name.length > 0 ? stub : v;
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
id: str(e.id),
|
|
149
|
+
task_id: str(e.task_id) || str(obj(e.data).task_id),
|
|
150
|
+
event_type: eventType,
|
|
151
|
+
actor: userStubFromAny(workflow, e.actor ?? e.actor_id),
|
|
152
|
+
old_value: value(e.old_value),
|
|
153
|
+
new_value: value(e.new_value),
|
|
154
|
+
data: obj(e.data),
|
|
155
|
+
created_at: str(e.created_at)
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
var REFERENCE_TYPES = [
|
|
159
|
+
"github_pr",
|
|
160
|
+
"github_commit",
|
|
161
|
+
"github_branch",
|
|
162
|
+
"github_issue",
|
|
163
|
+
"figma",
|
|
164
|
+
"url"
|
|
165
|
+
];
|
|
166
|
+
function hydrateReference(raw) {
|
|
167
|
+
const r = obj(raw);
|
|
168
|
+
const rt = str(r.reference_type);
|
|
169
|
+
const reference_type = REFERENCE_TYPES.includes(rt) ? rt : "url";
|
|
170
|
+
return {
|
|
171
|
+
id: str(r.id),
|
|
172
|
+
reference_type,
|
|
173
|
+
title: str(r.title),
|
|
174
|
+
url: str(r.url),
|
|
175
|
+
external_status: nullableStr(r.external_status),
|
|
176
|
+
external_sub_id: nullableStr(r.external_sub_id)
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function statusIdOf(r) {
|
|
180
|
+
if (typeof r.status_id === "string") return r.status_id;
|
|
181
|
+
if (r.status && typeof r.status === "object") {
|
|
182
|
+
const s = obj(r.status);
|
|
183
|
+
if (typeof s.id === "string") return s.id;
|
|
184
|
+
}
|
|
185
|
+
return "";
|
|
186
|
+
}
|
|
187
|
+
function statusRef(wf, statusId) {
|
|
188
|
+
const s = wf.statuses.find((x) => x.id === statusId);
|
|
189
|
+
return { id: statusId, name: s?.name ?? "" };
|
|
190
|
+
}
|
|
191
|
+
function stageRefForStatus(wf, statusId, r) {
|
|
192
|
+
const status = wf.statuses.find((x) => x.id === statusId);
|
|
193
|
+
const stageId = status?.stage_id ?? (typeof r.stage_id === "string" ? r.stage_id : "");
|
|
194
|
+
return stageRef(wf, stageId);
|
|
195
|
+
}
|
|
196
|
+
function stageRef(wf, stageId) {
|
|
197
|
+
const stage = wf.stages.find((x) => x.id === stageId);
|
|
198
|
+
return { id: stageId, name: stage?.name ?? "" };
|
|
199
|
+
}
|
|
200
|
+
function displayName(wf, userId) {
|
|
201
|
+
return wf.members.find((m) => m.id === userId)?.display_name ?? "";
|
|
202
|
+
}
|
|
203
|
+
function ownerStub(wf, expanded, idValue) {
|
|
204
|
+
const stub = userStubFromAny(wf, expanded);
|
|
205
|
+
if (stub) return stub;
|
|
206
|
+
return userStubFromAny(wf, idValue);
|
|
207
|
+
}
|
|
208
|
+
function authorStub(wf, expanded, idValue) {
|
|
209
|
+
return ownerStub(wf, expanded, idValue) ?? {
|
|
210
|
+
id: typeof idValue === "string" ? idValue : "",
|
|
211
|
+
display_name: ""
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function memberOf(wf, userId) {
|
|
215
|
+
return wf.members.find((m) => m.id === userId);
|
|
216
|
+
}
|
|
217
|
+
function userStubFromAny(wf, value) {
|
|
218
|
+
if (typeof value === "string") {
|
|
219
|
+
if (value.length === 0) return null;
|
|
220
|
+
return withProfile(wf, { id: value, display_name: displayName(wf, value) }, {});
|
|
221
|
+
}
|
|
222
|
+
if (value && typeof value === "object") {
|
|
223
|
+
const u = obj(value);
|
|
224
|
+
const id = str(u.id);
|
|
225
|
+
if (id.length === 0) return null;
|
|
226
|
+
const dn = typeof u.display_name === "string" && u.display_name.length > 0 ? u.display_name : displayName(wf, id);
|
|
227
|
+
return withProfile(wf, { id, display_name: dn }, u);
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
function withProfile(wf, stub, raw) {
|
|
232
|
+
if (raw.first_name !== void 0) stub.first_name = nullableStr(raw.first_name);
|
|
233
|
+
if (raw.last_name !== void 0) stub.last_name = nullableStr(raw.last_name);
|
|
234
|
+
if (raw.avatar_url !== void 0) stub.avatar_url = nullableStr(raw.avatar_url);
|
|
235
|
+
const jobTitle = typeof raw.job_title === "string" && raw.job_title.length > 0 ? raw.job_title : memberOf(wf, stub.id)?.job_title;
|
|
236
|
+
if (jobTitle) stub.job_title = jobTitle;
|
|
237
|
+
return stub;
|
|
238
|
+
}
|
|
239
|
+
function mentionStubs(wf, value) {
|
|
240
|
+
if (!Array.isArray(value)) return [];
|
|
241
|
+
return value.map((m) => userStubFromAny(wf, m)).filter((u) => u !== null);
|
|
242
|
+
}
|
|
243
|
+
function labelNames(wf, labels) {
|
|
244
|
+
if (!Array.isArray(labels)) return [];
|
|
245
|
+
return labels.map((l) => {
|
|
246
|
+
if (typeof l === "string") {
|
|
247
|
+
const byId = wf.labels.find((x) => x.id === l);
|
|
248
|
+
if (byId) return byId.name;
|
|
249
|
+
return l;
|
|
250
|
+
}
|
|
251
|
+
if (l && typeof l === "object") {
|
|
252
|
+
const lo = obj(l);
|
|
253
|
+
if (typeof lo.name === "string" && lo.name.length > 0) return lo.name;
|
|
254
|
+
if (typeof lo.id === "string") {
|
|
255
|
+
return wf.labels.find((x) => x.id === lo.id)?.name ?? "";
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return "";
|
|
259
|
+
}).filter((name) => name.length > 0);
|
|
260
|
+
}
|
|
261
|
+
function fieldName(wf, value) {
|
|
262
|
+
const v = str(value);
|
|
263
|
+
if (v.length === 0) return "";
|
|
264
|
+
for (const t of wf.templates) {
|
|
265
|
+
const f = t.fields.find((x) => x.id === v || x.key === v);
|
|
266
|
+
if (f) return f.name;
|
|
267
|
+
}
|
|
268
|
+
return v;
|
|
269
|
+
}
|
|
270
|
+
function byStageNames(wf, value) {
|
|
271
|
+
const out = {};
|
|
272
|
+
if (!value || typeof value !== "object") return out;
|
|
273
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
274
|
+
const stage = wf.stages.find((s) => s.id === key);
|
|
275
|
+
out[stage?.name ?? key] = num(raw);
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
function obj(v) {
|
|
280
|
+
return v && typeof v === "object" ? v : {};
|
|
281
|
+
}
|
|
282
|
+
function str(v, fallback = "") {
|
|
283
|
+
return typeof v === "string" ? v : fallback;
|
|
284
|
+
}
|
|
285
|
+
function nullableStr(v) {
|
|
286
|
+
return typeof v === "string" ? v : null;
|
|
287
|
+
}
|
|
288
|
+
function num(v) {
|
|
289
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ../pyramid/src/operations/index.ts
|
|
293
|
+
var PROJECT_ROLES = ["admin", "pm", "member", "viewer", "guest"];
|
|
294
|
+
function projectRole(v) {
|
|
295
|
+
return PROJECT_ROLES.includes(v) ? v : "member";
|
|
296
|
+
}
|
|
297
|
+
function toProjectSummary(raw) {
|
|
298
|
+
const p = rec(raw);
|
|
299
|
+
return {
|
|
300
|
+
id: readStr(p.id) ?? "",
|
|
301
|
+
handle: readStr(p.handle) ?? "",
|
|
302
|
+
name: readStr(p.name) ?? "",
|
|
303
|
+
task_prefix: readStr(p.task_prefix) ?? "",
|
|
304
|
+
role: projectRole(p.role),
|
|
305
|
+
archived: p.archived_at != null || p.archived === true
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
var WORKSPACE_ROLES = ["owner", "admin", "member"];
|
|
309
|
+
function workspaceRole(v) {
|
|
310
|
+
return WORKSPACE_ROLES.includes(v) ? v : "member";
|
|
311
|
+
}
|
|
312
|
+
var whoami = {
|
|
313
|
+
name: "pyramid.whoami",
|
|
314
|
+
summary: "Report the authenticated user, their workspace, and accessible projects.",
|
|
315
|
+
input: z.object({}),
|
|
316
|
+
async run(_input, ctx) {
|
|
317
|
+
const [me, projectsRaw, workspaces] = await Promise.all([
|
|
318
|
+
ctx.client.api.getMe(),
|
|
319
|
+
ctx.client.api.listProjects(),
|
|
320
|
+
ctx.client.api.listWorkspaces().catch(() => [])
|
|
321
|
+
]);
|
|
322
|
+
const m = rec(me);
|
|
323
|
+
const workspaceRaw = rec(m.workspace ?? workspaces[0]);
|
|
324
|
+
return {
|
|
325
|
+
user: {
|
|
326
|
+
id: readStr(m.id) ?? "",
|
|
327
|
+
display_name: readStr(m.display_name) ?? readStr(m.email) ?? "",
|
|
328
|
+
email: readStr(m.email) ?? ""
|
|
329
|
+
},
|
|
330
|
+
workspace: {
|
|
331
|
+
id: readStr(workspaceRaw.id) ?? "",
|
|
332
|
+
handle: readStr(workspaceRaw.handle) ?? "",
|
|
333
|
+
name: readStr(workspaceRaw.name) ?? readStr(workspaceRaw.handle) ?? "",
|
|
334
|
+
role: workspaceRole(workspaceRaw.role)
|
|
335
|
+
},
|
|
336
|
+
projects: projectsRaw.map(toProjectSummary)
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
var listProjects = {
|
|
341
|
+
name: "pyramid.project.list",
|
|
342
|
+
summary: "List the projects accessible to the authenticated user.",
|
|
343
|
+
input: z.object({}),
|
|
344
|
+
async run(_input, ctx) {
|
|
345
|
+
const raw = await ctx.client.api.listProjects();
|
|
346
|
+
return raw.map(toProjectSummary);
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
var getProjectWorkflowInput = z.object({ project: z.string() });
|
|
350
|
+
var getProjectWorkflow = {
|
|
351
|
+
name: "pyramid.project.workflow",
|
|
352
|
+
summary: "Show a project's workflow: stages, statuses, labels, members, and custom-field templates.",
|
|
353
|
+
input: getProjectWorkflowInput,
|
|
354
|
+
meta: { cli: { positionals: ["project"] } },
|
|
355
|
+
async run(input, ctx) {
|
|
356
|
+
const project = await ctx.client.resolver.resolveProject(input.project);
|
|
357
|
+
return ctx.client.resolver.getWorkflow(project.id);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
var listMyTasksInput = z.object({
|
|
361
|
+
limit: z.number().int().min(1).max(50).optional(),
|
|
362
|
+
cursor: z.string().optional()
|
|
363
|
+
});
|
|
364
|
+
var DEFAULT_LIMIT = 25;
|
|
365
|
+
var listMyTasks = {
|
|
366
|
+
name: "pyramid.task.list-mine",
|
|
367
|
+
summary: "List tasks you own or review, newest first, across projects. Returns one page with a cursor.",
|
|
368
|
+
input: listMyTasksInput,
|
|
369
|
+
meta: { cli: { verbAliases: ["next"] } },
|
|
370
|
+
async run(input, ctx) {
|
|
371
|
+
const limit = input.limit ?? DEFAULT_LIMIT;
|
|
372
|
+
const page = await ctx.client.api.listMyTasks({
|
|
373
|
+
limit,
|
|
374
|
+
cursor: input.cursor
|
|
375
|
+
});
|
|
376
|
+
const items = [];
|
|
377
|
+
const wfCache = /* @__PURE__ */ new Map();
|
|
378
|
+
for (const raw of page.data) {
|
|
379
|
+
const projectId = readStr(rec(raw).project_id);
|
|
380
|
+
let wf;
|
|
381
|
+
if (projectId) {
|
|
382
|
+
wf = wfCache.get(projectId);
|
|
383
|
+
if (!wf) {
|
|
384
|
+
wf = await ctx.client.resolver.getWorkflow(projectId);
|
|
385
|
+
wfCache.set(projectId, wf);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
items.push(hydrateTaskSummary(raw, wf ?? EMPTY_WORKFLOW));
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
items,
|
|
392
|
+
next_cursor: page.cursor,
|
|
393
|
+
has_more: page.cursor !== null
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
var EMPTY_WORKFLOW = {
|
|
398
|
+
project: { id: "", handle: "", name: "", task_prefix: "", role: "member", archived: false },
|
|
399
|
+
stages: [],
|
|
400
|
+
statuses: [],
|
|
401
|
+
labels: [],
|
|
402
|
+
members: [],
|
|
403
|
+
templates: []
|
|
404
|
+
};
|
|
405
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
406
|
+
var TASK_KEY_RE = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
|
|
407
|
+
async function resolveTaskRef(ctx, ref) {
|
|
408
|
+
const trimmed = ref.trim();
|
|
409
|
+
if (UUID_RE.test(trimmed)) {
|
|
410
|
+
const raw = rec(await ctx.client.api.getTask(trimmed));
|
|
411
|
+
return {
|
|
412
|
+
id: readStr(raw.id) ?? trimmed,
|
|
413
|
+
projectId: readStr(raw.project_id) ?? "",
|
|
414
|
+
key: readStr(raw.key) ?? ""
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
if (TASK_KEY_RE.test(trimmed)) {
|
|
418
|
+
const page = await ctx.client.api.searchTasks({ q: trimmed, limit: 50 });
|
|
419
|
+
const needle = trimmed.toLowerCase();
|
|
420
|
+
const hit = page.data.map(rec).find((r) => (readStr(r.key) ?? "").toLowerCase() === needle);
|
|
421
|
+
if (hit) {
|
|
422
|
+
return {
|
|
423
|
+
id: readStr(hit.id) ?? "",
|
|
424
|
+
projectId: readStr(hit.project_id) ?? "",
|
|
425
|
+
key: readStr(hit.key) ?? trimmed
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
throw pyramidError("task_not_found", `No task matches "${ref}".`);
|
|
429
|
+
}
|
|
430
|
+
throw pyramidError("task_not_found", `"${ref}" is neither a task key (e.g. WEB-42) nor a UUID.`);
|
|
431
|
+
}
|
|
432
|
+
async function hydrateDetailFor(ctx, raw, projectId) {
|
|
433
|
+
const wf = projectId ? await ctx.client.resolver.getWorkflow(projectId) : EMPTY_WORKFLOW;
|
|
434
|
+
return hydrateTaskDetail(raw, wf);
|
|
435
|
+
}
|
|
436
|
+
var customFieldSchema = z.object({
|
|
437
|
+
field: z.string(),
|
|
438
|
+
value: z.unknown()
|
|
439
|
+
});
|
|
440
|
+
async function resolveStatusWithStage(ctx, projectId, stage, status) {
|
|
441
|
+
if (status !== void 0) {
|
|
442
|
+
const resolved = await ctx.client.resolver.resolveStatus(projectId, status);
|
|
443
|
+
if (stage !== void 0) {
|
|
444
|
+
const resolvedStage = await ctx.client.resolver.resolveStage(projectId, stage);
|
|
445
|
+
if (resolved.stage_id !== resolvedStage.id) {
|
|
446
|
+
throw pyramidError("status_not_in_stage", `Status "${status}" is not in stage "${stage}".`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return { id: resolved.id, stage_id: resolved.stage_id };
|
|
450
|
+
}
|
|
451
|
+
if (stage !== void 0) {
|
|
452
|
+
const resolved = await ctx.client.resolver.resolveStatus(projectId, stage);
|
|
453
|
+
return { id: resolved.id, stage_id: resolved.stage_id };
|
|
454
|
+
}
|
|
455
|
+
return void 0;
|
|
456
|
+
}
|
|
457
|
+
async function resolveLabelIds(ctx, projectId, labels) {
|
|
458
|
+
if (labels === void 0) return void 0;
|
|
459
|
+
const ids = [];
|
|
460
|
+
for (const label of labels) {
|
|
461
|
+
const resolved = await ctx.client.resolver.resolveLabel(projectId, label);
|
|
462
|
+
ids.push(resolved.id);
|
|
463
|
+
}
|
|
464
|
+
return ids;
|
|
465
|
+
}
|
|
466
|
+
async function resolveMentionIds(ctx, projectId, mentions) {
|
|
467
|
+
if (mentions === void 0) return void 0;
|
|
468
|
+
const ids = [];
|
|
469
|
+
for (const mention of mentions) {
|
|
470
|
+
const resolved = await ctx.client.resolver.resolveUser(projectId, mention);
|
|
471
|
+
ids.push(resolved.id);
|
|
472
|
+
}
|
|
473
|
+
return ids;
|
|
474
|
+
}
|
|
475
|
+
async function buildCreateBody(ctx, projectId, row) {
|
|
476
|
+
const body = { title: row.title };
|
|
477
|
+
if (row.description !== void 0) body.description = row.description;
|
|
478
|
+
if (row.priority !== void 0) body.priority = row.priority;
|
|
479
|
+
if (row.due_date !== void 0) body.due_date = row.due_date;
|
|
480
|
+
if (row.estimate_hours !== void 0) body.estimate = row.estimate_hours;
|
|
481
|
+
if (row.guest_visible !== void 0) body.guest_visible = row.guest_visible;
|
|
482
|
+
const resolvedStatus = await resolveStatusWithStage(ctx, projectId, row.stage, row.status);
|
|
483
|
+
if (resolvedStatus !== void 0) body.status_id = resolvedStatus.id;
|
|
484
|
+
if (row.owner !== void 0) {
|
|
485
|
+
body.owner_id = (await ctx.client.resolver.resolveUser(projectId, row.owner)).id;
|
|
486
|
+
}
|
|
487
|
+
if (row.reviewer !== void 0) {
|
|
488
|
+
body.reviewer_id = (await ctx.client.resolver.resolveUser(projectId, row.reviewer)).id;
|
|
489
|
+
}
|
|
490
|
+
const labelIds = await resolveLabelIds(ctx, projectId, row.labels);
|
|
491
|
+
if (labelIds !== void 0) body.label_ids = labelIds;
|
|
492
|
+
if (row.custom_fields !== void 0) {
|
|
493
|
+
body.field_values = await resolveFieldValues(ctx, projectId, row.custom_fields);
|
|
494
|
+
}
|
|
495
|
+
return body;
|
|
496
|
+
}
|
|
497
|
+
async function resolveFieldValues(ctx, projectId, fields) {
|
|
498
|
+
const wf = await ctx.client.resolver.getWorkflow(projectId);
|
|
499
|
+
const out = {};
|
|
500
|
+
for (const cf of fields) {
|
|
501
|
+
const needle = cf.field.trim().toLowerCase();
|
|
502
|
+
let def;
|
|
503
|
+
for (const t of wf.templates) {
|
|
504
|
+
def = t.fields.find(
|
|
505
|
+
(f) => f.id === cf.field || f.key.toLowerCase() === needle || f.name.toLowerCase() === needle
|
|
506
|
+
);
|
|
507
|
+
if (def) break;
|
|
508
|
+
}
|
|
509
|
+
if (def === void 0) {
|
|
510
|
+
throw pyramidError("field_not_found", `No custom field matches "${cf.field}".`);
|
|
511
|
+
}
|
|
512
|
+
if (def.field_type === "user" && typeof cf.value === "string") {
|
|
513
|
+
out[def.id] = (await ctx.client.resolver.resolveUser(projectId, cf.value)).id;
|
|
514
|
+
} else {
|
|
515
|
+
validateFieldValue(def, cf.value);
|
|
516
|
+
out[def.id] = cf.value;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return out;
|
|
520
|
+
}
|
|
521
|
+
function validateFieldValue(def, value) {
|
|
522
|
+
const fail = (why) => {
|
|
523
|
+
throw pyramidError("invalid_field_value", `Custom field "${def.name}" ${why}.`);
|
|
524
|
+
};
|
|
525
|
+
switch (def.field_type) {
|
|
526
|
+
case "number":
|
|
527
|
+
if (typeof value !== "number") fail("expects a number");
|
|
528
|
+
break;
|
|
529
|
+
case "checkbox":
|
|
530
|
+
if (typeof value !== "boolean") fail("expects a boolean");
|
|
531
|
+
break;
|
|
532
|
+
case "date":
|
|
533
|
+
if (typeof value !== "string") fail("expects a date string (YYYY-MM-DD)");
|
|
534
|
+
break;
|
|
535
|
+
case "text":
|
|
536
|
+
if (typeof value !== "string") fail("expects a string");
|
|
537
|
+
break;
|
|
538
|
+
case "select":
|
|
539
|
+
if (typeof value !== "string") fail("expects a single option string");
|
|
540
|
+
if (def.options && !def.options.includes(value)) {
|
|
541
|
+
fail(`must be one of: ${def.options.join(", ")}`);
|
|
542
|
+
}
|
|
543
|
+
break;
|
|
544
|
+
case "multiselect":
|
|
545
|
+
if (!Array.isArray(value)) fail("expects an array of option strings");
|
|
546
|
+
if (def.options) {
|
|
547
|
+
for (const v of value) {
|
|
548
|
+
if (typeof v !== "string" || !def.options.includes(v)) {
|
|
549
|
+
fail(`values must each be one of: ${def.options.join(", ")}`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
var taskListInput = z.object({
|
|
557
|
+
project: z.string(),
|
|
558
|
+
status: z.string().optional(),
|
|
559
|
+
stage: z.string().optional(),
|
|
560
|
+
// Owner and Reviewer — the words the product uses. There is no "assignee".
|
|
561
|
+
owner: z.string().optional(),
|
|
562
|
+
reviewer: z.string().optional(),
|
|
563
|
+
label: z.string().optional(),
|
|
564
|
+
query: z.string().optional(),
|
|
565
|
+
archived: z.boolean().optional(),
|
|
566
|
+
limit: z.number().int().min(1).max(50).optional(),
|
|
567
|
+
cursor: z.string().optional()
|
|
568
|
+
});
|
|
569
|
+
var taskList = {
|
|
570
|
+
name: "pyramid.task.list",
|
|
571
|
+
summary: "List a project's tasks, filtered by status/stage/owner/reviewer/label/query. Returns one page with a cursor.",
|
|
572
|
+
input: taskListInput,
|
|
573
|
+
meta: {
|
|
574
|
+
cli: { positionals: ["project"], routes: { mine: "pyramid.task.list-mine" } }
|
|
575
|
+
},
|
|
576
|
+
async run(input, ctx) {
|
|
577
|
+
const project = await ctx.client.resolver.resolveProject(input.project);
|
|
578
|
+
const archived = input.archived ?? false;
|
|
579
|
+
let page;
|
|
580
|
+
if (archived) {
|
|
581
|
+
page = await ctx.client.api.listArchived(project.id, {
|
|
582
|
+
limit: input.limit ?? DEFAULT_LIMIT,
|
|
583
|
+
cursor: input.cursor
|
|
584
|
+
});
|
|
585
|
+
} else {
|
|
586
|
+
const statusId = input.status !== void 0 ? (await ctx.client.resolver.resolveStatus(project.id, input.status)).id : void 0;
|
|
587
|
+
const stageId = input.stage !== void 0 ? (await ctx.client.resolver.resolveStage(project.id, input.stage)).id : void 0;
|
|
588
|
+
const ownerId = input.owner !== void 0 ? (await ctx.client.resolver.resolveUser(project.id, input.owner)).id : void 0;
|
|
589
|
+
const reviewerId = input.reviewer !== void 0 ? (await ctx.client.resolver.resolveUser(project.id, input.reviewer)).id : void 0;
|
|
590
|
+
const labelId = input.label !== void 0 ? (await ctx.client.resolver.resolveLabel(project.id, input.label)).id : void 0;
|
|
591
|
+
page = await ctx.client.api.listTasks(project.id, {
|
|
592
|
+
status: statusId,
|
|
593
|
+
stage_id: stageId,
|
|
594
|
+
owner_id: ownerId,
|
|
595
|
+
reviewer_id: reviewerId,
|
|
596
|
+
label_id: labelId,
|
|
597
|
+
q: input.query,
|
|
598
|
+
limit: input.limit ?? DEFAULT_LIMIT,
|
|
599
|
+
cursor: input.cursor
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
const wf = await ctx.client.resolver.getWorkflow(project.id);
|
|
603
|
+
const items = page.data.map((raw) => hydrateTaskSummary(raw, wf)).filter((t) => archived || !t.archived);
|
|
604
|
+
return {
|
|
605
|
+
items,
|
|
606
|
+
next_cursor: page.cursor,
|
|
607
|
+
has_more: page.cursor !== null
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
var taskShowInput = z.object({
|
|
612
|
+
task: z.string(),
|
|
613
|
+
// The backend's only drill-in is `?expand`, which inlines the related owner,
|
|
614
|
+
// reviewer, and label rows (DOC-BACKEND-CONTRACT). It does NOT expose
|
|
615
|
+
// timeline / comments / attachments — those are separate tools (e.g. list_comments).
|
|
616
|
+
expand: z.boolean().optional()
|
|
617
|
+
});
|
|
618
|
+
var taskShow = {
|
|
619
|
+
name: "pyramid.task.get",
|
|
620
|
+
summary: "Show one task's full detail by key (WEB-42) or UUID; `expand` inlines owner/reviewer/labels.",
|
|
621
|
+
input: taskShowInput,
|
|
622
|
+
meta: { cli: { positionals: ["task"], verbAliases: ["show"] } },
|
|
623
|
+
async run(input, ctx) {
|
|
624
|
+
const ref = await resolveTaskRef(ctx, input.task);
|
|
625
|
+
const raw = await ctx.client.api.getTask(
|
|
626
|
+
ref.id,
|
|
627
|
+
input.expand ? "owner,reviewer,labels" : void 0
|
|
628
|
+
);
|
|
629
|
+
const projectId = readStr(rec(raw).project_id) ?? ref.projectId;
|
|
630
|
+
return hydrateDetailFor(ctx, raw, projectId);
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
var priorityEnum = z.enum(["none", "low", "medium", "high", "urgent"]);
|
|
634
|
+
var taskCreateInput = z.object({
|
|
635
|
+
project: z.string(),
|
|
636
|
+
title: z.string(),
|
|
637
|
+
description: z.string().optional(),
|
|
638
|
+
stage: z.string().optional(),
|
|
639
|
+
status: z.string().optional(),
|
|
640
|
+
owner: z.string().optional(),
|
|
641
|
+
reviewer: z.string().optional(),
|
|
642
|
+
labels: z.array(z.string()).optional(),
|
|
643
|
+
priority: priorityEnum.optional(),
|
|
644
|
+
due_date: z.string().optional(),
|
|
645
|
+
estimate_hours: z.number().optional(),
|
|
646
|
+
guest_visible: z.boolean().optional(),
|
|
647
|
+
custom_fields: z.array(customFieldSchema).optional()
|
|
648
|
+
});
|
|
649
|
+
var taskCreate = {
|
|
650
|
+
name: "pyramid.task.create",
|
|
651
|
+
summary: "Create a task in a project. Accepts names/keys for stage/status/owner/reviewer/labels.",
|
|
652
|
+
input: taskCreateInput,
|
|
653
|
+
meta: { cli: { positionals: ["title"] } },
|
|
654
|
+
async run(input, ctx) {
|
|
655
|
+
const project = await ctx.client.resolver.resolveProject(input.project);
|
|
656
|
+
const body = await buildCreateBody(ctx, project.id, input);
|
|
657
|
+
const raw = await ctx.client.api.createTask(project.id, body);
|
|
658
|
+
return hydrateDetailFor(ctx, raw, project.id);
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
var taskBulkCreateInput = z.object({
|
|
662
|
+
project: z.string(),
|
|
663
|
+
template: z.string().optional(),
|
|
664
|
+
defaults: z.object({
|
|
665
|
+
stage: z.string().optional(),
|
|
666
|
+
status: z.string().optional(),
|
|
667
|
+
labels: z.array(z.string()).optional()
|
|
668
|
+
}).optional(),
|
|
669
|
+
tasks: z.array(
|
|
670
|
+
z.object({
|
|
671
|
+
title: z.string(),
|
|
672
|
+
description: z.string().optional(),
|
|
673
|
+
stage: z.string().optional(),
|
|
674
|
+
status: z.string().optional(),
|
|
675
|
+
owner: z.string().optional(),
|
|
676
|
+
reviewer: z.string().optional(),
|
|
677
|
+
labels: z.array(z.string()).optional(),
|
|
678
|
+
priority: priorityEnum.optional(),
|
|
679
|
+
due_date: z.string().optional(),
|
|
680
|
+
estimate_hours: z.number().optional(),
|
|
681
|
+
guest_visible: z.boolean().optional(),
|
|
682
|
+
custom_fields: z.array(customFieldSchema).optional()
|
|
683
|
+
})
|
|
684
|
+
).min(1).max(100)
|
|
685
|
+
});
|
|
686
|
+
var taskBulkCreate = {
|
|
687
|
+
name: "pyramid.task.create-bulk",
|
|
688
|
+
summary: "Bulk-create up to 100 tasks in a project from a required template (returns the created tasks).",
|
|
689
|
+
input: taskBulkCreateInput,
|
|
690
|
+
// CLI surfaces bulk-create on `task create` via a special-cased `--file`.
|
|
691
|
+
meta: { cli: { positionals: ["project"], verbAliases: ["bulk"], fileInput: "tasks" } },
|
|
692
|
+
async run(input, ctx) {
|
|
693
|
+
const project = await ctx.client.resolver.resolveProject(input.project);
|
|
694
|
+
const defaults = input.defaults ?? {};
|
|
695
|
+
if (input.template === void 0) {
|
|
696
|
+
throw pyramidError(
|
|
697
|
+
"validation_failed",
|
|
698
|
+
"Bulk create requires a `template` (the task-schema template to instantiate)."
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
const templateId = await resolveTemplateId(ctx, project.id, input.template);
|
|
702
|
+
const rows = [];
|
|
703
|
+
for (const row of input.tasks) {
|
|
704
|
+
rows.push(
|
|
705
|
+
await buildCreateBody(ctx, project.id, {
|
|
706
|
+
...row,
|
|
707
|
+
stage: row.stage ?? defaults.stage,
|
|
708
|
+
status: row.status ?? defaults.status,
|
|
709
|
+
labels: row.labels ?? defaults.labels
|
|
710
|
+
})
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
const raw = rec(
|
|
714
|
+
await ctx.client.api.bulkCreate({
|
|
715
|
+
project_id: project.id,
|
|
716
|
+
template_id: templateId,
|
|
717
|
+
tasks: rows
|
|
718
|
+
})
|
|
719
|
+
);
|
|
720
|
+
const created = Array.isArray(raw.created) ? raw.created : [];
|
|
721
|
+
const wf = await ctx.client.resolver.getWorkflow(project.id);
|
|
722
|
+
return created.map((r) => hydrateTaskDetail(r, wf));
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
async function resolveTemplateId(ctx, projectId, template) {
|
|
726
|
+
const wf = await ctx.client.resolver.getWorkflow(projectId);
|
|
727
|
+
const needle = template.trim().toLowerCase();
|
|
728
|
+
const match = wf.templates.find((t) => t.id === template || t.name.toLowerCase() === needle);
|
|
729
|
+
if (match) return match.id;
|
|
730
|
+
throw pyramidError("validation_failed", `No task template matches "${template}".`, {
|
|
731
|
+
candidates: wf.templates.map((t) => t.name)
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
var taskUpdateInput = z.object({
|
|
735
|
+
task: z.string(),
|
|
736
|
+
title: z.string().optional(),
|
|
737
|
+
description: z.string().nullable().optional(),
|
|
738
|
+
priority: z.string().optional(),
|
|
739
|
+
due_date: z.string().nullable().optional(),
|
|
740
|
+
start_date: z.string().nullable().optional(),
|
|
741
|
+
estimate: z.number().optional(),
|
|
742
|
+
guest_visible: z.boolean().optional(),
|
|
743
|
+
owner: z.string().nullable().optional(),
|
|
744
|
+
reviewer: z.string().nullable().optional(),
|
|
745
|
+
add_labels: z.array(z.string()).optional(),
|
|
746
|
+
remove_labels: z.array(z.string()).optional(),
|
|
747
|
+
custom_fields: z.array(customFieldSchema).optional()
|
|
748
|
+
});
|
|
749
|
+
var taskUpdate = {
|
|
750
|
+
name: "pyramid.task.update",
|
|
751
|
+
summary: "Update a task, including its owner/reviewer (pass null to clear). Labels and custom fields fan out to their endpoints.",
|
|
752
|
+
input: taskUpdateInput,
|
|
753
|
+
meta: { cli: { positionals: ["task"] } },
|
|
754
|
+
async run(input, ctx) {
|
|
755
|
+
const ref = await resolveTaskRef(ctx, input.task);
|
|
756
|
+
const projectId = ref.projectId;
|
|
757
|
+
const patch = {};
|
|
758
|
+
if (input.title !== void 0) patch.title = input.title;
|
|
759
|
+
if (input.description !== void 0) patch.description = input.description;
|
|
760
|
+
if (input.priority !== void 0) patch.priority = input.priority;
|
|
761
|
+
if (input.due_date !== void 0) patch.due_date = input.due_date;
|
|
762
|
+
if (input.start_date !== void 0) patch.start_date = input.start_date;
|
|
763
|
+
if (input.estimate !== void 0) patch.estimate = input.estimate;
|
|
764
|
+
if (input.guest_visible !== void 0) patch.guest_visible = input.guest_visible;
|
|
765
|
+
if (input.owner !== void 0) {
|
|
766
|
+
patch.owner_id = input.owner === null ? null : (await ctx.client.resolver.resolveUser(projectId, input.owner)).id;
|
|
767
|
+
}
|
|
768
|
+
if (input.reviewer !== void 0) {
|
|
769
|
+
patch.reviewer_id = input.reviewer === null ? null : (await ctx.client.resolver.resolveUser(projectId, input.reviewer)).id;
|
|
770
|
+
}
|
|
771
|
+
if (Object.keys(patch).length > 0) {
|
|
772
|
+
await failingSubUpdate("content", () => ctx.client.api.updateTask(ref.id, patch));
|
|
773
|
+
}
|
|
774
|
+
const addLabelIds = await resolveLabelIds(ctx, projectId, input.add_labels);
|
|
775
|
+
for (const labelId of addLabelIds ?? []) {
|
|
776
|
+
await failingSubUpdate("add_label", () => ctx.client.api.addTaskLabel(ref.id, labelId));
|
|
777
|
+
}
|
|
778
|
+
const removeLabelIds = await resolveLabelIds(ctx, projectId, input.remove_labels);
|
|
779
|
+
for (const labelId of removeLabelIds ?? []) {
|
|
780
|
+
await failingSubUpdate("remove_label", () => ctx.client.api.removeTaskLabel(ref.id, labelId));
|
|
781
|
+
}
|
|
782
|
+
if (input.custom_fields !== void 0) {
|
|
783
|
+
const fieldValues = await resolveFieldValues(ctx, projectId, input.custom_fields);
|
|
784
|
+
await failingSubUpdate(
|
|
785
|
+
"field_values",
|
|
786
|
+
() => ctx.client.api.setFieldValues(ref.id, { field_values: fieldValues })
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
const raw = await ctx.client.api.getTask(ref.id);
|
|
790
|
+
return hydrateDetailFor(ctx, raw, projectId);
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
async function failingSubUpdate(which, fn) {
|
|
794
|
+
try {
|
|
795
|
+
return await fn();
|
|
796
|
+
} catch (err) {
|
|
797
|
+
if (err instanceof MagicError) {
|
|
798
|
+
throw new MagicError(err.code, `update_task ${which} failed: ${err.message}`, {
|
|
799
|
+
product: err.product,
|
|
800
|
+
...err.hint !== void 0 ? { hint: err.hint } : {},
|
|
801
|
+
...err.candidates !== void 0 ? { candidates: err.candidates } : {},
|
|
802
|
+
...err.details !== void 0 ? { details: err.details } : {}
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
throw err;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
var taskMoveInput = z.object({
|
|
809
|
+
task: z.string(),
|
|
810
|
+
status: z.string(),
|
|
811
|
+
after_task: z.string().optional(),
|
|
812
|
+
before_task: z.string().optional()
|
|
813
|
+
});
|
|
814
|
+
var taskMove = {
|
|
815
|
+
name: "pyramid.task.move",
|
|
816
|
+
summary: "Move a task to a target status (carries its stage), optionally positioned by neighbor key.",
|
|
817
|
+
input: taskMoveInput,
|
|
818
|
+
meta: { cli: { positionals: ["task"] } },
|
|
819
|
+
async run(input, ctx) {
|
|
820
|
+
if (input.after_task !== void 0 && input.before_task !== void 0) {
|
|
821
|
+
throw pyramidError("validation_failed", "Pass at most one of after_task / before_task.");
|
|
822
|
+
}
|
|
823
|
+
const ref = await resolveTaskRef(ctx, input.task);
|
|
824
|
+
const status = await ctx.client.resolver.resolveStatus(ref.projectId, input.status);
|
|
825
|
+
const body = { status_id: status.id };
|
|
826
|
+
if (input.after_task !== void 0) {
|
|
827
|
+
body.after_id = (await resolveTaskRef(ctx, input.after_task)).id;
|
|
828
|
+
}
|
|
829
|
+
if (input.before_task !== void 0) {
|
|
830
|
+
body.before_id = (await resolveTaskRef(ctx, input.before_task)).id;
|
|
831
|
+
}
|
|
832
|
+
const raw = rec(await ctx.client.api.moveTask(ref.id, body));
|
|
833
|
+
return hydrateDetailFor(ctx, raw.task, ref.projectId);
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
var taskArchiveInput = z.object({
|
|
837
|
+
task: z.string(),
|
|
838
|
+
// The CLI archives by default; `--unarchive` sets false.
|
|
839
|
+
archived: z.boolean().default(true)
|
|
840
|
+
});
|
|
841
|
+
var taskArchive = {
|
|
842
|
+
name: "pyramid.task.archive",
|
|
843
|
+
summary: "Soft-archive (or unarchive) a task. Reversible \u2014 not destructive-gated.",
|
|
844
|
+
input: taskArchiveInput,
|
|
845
|
+
meta: { cli: { positionals: ["task"], presets: { unarchive: { archived: false } } } },
|
|
846
|
+
async run(input, ctx) {
|
|
847
|
+
const ref = await resolveTaskRef(ctx, input.task);
|
|
848
|
+
const raw = input.archived ? await ctx.client.api.archiveTask(ref.id) : await ctx.client.api.unarchiveTask(ref.id);
|
|
849
|
+
return hydrateDetailFor(ctx, raw, ref.projectId);
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
var taskDeleteInput = z.object({ task: z.string() });
|
|
853
|
+
var taskDelete = {
|
|
854
|
+
name: "pyramid.task.delete",
|
|
855
|
+
summary: "Hard-delete a task. Requires MAGIC_ALLOW_DESTRUCTIVE=1 (otherwise destructive_action_disabled).",
|
|
856
|
+
input: taskDeleteInput,
|
|
857
|
+
meta: {
|
|
858
|
+
destructive: true,
|
|
859
|
+
cli: { positionals: ["task"] }
|
|
860
|
+
},
|
|
861
|
+
// meta.destructive is the gate: the kit's CLI runner and MCP callTool enforce it.
|
|
862
|
+
async run(input, ctx) {
|
|
863
|
+
const ref = await resolveTaskRef(ctx, input.task);
|
|
864
|
+
await ctx.client.api.deleteTask(ref.id, true);
|
|
865
|
+
return { id: ref.id, key: ref.key, deleted: true };
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
var commentAddInput = z.object({
|
|
869
|
+
task: z.string(),
|
|
870
|
+
content: z.string(),
|
|
871
|
+
stage: z.string().optional(),
|
|
872
|
+
mentions: z.array(z.string()).optional()
|
|
873
|
+
});
|
|
874
|
+
var commentAdd = {
|
|
875
|
+
name: "pyramid.task.comment",
|
|
876
|
+
summary: "Add a stage-scoped comment to a task (defaults to the task's current stage).",
|
|
877
|
+
input: commentAddInput,
|
|
878
|
+
meta: {
|
|
879
|
+
cli: { positionals: ["task", "content"] }
|
|
880
|
+
},
|
|
881
|
+
async run(input, ctx) {
|
|
882
|
+
const ref = await resolveTaskRef(ctx, input.task);
|
|
883
|
+
const projectId = ref.projectId;
|
|
884
|
+
let stageId;
|
|
885
|
+
if (input.stage !== void 0) {
|
|
886
|
+
stageId = (await ctx.client.resolver.resolveStage(projectId, input.stage)).id;
|
|
887
|
+
} else {
|
|
888
|
+
stageId = await currentStageId(ctx, ref);
|
|
889
|
+
}
|
|
890
|
+
const mentionIds = await resolveMentionIds(ctx, projectId, input.mentions);
|
|
891
|
+
const body = { content: input.content };
|
|
892
|
+
if (stageId !== void 0) body.stage_id = stageId;
|
|
893
|
+
if (mentionIds !== void 0) body.mention_user_ids = mentionIds;
|
|
894
|
+
const raw = await ctx.client.api.addComment(ref.id, body);
|
|
895
|
+
const wf = projectId ? await ctx.client.resolver.getWorkflow(projectId) : EMPTY_WORKFLOW;
|
|
896
|
+
return hydrateComment(raw, wf);
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
async function currentStageId(ctx, ref) {
|
|
900
|
+
if (!ref.projectId) return void 0;
|
|
901
|
+
const raw = rec(await ctx.client.api.getTask(ref.id));
|
|
902
|
+
const statusId = readStr(raw.status_id);
|
|
903
|
+
if (statusId) {
|
|
904
|
+
const wf = await ctx.client.resolver.getWorkflow(ref.projectId);
|
|
905
|
+
const status = wf.statuses.find((s) => s.id === statusId);
|
|
906
|
+
if (status) return status.stage_id;
|
|
907
|
+
}
|
|
908
|
+
return readStr(raw.stage_id);
|
|
909
|
+
}
|
|
910
|
+
var commentReplyInput = z.object({
|
|
911
|
+
comment_id: z.string(),
|
|
912
|
+
content: z.string(),
|
|
913
|
+
mentions: z.array(z.string()).optional()
|
|
914
|
+
});
|
|
915
|
+
var commentReply = {
|
|
916
|
+
name: "pyramid.comment.reply",
|
|
917
|
+
summary: "Reply to a ROOT comment (one level only; replying to a reply -> reply_depth_exceeded).",
|
|
918
|
+
input: commentReplyInput,
|
|
919
|
+
meta: {
|
|
920
|
+
cli: { positionals: ["comment_id", "content"] }
|
|
921
|
+
},
|
|
922
|
+
async run(input, ctx) {
|
|
923
|
+
const target = rec(await ctx.client.api.getComment(input.comment_id));
|
|
924
|
+
const parentId = readStr(target.parent_id);
|
|
925
|
+
const threadRootId = readStr(target.thread_root_id);
|
|
926
|
+
const selfId = readStr(target.id);
|
|
927
|
+
if (parentId !== void 0 || threadRootId !== void 0 && threadRootId !== selfId) {
|
|
928
|
+
throw pyramidError(
|
|
929
|
+
"reply_depth_exceeded",
|
|
930
|
+
"Replies are one level deep \u2014 reply to the root comment instead."
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
const taskId = readStr(target.task_id);
|
|
934
|
+
const projectId = taskId ? (await resolveTaskRef(ctx, taskId)).projectId : "";
|
|
935
|
+
const mentionIds = await resolveMentionIds(ctx, projectId, input.mentions);
|
|
936
|
+
const body = { content: input.content };
|
|
937
|
+
if (mentionIds !== void 0) body.mention_user_ids = mentionIds;
|
|
938
|
+
const raw = await ctx.client.api.replyComment(input.comment_id, body);
|
|
939
|
+
const wf = projectId ? await ctx.client.resolver.getWorkflow(projectId) : EMPTY_WORKFLOW;
|
|
940
|
+
return hydrateComment(raw, wf);
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
var searchTasksInput = z.object({
|
|
944
|
+
query: z.string(),
|
|
945
|
+
limit: z.number().int().min(1).max(50).optional()
|
|
946
|
+
});
|
|
947
|
+
var searchTasks = {
|
|
948
|
+
name: "pyramid.task.search",
|
|
949
|
+
summary: "Full-text search tasks across the workspace by title/key/content. Returns one page.",
|
|
950
|
+
input: searchTasksInput,
|
|
951
|
+
meta: { cli: { positionals: ["query"] } },
|
|
952
|
+
async run(input, ctx) {
|
|
953
|
+
const page = await ctx.client.api.searchTasks({
|
|
954
|
+
q: input.query,
|
|
955
|
+
limit: input.limit ?? DEFAULT_LIMIT
|
|
956
|
+
});
|
|
957
|
+
const wfCache = /* @__PURE__ */ new Map();
|
|
958
|
+
const items = [];
|
|
959
|
+
for (const raw of page.data) {
|
|
960
|
+
const pid = readStr(rec(raw).project_id) ?? "";
|
|
961
|
+
let wf = wfCache.get(pid);
|
|
962
|
+
if (wf === void 0) {
|
|
963
|
+
wf = pid ? await ctx.client.resolver.getWorkflow(pid) : EMPTY_WORKFLOW;
|
|
964
|
+
wfCache.set(pid, wf);
|
|
965
|
+
}
|
|
966
|
+
items.push(hydrateTaskSummary(raw, wf));
|
|
967
|
+
}
|
|
968
|
+
return { items, next_cursor: page.cursor, has_more: page.cursor !== null };
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
var listCommentsInput = z.object({
|
|
972
|
+
task: z.string(),
|
|
973
|
+
// Stage-scoped (r4): defaults to the task's CURRENT stage; pass "all" to list
|
|
974
|
+
// comments across every stage, or a stage name/key to scope to that stage.
|
|
975
|
+
stage: z.string().optional(),
|
|
976
|
+
limit: z.number().int().min(1).max(50).optional(),
|
|
977
|
+
cursor: z.string().optional()
|
|
978
|
+
});
|
|
979
|
+
var listComments = {
|
|
980
|
+
name: "pyramid.comment.list",
|
|
981
|
+
summary: "List a task's comments, oldest first. Defaults to the task's current stage; `stage: \"all\"` lists every stage.",
|
|
982
|
+
input: listCommentsInput,
|
|
983
|
+
meta: { cli: { positionals: ["task"] } },
|
|
984
|
+
async run(input, ctx) {
|
|
985
|
+
const ref = await resolveTaskRef(ctx, input.task);
|
|
986
|
+
let stageId;
|
|
987
|
+
if (input.stage === void 0) {
|
|
988
|
+
stageId = await currentStageId(ctx, ref);
|
|
989
|
+
} else if (input.stage.trim().toLowerCase() === "all") {
|
|
990
|
+
stageId = void 0;
|
|
991
|
+
} else {
|
|
992
|
+
stageId = (await ctx.client.resolver.resolveStage(ref.projectId, input.stage)).id;
|
|
993
|
+
}
|
|
994
|
+
const page = await ctx.client.api.listComments(ref.id, {
|
|
995
|
+
stage_id: stageId,
|
|
996
|
+
limit: input.limit ?? DEFAULT_LIMIT,
|
|
997
|
+
cursor: input.cursor
|
|
998
|
+
});
|
|
999
|
+
const wf = ref.projectId ? await ctx.client.resolver.getWorkflow(ref.projectId) : EMPTY_WORKFLOW;
|
|
1000
|
+
const items = page.data.map((raw) => hydrateComment(raw, wf));
|
|
1001
|
+
return { items, next_cursor: page.cursor, has_more: page.cursor !== null };
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
var TIMELINE_DEFAULT_LIMIT = 50;
|
|
1005
|
+
var TIMELINE_MAX_LIMIT = 200;
|
|
1006
|
+
var taskTimelineInput = z.object({
|
|
1007
|
+
task: z.string(),
|
|
1008
|
+
/** Narrow to one kind, e.g. "owner_changed" / "status_changed". */
|
|
1009
|
+
event_type: z.string().optional(),
|
|
1010
|
+
limit: z.number().int().min(1).max(TIMELINE_MAX_LIMIT).optional(),
|
|
1011
|
+
cursor: z.string().optional()
|
|
1012
|
+
});
|
|
1013
|
+
var taskTimeline = {
|
|
1014
|
+
name: "pyramid.task.timeline",
|
|
1015
|
+
summary: "A task's history (who changed what, when), OLDEST first. Optionally filter to one event_type, e.g. owner_changed.",
|
|
1016
|
+
input: taskTimelineInput,
|
|
1017
|
+
meta: { cli: { positionals: ["task"] } },
|
|
1018
|
+
async run(input, ctx) {
|
|
1019
|
+
const ref = await resolveTaskRef(ctx, input.task);
|
|
1020
|
+
const page = await ctx.client.api.getTaskTimeline(ref.id, {
|
|
1021
|
+
limit: input.limit ?? TIMELINE_DEFAULT_LIMIT,
|
|
1022
|
+
cursor: input.cursor,
|
|
1023
|
+
event_type: input.event_type
|
|
1024
|
+
});
|
|
1025
|
+
const wf = ref.projectId ? await ctx.client.resolver.getWorkflow(ref.projectId) : EMPTY_WORKFLOW;
|
|
1026
|
+
const items = page.data.map((raw) => hydrateTimelineEvent(raw, wf));
|
|
1027
|
+
return { items, next_cursor: page.cursor, has_more: page.cursor !== null };
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
var operations = [
|
|
1031
|
+
whoami,
|
|
1032
|
+
listProjects,
|
|
1033
|
+
getProjectWorkflow,
|
|
1034
|
+
listMyTasks,
|
|
1035
|
+
taskList,
|
|
1036
|
+
taskShow,
|
|
1037
|
+
taskTimeline,
|
|
1038
|
+
searchTasks,
|
|
1039
|
+
taskCreate,
|
|
1040
|
+
taskBulkCreate,
|
|
1041
|
+
taskUpdate,
|
|
1042
|
+
taskMove,
|
|
1043
|
+
taskArchive,
|
|
1044
|
+
taskDelete,
|
|
1045
|
+
commentAdd,
|
|
1046
|
+
commentReply,
|
|
1047
|
+
listComments
|
|
1048
|
+
];
|
|
1049
|
+
var operationsByName = new Map(operations.map((o) => [o.name, o]));
|
|
1050
|
+
|
|
1051
|
+
export {
|
|
1052
|
+
pyramidError,
|
|
1053
|
+
mapPyramidHttpError,
|
|
1054
|
+
toPyramidError,
|
|
1055
|
+
operations,
|
|
1056
|
+
operationsByName
|
|
1057
|
+
};
|