@sideboard-ai/core 0.1.264 → 0.1.267
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/{abletime-6AOJI2YZ.js → abletime-4ZSDI4YP.js} +9 -6
- package/dist/{abletime-U75ZGDIX.js → abletime-JNQAHLHH.js} +7 -4
- package/dist/{abletime-oauth-TA2YDCYX.js → abletime-oauth-4RXZH2KU.js} +3 -3
- package/dist/{abletime-oauth-LOAHGG4X.js → abletime-oauth-DCEIINYT.js} +5 -5
- package/dist/{abletime-rest-CTA5WV26.js → abletime-rest-KVBJFVFD.js} +3 -3
- package/dist/{abletime-rest-YZPFM6IY.js → abletime-rest-VN6PZ6P2.js} +5 -5
- package/dist/agents/cursor-runner.js +7 -5
- package/dist/{agents-3TUMFSMH.js → agents-G3JTULM2.js} +7 -5
- package/dist/{agents-6FWDRVA2.js → agents-XRXJCAWJ.js} +11 -10
- package/dist/{app-settings-RKFFUIFQ.js → app-settings-L5HGYXWJ.js} +1 -3
- package/dist/{app-settings-MVZPM6WW.js → app-settings-RAC4PSSP.js} +3 -5
- package/dist/{caffeinate-hold-UAGLSGMM.js → caffeinate-hold-7JM5H5P3.js} +2 -2
- package/dist/{chunk-F5MFNUAA.js → chunk-27Z4MEQ5.js} +1 -1
- package/dist/{chunk-EK3KVT7I.js → chunk-2XIRV7FS.js} +53 -1254
- package/dist/{chunk-3H7O27X2.js → chunk-3VBS6MES.js} +39 -1017
- package/dist/chunk-4ECDIKAS.js +2273 -0
- package/dist/{chunk-H547WSSZ.js → chunk-4RZRHTSZ.js} +1 -1
- package/dist/{chunk-QN5DL2QO.js → chunk-5DZUWGKV.js} +1 -1
- package/dist/chunk-5N4Q46SG.js +392 -0
- package/dist/{chunk-YKYEHDMX.js → chunk-AGXZAMOF.js} +5 -5
- package/dist/{chunk-RKPBIB2U.js → chunk-DB7MVDYS.js} +33 -7
- package/dist/{chunk-BOMSJUXW.js → chunk-DBFAFI5A.js} +5 -5
- package/dist/chunk-FAGK33E2.js +994 -0
- package/dist/{chunk-A5SAYFJ4.js → chunk-FIBZEFKA.js} +4 -390
- package/dist/{chunk-5PK53SAP.js → chunk-GONPMH36.js} +5 -5
- package/dist/{chunk-L5WGAIL2.js → chunk-GZXKUUJO.js} +35 -9
- package/dist/{chunk-Z2LGMV6C.js → chunk-H5XL4KUS.js} +21 -0
- package/dist/{chunk-T3AYRQJZ.js → chunk-HSRN2CSL.js} +473 -2487
- package/dist/{chunk-TJ673EZT.js → chunk-JANCPF7Z.js} +2 -2
- package/dist/chunk-KZ2CDZBZ.js +828 -0
- package/dist/{chunk-6YY7XPPL.js → chunk-L6CYOON4.js} +5 -5
- package/dist/{chunk-6YOXQVJ7.js → chunk-LAIBYKZS.js} +3 -3
- package/dist/{chunk-4ALDX5IO.js → chunk-NWE3NOWQ.js} +3 -3
- package/dist/{chunk-36FOBJBF.js → chunk-OIENQEGG.js} +63 -38
- package/dist/{chunk-2QUQH2SZ.js → chunk-PAH7KDL2.js} +68 -24
- package/dist/{chunk-XAISOBBG.js → chunk-TQRSVE7D.js} +1 -1
- package/dist/chunk-UKSJO36E.js +1224 -0
- package/dist/{chunk-CEMNIGQH.js → chunk-UN5Q7POQ.js} +1 -1
- package/dist/{chunk-5ZPT5YAV.js → chunk-USBZ7PH4.js} +1 -1
- package/dist/chunk-UXI52MLJ.js +826 -0
- package/dist/chunk-X7RP3TQB.js +2385 -0
- package/dist/{chunk-QX6QUZVD.js → chunk-ZSGSVFWD.js} +424 -2611
- package/dist/conductor-MLM5NGTP.js +104 -0
- package/dist/conductor-NRKTEN5X.js +105 -0
- package/dist/{connected-teams-7CENZDWU.js → connected-teams-JIUOSZ6C.js} +2 -2
- package/dist/{coordinator-prompt-J7DKHOCH.js → coordinator-prompt-4GHKCVRH.js} +7 -7
- package/dist/{coordinator-prompt-HZCQZ6C5.js → coordinator-prompt-BKRQDZFT.js} +3 -3
- package/dist/{cursor-recover-FALSNEZN.js → cursor-recover-MB3T2K5T.js} +2 -2
- package/dist/{global-workspace-TI26K25J.js → global-workspace-EEWJOMED.js} +9 -3
- package/dist/{global-workspace-6LDUZ2NF.js → global-workspace-EWFL4HPW.js} +13 -7
- package/dist/index.cjs +3918 -2592
- package/dist/index.d.cts +363 -68
- package/dist/index.d.ts +363 -68
- package/dist/index.js +4181 -3462
- package/dist/{managed-accounts-4BLTJKAN.js → managed-accounts-46A4KRUE.js} +2 -2
- package/dist/mcp/run-stdio.cjs +1894 -2012
- package/dist/mcp/run-stdio.js +719 -1445
- package/dist/{orchestrator-5YU46YNS.js → orchestrator-6ZQ2CYS4.js} +9 -6
- package/dist/{orchestrator-4JSG7BWC.js → orchestrator-MA3MHYIM.js} +15 -13
- package/dist/{paths-IUR3ABNG.js → paths-HYT5WOOX.js} +1 -1
- package/dist/{thread-store-FJ5CDX2M.js → thread-store-B3ZM4QEZ.js} +3 -3
- package/dist/{workspaces-QJ3XIPUJ.js → workspaces-F33Z4CEY.js} +3 -3
- package/dist/{workspaces-WOQHJFRQ.js → workspaces-SLP24Z3S.js} +7 -7
- package/dist/{worktree-6U2FA5LO.js → worktree-ACW23KJ5.js} +6 -6
- package/dist/{worktree-KQA4ON2G.js → worktree-C7RPVHZ7.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-CEOTK5GZ.js +0 -479
- package/dist/chunk-K7B25HO4.js +0 -477
|
@@ -0,0 +1,828 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
abletimeRestUrl,
|
|
5
|
+
textFromAbleTimeDoc
|
|
6
|
+
} from "./chunk-DBFAFI5A.js";
|
|
7
|
+
import {
|
|
8
|
+
assertAbleTimeCredential,
|
|
9
|
+
callAbleTimeTool,
|
|
10
|
+
normalizeAbleTimeHost,
|
|
11
|
+
rewriteAbleTimeError
|
|
12
|
+
} from "./chunk-GONPMH36.js";
|
|
13
|
+
import {
|
|
14
|
+
httpFetch
|
|
15
|
+
} from "./chunk-VKQGAWHY.js";
|
|
16
|
+
import {
|
|
17
|
+
ATTACHMENTS_DIR,
|
|
18
|
+
attachmentsGitignoreBody
|
|
19
|
+
} from "./chunk-N2WW62LF.js";
|
|
20
|
+
import {
|
|
21
|
+
getAbleTimeAccessToken,
|
|
22
|
+
getAbleTimeHost,
|
|
23
|
+
isAbleTimeConnected,
|
|
24
|
+
saveAbleTimeConnection
|
|
25
|
+
} from "./chunk-OIENQEGG.js";
|
|
26
|
+
|
|
27
|
+
// src/integrations/issue-attachments.ts
|
|
28
|
+
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
29
|
+
import { basename, extname, join } from "path";
|
|
30
|
+
import { randomUUID } from "crypto";
|
|
31
|
+
var ISSUE_ATTACHMENT_MAX_BYTES = 25 * 1024 * 1024;
|
|
32
|
+
var MARKDOWN_IMAGE_RE = /!\[[^\]]*]\(\s*<?(https?:\/\/[^)\s>]+)>?/gi;
|
|
33
|
+
var MARKDOWN_LINK_RE = /(?<!!)\[[^\]]*]\(\s*<?(https?:\/\/[^)\s>]+)>?/gi;
|
|
34
|
+
var HTML_SRC_RE = /\b(?:src|href)\s*=\s*["'](https?:\/\/[^"']+)["']/gi;
|
|
35
|
+
var BARE_URL_RE = /https?:\/\/[^\s<>"'`)]+/gi;
|
|
36
|
+
var EXT_BY_MIME = {
|
|
37
|
+
"image/png": ".png",
|
|
38
|
+
"image/jpeg": ".jpg",
|
|
39
|
+
"image/jpg": ".jpg",
|
|
40
|
+
"image/gif": ".gif",
|
|
41
|
+
"image/webp": ".webp",
|
|
42
|
+
"image/svg+xml": ".svg",
|
|
43
|
+
"application/pdf": ".pdf",
|
|
44
|
+
"text/plain": ".txt",
|
|
45
|
+
"text/markdown": ".md",
|
|
46
|
+
"application/json": ".json",
|
|
47
|
+
"application/zip": ".zip"
|
|
48
|
+
};
|
|
49
|
+
function issueAttachmentWorktreePath(repoPath) {
|
|
50
|
+
const raw = (repoPath ?? "").trim();
|
|
51
|
+
return raw || process.cwd();
|
|
52
|
+
}
|
|
53
|
+
function ensureIssueAttachmentsDir(worktreePath) {
|
|
54
|
+
const dir = join(worktreePath, ATTACHMENTS_DIR);
|
|
55
|
+
mkdirSync(dir, { recursive: true });
|
|
56
|
+
const gi = join(dir, ".gitignore");
|
|
57
|
+
if (!existsSync(gi)) {
|
|
58
|
+
writeFileSync(gi, attachmentsGitignoreBody(), "utf8");
|
|
59
|
+
}
|
|
60
|
+
return dir;
|
|
61
|
+
}
|
|
62
|
+
function hostnameOf(url) {
|
|
63
|
+
try {
|
|
64
|
+
return new URL(url).hostname.toLowerCase();
|
|
65
|
+
} catch {
|
|
66
|
+
return "";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function hostMatches(url, suffixes) {
|
|
70
|
+
const host = hostnameOf(url);
|
|
71
|
+
if (!host) return false;
|
|
72
|
+
return suffixes.some((suffix) => host === suffix || host.endsWith(`.${suffix}`));
|
|
73
|
+
}
|
|
74
|
+
function looksLikeHttpUrl(value) {
|
|
75
|
+
return /^https?:\/\//i.test(value.trim());
|
|
76
|
+
}
|
|
77
|
+
function looksLikeFileUrl(url) {
|
|
78
|
+
try {
|
|
79
|
+
const path = new URL(url).pathname;
|
|
80
|
+
return /\.[a-z0-9]{2,8}$/i.test(path);
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function filenameFromUrl(url, fallback = "attachment") {
|
|
86
|
+
try {
|
|
87
|
+
const parsed = new URL(url);
|
|
88
|
+
const last = decodeURIComponent(basename(parsed.pathname)).replace(/[/\\]/g, "_");
|
|
89
|
+
if (last && last !== "/" && last !== ".") return last;
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
return fallback;
|
|
93
|
+
}
|
|
94
|
+
function filenameFromContentDisposition(header) {
|
|
95
|
+
if (!header) return void 0;
|
|
96
|
+
const star = header.match(/filename\*\s*=\s*(?:UTF-8''|utf-8'')([^;]+)/i);
|
|
97
|
+
if (star?.[1]) {
|
|
98
|
+
try {
|
|
99
|
+
return decodeURIComponent(star[1].trim().replace(/^["']|["']$/g, ""));
|
|
100
|
+
} catch {
|
|
101
|
+
return star[1].trim().replace(/^["']|["']$/g, "");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const plain = header.match(/filename\s*=\s*("(?:\\.|[^"])*"|[^;]+)/i);
|
|
105
|
+
if (!plain?.[1]) return void 0;
|
|
106
|
+
return plain[1].trim().replace(/^["']|["']$/g, "").replace(/\\"/g, '"');
|
|
107
|
+
}
|
|
108
|
+
function uniqueAttachmentFileName(dir, originalName) {
|
|
109
|
+
const safe = originalName.replace(/[/\\]/g, "_").replace(/^\.+/, "") || "file";
|
|
110
|
+
if (!existsSync(join(dir, safe))) return safe;
|
|
111
|
+
const ext = extname(safe);
|
|
112
|
+
const stem = ext ? safe.slice(0, -ext.length) : safe;
|
|
113
|
+
for (let i = 1; i < 1e4; i++) {
|
|
114
|
+
const candidate = `${stem}-${i}${ext}`;
|
|
115
|
+
if (!existsSync(join(dir, candidate))) return candidate;
|
|
116
|
+
}
|
|
117
|
+
return `${stem}-${randomUUID()}${ext}`;
|
|
118
|
+
}
|
|
119
|
+
function extractHttpUrls(markdown) {
|
|
120
|
+
const text = markdown ?? "";
|
|
121
|
+
if (!text.trim()) return [];
|
|
122
|
+
const found = [];
|
|
123
|
+
const seen = /* @__PURE__ */ new Set();
|
|
124
|
+
const push = (raw) => {
|
|
125
|
+
const url = raw.replace(/[).,;]+$/, "");
|
|
126
|
+
if (!looksLikeHttpUrl(url) || seen.has(url)) return;
|
|
127
|
+
seen.add(url);
|
|
128
|
+
found.push(url);
|
|
129
|
+
};
|
|
130
|
+
for (const re of [MARKDOWN_IMAGE_RE, MARKDOWN_LINK_RE, HTML_SRC_RE, BARE_URL_RE]) {
|
|
131
|
+
re.lastIndex = 0;
|
|
132
|
+
let match;
|
|
133
|
+
while (match = re.exec(text)) {
|
|
134
|
+
if (match[1]) push(match[1]);
|
|
135
|
+
else if (re === BARE_URL_RE) push(match[0]);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return found;
|
|
139
|
+
}
|
|
140
|
+
function mergeIssueAttachments(items) {
|
|
141
|
+
const out = [];
|
|
142
|
+
const seen = /* @__PURE__ */ new Set();
|
|
143
|
+
for (const item of items) {
|
|
144
|
+
if (!item) continue;
|
|
145
|
+
const url = item.url.trim();
|
|
146
|
+
const id = item.id.trim() || url;
|
|
147
|
+
if (!id && !url) continue;
|
|
148
|
+
const key = url || id;
|
|
149
|
+
if (seen.has(key) || seen.has(id)) continue;
|
|
150
|
+
seen.add(key);
|
|
151
|
+
if (id !== key) seen.add(id);
|
|
152
|
+
out.push({
|
|
153
|
+
id: id || url,
|
|
154
|
+
name: item.name.trim() || filenameFromUrl(url),
|
|
155
|
+
url,
|
|
156
|
+
...item.contentType ? { contentType: item.contentType } : {},
|
|
157
|
+
...item.sourceType ? { sourceType: item.sourceType } : {}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
function extensionForContentType(contentType, name) {
|
|
163
|
+
if (extname(name)) return name;
|
|
164
|
+
const mime = (contentType ?? "").split(";")[0]?.trim().toLowerCase() ?? "";
|
|
165
|
+
const ext = EXT_BY_MIME[mime];
|
|
166
|
+
return ext ? `${name}${ext}` : name;
|
|
167
|
+
}
|
|
168
|
+
function assertNotHtml(contentType, url) {
|
|
169
|
+
const mime = (contentType ?? "").split(";")[0]?.trim().toLowerCase() ?? "";
|
|
170
|
+
if (mime === "text/html" || mime === "application/xhtml+xml") {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Attachment at ${url} returned HTML, not a file. Use the listed file URL (uploads / user-attachments), not a ticket or PR page.`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function downloadAuthenticatedFile(opts) {
|
|
177
|
+
const url = opts.url.trim();
|
|
178
|
+
if (!looksLikeHttpUrl(url)) {
|
|
179
|
+
throw new Error(`Attachment URL must be http(s) (got ${url || "empty"})`);
|
|
180
|
+
}
|
|
181
|
+
const res = await httpFetch(url, {
|
|
182
|
+
method: "GET",
|
|
183
|
+
redirect: "follow",
|
|
184
|
+
headers: {
|
|
185
|
+
Accept: "*/*",
|
|
186
|
+
...opts.headers ?? {}
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
if (!res.ok) {
|
|
190
|
+
const detail = await res.text().catch(() => "");
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Attachment download failed ${res.status}${detail ? `: ${detail.slice(0, 200)}` : ""} (${url})`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
const contentType = res.headers.get("content-type") ?? void 0;
|
|
196
|
+
assertNotHtml(contentType, url);
|
|
197
|
+
const lengthHeader = res.headers.get("content-length");
|
|
198
|
+
if (lengthHeader) {
|
|
199
|
+
const length = Number(lengthHeader);
|
|
200
|
+
if (Number.isFinite(length) && length > ISSUE_ATTACHMENT_MAX_BYTES) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`Attachment is ${length} bytes; max is ${ISSUE_ATTACHMENT_MAX_BYTES} (${url})`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
207
|
+
if (buf.length > ISSUE_ATTACHMENT_MAX_BYTES) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`Attachment is ${buf.length} bytes; max is ${ISSUE_ATTACHMENT_MAX_BYTES} (${url})`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
const dir = ensureIssueAttachmentsDir(opts.worktreePath);
|
|
213
|
+
const hinted = opts.name?.trim() || filenameFromContentDisposition(res.headers.get("content-disposition")) || filenameFromUrl(url);
|
|
214
|
+
const name = uniqueAttachmentFileName(dir, extensionForContentType(contentType, hinted));
|
|
215
|
+
writeFileSync(join(dir, name), buf);
|
|
216
|
+
return {
|
|
217
|
+
name,
|
|
218
|
+
path: `${ATTACHMENTS_DIR}/${name}`,
|
|
219
|
+
bytes: buf.length,
|
|
220
|
+
...contentType ? { contentType } : {}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/integrations/abletime-attachments.ts
|
|
225
|
+
function asRecord(value) {
|
|
226
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
227
|
+
}
|
|
228
|
+
function asString(value) {
|
|
229
|
+
return typeof value === "string" ? value.trim() : "";
|
|
230
|
+
}
|
|
231
|
+
function asList(value) {
|
|
232
|
+
if (Array.isArray(value)) return value;
|
|
233
|
+
const record = asRecord(value);
|
|
234
|
+
if (!record) return [];
|
|
235
|
+
for (const key of ["data", "tasks", "projects", "items", "results", "nodes"]) {
|
|
236
|
+
if (Array.isArray(record[key])) return record[key];
|
|
237
|
+
}
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
function firstString(record, keys) {
|
|
241
|
+
if (!record) return "";
|
|
242
|
+
for (const key of keys) {
|
|
243
|
+
const value = asString(record[key]);
|
|
244
|
+
if (value) return value;
|
|
245
|
+
}
|
|
246
|
+
return "";
|
|
247
|
+
}
|
|
248
|
+
function filesFromAbleTimeDoc(value) {
|
|
249
|
+
const out = [];
|
|
250
|
+
const seen = /* @__PURE__ */ new Set();
|
|
251
|
+
const walk = (node) => {
|
|
252
|
+
const rec = asRecord(node);
|
|
253
|
+
if (!rec) return;
|
|
254
|
+
const attrs = asRecord(rec.attrs) ?? rec;
|
|
255
|
+
const url = asString(attrs.src) || asString(attrs.href) || asString(attrs.url) || asString(attrs.fileUrl) || asString(attrs.downloadUrl);
|
|
256
|
+
if (url && !seen.has(url)) {
|
|
257
|
+
seen.add(url);
|
|
258
|
+
out.push({
|
|
259
|
+
id: asString(attrs.id) || asString(attrs.fileId) || void 0,
|
|
260
|
+
name: asString(attrs.name) || asString(attrs.title) || asString(attrs.filename) || asString(attrs.alt) || "attachment",
|
|
261
|
+
url
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
if (Array.isArray(rec.content)) rec.content.forEach(walk);
|
|
265
|
+
};
|
|
266
|
+
walk(value);
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
function listedAbleTimeFiles(raw) {
|
|
270
|
+
const out = [];
|
|
271
|
+
for (const item of asList(raw)) {
|
|
272
|
+
const rec = asRecord(item);
|
|
273
|
+
if (!rec) continue;
|
|
274
|
+
const url = firstString(rec, ["url", "href", "src", "fileUrl", "downloadUrl", "permalink"]);
|
|
275
|
+
if (!url) continue;
|
|
276
|
+
const id = firstString(rec, ["id", "fileId", "attachmentId"]) || url;
|
|
277
|
+
const name = firstString(rec, ["name", "title", "filename", "fileName", "alt"]) || "attachment";
|
|
278
|
+
out.push({ id, name, url });
|
|
279
|
+
}
|
|
280
|
+
return out;
|
|
281
|
+
}
|
|
282
|
+
function mapAbleTimeAttachments(record) {
|
|
283
|
+
const listed = [
|
|
284
|
+
...listedAbleTimeFiles(record.attachments),
|
|
285
|
+
...listedAbleTimeFiles(record.files),
|
|
286
|
+
...listedAbleTimeFiles(record.media)
|
|
287
|
+
];
|
|
288
|
+
const fromDocs = [
|
|
289
|
+
...filesFromAbleTimeDoc(record.description),
|
|
290
|
+
...filesFromAbleTimeDoc(record.content),
|
|
291
|
+
...asList(record.comments).flatMap((item) => {
|
|
292
|
+
const rec = asRecord(item);
|
|
293
|
+
return rec ? filesFromAbleTimeDoc(rec.content ?? rec.body) : [];
|
|
294
|
+
})
|
|
295
|
+
].map((file) => ({
|
|
296
|
+
id: file.id || file.url,
|
|
297
|
+
name: file.name,
|
|
298
|
+
url: file.url
|
|
299
|
+
}));
|
|
300
|
+
const fromText = extractHttpUrls(
|
|
301
|
+
firstString(record, ["description", "body", "descriptionSanitized"]) || textFromAbleTimeDoc(record.description)
|
|
302
|
+
).filter(looksLikeFileUrl).map((url) => ({
|
|
303
|
+
id: url,
|
|
304
|
+
name: url.split("/").pop() || "attachment",
|
|
305
|
+
url
|
|
306
|
+
}));
|
|
307
|
+
return mergeIssueAttachments([...listed, ...fromDocs, ...fromText]);
|
|
308
|
+
}
|
|
309
|
+
function ableTimeFileHostSuffixes(host) {
|
|
310
|
+
try {
|
|
311
|
+
const hostname = new URL(normalizeAbleTimeHost(host)).hostname.toLowerCase();
|
|
312
|
+
const parts = hostname.split(".");
|
|
313
|
+
const root = parts.slice(-2).join(".");
|
|
314
|
+
return Array.from(/* @__PURE__ */ new Set([hostname, root, "abletime.com"]));
|
|
315
|
+
} catch {
|
|
316
|
+
return ["abletime.com"];
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async function resolveAbleTimeAttachmentUrl(id, opts) {
|
|
320
|
+
for (const path of [`/files/${encodeURIComponent(id)}`, `/attachments/${encodeURIComponent(id)}`]) {
|
|
321
|
+
const res = await httpFetch(abletimeRestUrl(path, opts.host), {
|
|
322
|
+
method: "GET",
|
|
323
|
+
headers: {
|
|
324
|
+
Accept: "application/json",
|
|
325
|
+
Authorization: `Bearer ${opts.token}`
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
if (!res.ok) continue;
|
|
329
|
+
const contentType = (res.headers.get("content-type") ?? "").toLowerCase();
|
|
330
|
+
if (!contentType.includes("json")) continue;
|
|
331
|
+
const json = await res.json().catch(() => null);
|
|
332
|
+
const rec = json && typeof json === "object" && !Array.isArray(json) ? json.data && typeof json.data === "object" ? json.data : json : null;
|
|
333
|
+
if (!rec) continue;
|
|
334
|
+
const url = firstString(rec, ["url", "href", "src", "fileUrl", "downloadUrl"]) || firstString(asRecord(rec.file), ["url", "href"]);
|
|
335
|
+
if (url) {
|
|
336
|
+
return {
|
|
337
|
+
url,
|
|
338
|
+
name: firstString(rec, ["name", "title", "filename", "fileName"]) || void 0
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
throw new Error(`AbleTime attachment not found: ${id}`);
|
|
343
|
+
}
|
|
344
|
+
async function downloadAbleTimeAttachment(input, opts) {
|
|
345
|
+
const id = input.id.trim();
|
|
346
|
+
if (!id) throw new Error("AbleTime attachment id or URL is required");
|
|
347
|
+
const token = assertAbleTimeCredential(opts?.token ?? getAbleTimeAccessToken() ?? "");
|
|
348
|
+
const host = normalizeAbleTimeHost(opts?.host ?? getAbleTimeHost());
|
|
349
|
+
let url = id;
|
|
350
|
+
let name = input.name?.trim();
|
|
351
|
+
if (!looksLikeHttpUrl(id)) {
|
|
352
|
+
const resolved = await resolveAbleTimeAttachmentUrl(id, { token, host });
|
|
353
|
+
url = resolved.url;
|
|
354
|
+
name = name || resolved.name;
|
|
355
|
+
}
|
|
356
|
+
const sendAuth = hostMatches(url, ableTimeFileHostSuffixes(host));
|
|
357
|
+
return downloadAuthenticatedFile({
|
|
358
|
+
url,
|
|
359
|
+
headers: sendAuth ? { Authorization: `Bearer ${token}` } : void 0,
|
|
360
|
+
worktreePath: issueAttachmentWorktreePath(opts?.destPath),
|
|
361
|
+
name
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/integrations/abletime.ts
|
|
366
|
+
var CLOSED_STATES = /* @__PURE__ */ new Set(["done", "completed", "canceled", "cancelled", "archived", "closed"]);
|
|
367
|
+
function asRecord2(value) {
|
|
368
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
369
|
+
}
|
|
370
|
+
function asString2(value) {
|
|
371
|
+
return typeof value === "string" ? value.trim() : "";
|
|
372
|
+
}
|
|
373
|
+
function firstString2(record, keys) {
|
|
374
|
+
if (!record) return "";
|
|
375
|
+
for (const key of keys) {
|
|
376
|
+
const value = asString2(record[key]);
|
|
377
|
+
if (value) return value;
|
|
378
|
+
}
|
|
379
|
+
return "";
|
|
380
|
+
}
|
|
381
|
+
function firstRecord(record, keys) {
|
|
382
|
+
if (!record) return null;
|
|
383
|
+
for (const key of keys) {
|
|
384
|
+
const value = asRecord2(record[key]);
|
|
385
|
+
if (value) return value;
|
|
386
|
+
}
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
function asList2(value) {
|
|
390
|
+
if (Array.isArray(value)) return value;
|
|
391
|
+
const record = asRecord2(value);
|
|
392
|
+
if (!record) return [];
|
|
393
|
+
for (const key of ["data", "tasks", "projects", "items", "results", "nodes"]) {
|
|
394
|
+
if (Array.isArray(record[key])) return record[key];
|
|
395
|
+
}
|
|
396
|
+
return [];
|
|
397
|
+
}
|
|
398
|
+
function labelsOf(record) {
|
|
399
|
+
const raw = record.tags ?? record.labels ?? record.tag_names;
|
|
400
|
+
if (!Array.isArray(raw)) return [];
|
|
401
|
+
return raw.map((item) => {
|
|
402
|
+
if (typeof item === "string") return item.trim();
|
|
403
|
+
const rec = asRecord2(item);
|
|
404
|
+
return rec ? firstString2(rec, ["name", "title", "label"]) : "";
|
|
405
|
+
}).filter(Boolean);
|
|
406
|
+
}
|
|
407
|
+
function commentsOf(record) {
|
|
408
|
+
const out = [];
|
|
409
|
+
for (const item of asList2(record.comments ?? record.notes ?? record.discussion)) {
|
|
410
|
+
const rec = asRecord2(item);
|
|
411
|
+
if (!rec) continue;
|
|
412
|
+
const body = firstString2(rec, ["body", "text", "comment", "message", "contentSanitized"]) || textFromAbleTimeDoc(rec.content);
|
|
413
|
+
if (!body) continue;
|
|
414
|
+
const user = firstString2(asRecord2(rec.user) ?? asRecord2(rec.author), ["name", "display_name", "username"]) || firstString2(rec, ["user_name", "author", "username"]);
|
|
415
|
+
const id = firstString2(rec, ["id", "comment_id", "commentId"]);
|
|
416
|
+
const url = firstString2(rec, ["url", "permalink"]);
|
|
417
|
+
const createdAt = firstString2(rec, ["created_at", "createdAt", "created"]);
|
|
418
|
+
out.push({
|
|
419
|
+
body,
|
|
420
|
+
...id ? { id } : {},
|
|
421
|
+
...url ? { url } : {},
|
|
422
|
+
...createdAt ? { createdAt } : {},
|
|
423
|
+
...user ? { user } : {}
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
return out;
|
|
427
|
+
}
|
|
428
|
+
function assigneeOf(record) {
|
|
429
|
+
const nested = firstRecord(record, ["assignee", "assigned_to", "user", "owner"]);
|
|
430
|
+
const name = firstString2(nested, ["name", "display_name", "full_name", "email"]) || firstString2(record, ["assignee_name", "assigneeName"]);
|
|
431
|
+
if (!name) return void 0;
|
|
432
|
+
const id = firstString2(nested, ["id", "user_id"]);
|
|
433
|
+
return { id: id || void 0, name };
|
|
434
|
+
}
|
|
435
|
+
function taskUrl(task, host) {
|
|
436
|
+
const base = normalizeAbleTimeHost(host);
|
|
437
|
+
const slug = encodeURIComponent(task.identifier || task.id);
|
|
438
|
+
return `${base}/tasks/${slug}`;
|
|
439
|
+
}
|
|
440
|
+
function mapAbleTimeTask(raw, host) {
|
|
441
|
+
const record = asRecord2(raw);
|
|
442
|
+
if (!record) return null;
|
|
443
|
+
const nested = firstRecord(record, ["task", "data"]) ?? record;
|
|
444
|
+
const id = firstString2(nested, ["id", "task_id", "taskId", "timeflowTaskId"]);
|
|
445
|
+
const identifier = firstString2(nested, ["reference", "ref", "identifier", "key", "code", "number", "taskRef"]) || id;
|
|
446
|
+
const title = firstString2(nested, ["title", "name"]);
|
|
447
|
+
if (!id && !identifier && !title) return null;
|
|
448
|
+
const resolvedId = id || identifier;
|
|
449
|
+
const resolvedIdentifier = identifier || id;
|
|
450
|
+
const url = firstString2(nested, ["url", "permalink", "html_url"]) || taskUrl({ id: resolvedId, identifier: resolvedIdentifier }, host);
|
|
451
|
+
return {
|
|
452
|
+
id: resolvedId,
|
|
453
|
+
identifier: resolvedIdentifier,
|
|
454
|
+
title: title || resolvedIdentifier,
|
|
455
|
+
url,
|
|
456
|
+
description: firstString2(nested, ["description", "body", "details", "descriptionSanitized"]) || textFromAbleTimeDoc(nested.description) || void 0,
|
|
457
|
+
state: firstString2(nested, ["state", "board_state", "boardState", "status", "taskState"]) || void 0,
|
|
458
|
+
projectId: firstString2(nested, ["project_id", "projectId"]) || firstString2(asRecord2(nested.project), ["id"]) || void 0,
|
|
459
|
+
categoryId: firstString2(nested, ["category_id", "categoryId", "projectCategoryId"]) || firstString2(asRecord2(nested.category), ["id"]) || void 0,
|
|
460
|
+
assignee: assigneeOf(nested),
|
|
461
|
+
labels: labelsOf(nested),
|
|
462
|
+
comments: commentsOf(nested),
|
|
463
|
+
attachments: mapAbleTimeAttachments(nested),
|
|
464
|
+
createdAt: firstString2(nested, ["created_at", "createdAt", "created", "dateCreated"]) || void 0,
|
|
465
|
+
updatedAt: firstString2(nested, [
|
|
466
|
+
"updated_at",
|
|
467
|
+
"updatedAt",
|
|
468
|
+
"modified_at",
|
|
469
|
+
"modifiedAt",
|
|
470
|
+
"last_activity_at",
|
|
471
|
+
"lastActivityAt",
|
|
472
|
+
"lastUpdate"
|
|
473
|
+
]) || void 0
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
function toAbleTimeIssueInfo(task) {
|
|
477
|
+
return {
|
|
478
|
+
id: task.id || task.identifier,
|
|
479
|
+
identifier: task.identifier,
|
|
480
|
+
title: task.title,
|
|
481
|
+
url: task.url,
|
|
482
|
+
labels: task.labels,
|
|
483
|
+
provider: "abletime",
|
|
484
|
+
assignee: task.assignee?.name,
|
|
485
|
+
assignees: task.assignee?.name ? [task.assignee.name] : void 0,
|
|
486
|
+
...task.createdAt ? { createdAt: task.createdAt } : {},
|
|
487
|
+
...task.updatedAt ? { updatedAt: task.updatedAt } : {}
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function issueAttachmentForAbleTimeTask(task) {
|
|
491
|
+
return {
|
|
492
|
+
id: `abletime-${task.id || task.identifier}`,
|
|
493
|
+
name: task.identifier || task.title,
|
|
494
|
+
kind: "issue",
|
|
495
|
+
content: [
|
|
496
|
+
`Linked issue: ${task.identifier} \u2014 ${task.title}`,
|
|
497
|
+
task.url ? `URL: ${task.url}` : null,
|
|
498
|
+
task.labels.length ? `Labels: ${task.labels.join(", ")}` : null
|
|
499
|
+
].filter(Boolean).join("\n")
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
function mapProject(raw) {
|
|
503
|
+
const record = asRecord2(raw);
|
|
504
|
+
if (!record) return null;
|
|
505
|
+
const id = firstString2(record, ["id", "project_id", "projectId"]);
|
|
506
|
+
const name = firstString2(record, ["name", "title", "projectName"]);
|
|
507
|
+
if (!id && !name) return null;
|
|
508
|
+
const categories = asList2(record.categories ?? record.category).map((item) => {
|
|
509
|
+
const rec = asRecord2(item);
|
|
510
|
+
if (!rec) return null;
|
|
511
|
+
const categoryId = firstString2(rec, ["id", "category_id", "projectCategoryId", "categoryId"]);
|
|
512
|
+
const categoryName = firstString2(rec, ["name", "title", "categoryName"]);
|
|
513
|
+
if (!categoryId && !categoryName) return null;
|
|
514
|
+
return { id: categoryId || categoryName, name: categoryName || categoryId };
|
|
515
|
+
}).filter((item) => Boolean(item));
|
|
516
|
+
return { id: id || name, name: name || id, categories };
|
|
517
|
+
}
|
|
518
|
+
function isOpenTask(task) {
|
|
519
|
+
const state = (task.state ?? "").toLowerCase();
|
|
520
|
+
return !state || !CLOSED_STATES.has(state);
|
|
521
|
+
}
|
|
522
|
+
async function getAbleTimeOrientation(opts) {
|
|
523
|
+
const raw = await callAbleTimeTool("orientation", {}, opts);
|
|
524
|
+
const record = asRecord2(raw);
|
|
525
|
+
const viewerRec = firstRecord(record, ["viewer", "principal", "user", "actor", "me", "person"]) ?? record;
|
|
526
|
+
const viewer = {
|
|
527
|
+
id: firstString2(viewerRec, ["id", "user_id"]) || void 0,
|
|
528
|
+
name: firstString2(viewerRec, ["name", "display_name", "full_name"]) || firstString2(record, ["name"]) || void 0
|
|
529
|
+
};
|
|
530
|
+
const projects = asList2(
|
|
531
|
+
record?.projects ?? record?.current_projects ?? record?.currentProjects
|
|
532
|
+
).map(mapProject).filter((item) => Boolean(item));
|
|
533
|
+
const tasks = asList2(record?.tasks ?? record?.active_tasks ?? record?.activeTasks).map((item) => mapAbleTimeTask(item, opts?.host)).filter((item) => Boolean(item));
|
|
534
|
+
return { viewer, projects, tasks, raw };
|
|
535
|
+
}
|
|
536
|
+
async function listAbleTimeProjects(opts) {
|
|
537
|
+
const raw = await callAbleTimeTool("list_projects", { include_categories: true }, opts);
|
|
538
|
+
return asList2(raw).map(mapProject).filter((item) => Boolean(item));
|
|
539
|
+
}
|
|
540
|
+
async function listAbleTimeTasks(opts) {
|
|
541
|
+
const args = {};
|
|
542
|
+
if (opts?.projectId) {
|
|
543
|
+
args.project_id = opts.projectId;
|
|
544
|
+
args.project = opts.projectId;
|
|
545
|
+
}
|
|
546
|
+
if (opts?.query) args.query = opts.query;
|
|
547
|
+
const raw = await callAbleTimeTool("list_tasks", args, opts);
|
|
548
|
+
const tasks = asList2(raw).map((item) => mapAbleTimeTask(item, opts?.host)).filter((item) => Boolean(item));
|
|
549
|
+
return opts?.includeClosed ? tasks : tasks.filter(isOpenTask);
|
|
550
|
+
}
|
|
551
|
+
async function searchAbleTimeTasks(query, opts) {
|
|
552
|
+
const q = query.trim();
|
|
553
|
+
if (!q) return [];
|
|
554
|
+
const raw = await callAbleTimeTool("search_tasks", { query: q, q }, opts);
|
|
555
|
+
return asList2(raw).map((item) => mapAbleTimeTask(item, opts?.host)).filter((item) => Boolean(item)).filter(isOpenTask);
|
|
556
|
+
}
|
|
557
|
+
async function getAbleTimeTask(id, opts) {
|
|
558
|
+
const raw = await callAbleTimeTool("get_task", { id, task_id: id }, opts);
|
|
559
|
+
const task = mapAbleTimeTask(raw, opts?.host);
|
|
560
|
+
if (!task) throw new Error(`AbleTime task not found: ${id}`);
|
|
561
|
+
return task;
|
|
562
|
+
}
|
|
563
|
+
async function createAbleTimeTask(input, opts) {
|
|
564
|
+
const title = input.title.trim();
|
|
565
|
+
if (!title) throw new Error("AbleTime task title is required");
|
|
566
|
+
const project = await resolveAbleTimeProject(input.projectId, opts);
|
|
567
|
+
const category = resolveAbleTimeCategory(project, input.categoryId);
|
|
568
|
+
const parent = input.parent?.trim();
|
|
569
|
+
const descriptionParts = [
|
|
570
|
+
parent ? `Spin-off of ${parent}.` : null,
|
|
571
|
+
input.description?.trim() || null
|
|
572
|
+
].filter(Boolean);
|
|
573
|
+
const raw = await callAbleTimeTool(
|
|
574
|
+
"create_task",
|
|
575
|
+
{
|
|
576
|
+
title,
|
|
577
|
+
description: descriptionParts.join("\n\n") || void 0,
|
|
578
|
+
state: input.state ?? "todo",
|
|
579
|
+
project: project.id,
|
|
580
|
+
project_id: project.id,
|
|
581
|
+
category: category?.id,
|
|
582
|
+
category_id: category?.id,
|
|
583
|
+
parent,
|
|
584
|
+
parent_id: parent,
|
|
585
|
+
related_task_id: parent
|
|
586
|
+
},
|
|
587
|
+
opts
|
|
588
|
+
);
|
|
589
|
+
const task = mapAbleTimeTask(raw, opts?.host);
|
|
590
|
+
if (!task) throw new Error("AbleTime create_task returned no task");
|
|
591
|
+
return task;
|
|
592
|
+
}
|
|
593
|
+
async function commentAbleTimeTask(input, opts) {
|
|
594
|
+
const id = input.id.trim();
|
|
595
|
+
const body = input.body.trim();
|
|
596
|
+
if (!id) throw new Error("AbleTime task id is required");
|
|
597
|
+
if (!body) throw new Error("AbleTime comment body is required");
|
|
598
|
+
const raw = await callAbleTimeTool(
|
|
599
|
+
"create_comment",
|
|
600
|
+
{ id, task_id: id, task: id, body, comment: body, text: body },
|
|
601
|
+
opts
|
|
602
|
+
);
|
|
603
|
+
const rec = asRecord2(raw);
|
|
604
|
+
return {
|
|
605
|
+
id: rec ? firstString2(rec, ["id", "comment_id"]) || void 0 : void 0,
|
|
606
|
+
body: rec ? firstString2(rec, ["body", "text", "comment"]) || body : body
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
function isAbleTimeNoneToken(value) {
|
|
610
|
+
if (value == null) return true;
|
|
611
|
+
const s = value.trim();
|
|
612
|
+
return !s || /^(none|clear|unschedule|null)$/i.test(s);
|
|
613
|
+
}
|
|
614
|
+
function taskIdArgs(id) {
|
|
615
|
+
return { id, task_id: id, task: id };
|
|
616
|
+
}
|
|
617
|
+
function ableTimeDependencyTarget(thisId, type, relatedId) {
|
|
618
|
+
const s = type.trim().toLowerCase().replace(/[\s_-]+/g, "");
|
|
619
|
+
if (s === "none" || s === "clear" || s === "null") {
|
|
620
|
+
return { taskId: thisId, dependsOn: null };
|
|
621
|
+
}
|
|
622
|
+
if (s === "blockedby" || s === "blocked" || s === "dependson" || s === "waiton") {
|
|
623
|
+
return { taskId: thisId, dependsOn: relatedId };
|
|
624
|
+
}
|
|
625
|
+
if (s === "blocks" || s === "blocking") {
|
|
626
|
+
return { taskId: relatedId, dependsOn: thisId };
|
|
627
|
+
}
|
|
628
|
+
throw new Error(
|
|
629
|
+
`AbleTime relation type must be blocks or blockedBy (got ${type}). AbleTime models one wait-on dependency.`
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
async function setAbleTimeTaskDependency(taskId, dependsOn, opts) {
|
|
633
|
+
const args = {
|
|
634
|
+
...taskIdArgs(taskId)
|
|
635
|
+
};
|
|
636
|
+
if (dependsOn) {
|
|
637
|
+
args.depends_on = dependsOn;
|
|
638
|
+
args.depends_on_id = dependsOn;
|
|
639
|
+
args.dependency = dependsOn;
|
|
640
|
+
args.dependency_id = dependsOn;
|
|
641
|
+
args.blocked_by = dependsOn;
|
|
642
|
+
args.wait_on = dependsOn;
|
|
643
|
+
args.related_task_id = dependsOn;
|
|
644
|
+
} else {
|
|
645
|
+
args.depends_on = null;
|
|
646
|
+
args.dependency = null;
|
|
647
|
+
args.blocked_by = null;
|
|
648
|
+
args.clear = true;
|
|
649
|
+
}
|
|
650
|
+
await callAbleTimeTool("set_task_dependency", args, opts);
|
|
651
|
+
}
|
|
652
|
+
async function updateAbleTimeTask(input, opts) {
|
|
653
|
+
const id = input.id.trim();
|
|
654
|
+
if (!id) throw new Error("AbleTime task id is required");
|
|
655
|
+
const title = input.title?.trim();
|
|
656
|
+
const description = input.description;
|
|
657
|
+
const state = input.state?.trim();
|
|
658
|
+
const addRelations = input.relations?.length ? input.relations : void 0;
|
|
659
|
+
const removeRelations = input.removeRelations?.length ? input.removeRelations : void 0;
|
|
660
|
+
const updateArgs = { ...taskIdArgs(id) };
|
|
661
|
+
if (title) updateArgs.title = title;
|
|
662
|
+
if (description !== void 0) updateArgs.description = description;
|
|
663
|
+
if (input.labels !== void 0) {
|
|
664
|
+
updateArgs.tags = input.labels;
|
|
665
|
+
updateArgs.labels = input.labels;
|
|
666
|
+
updateArgs.tag_names = input.labels;
|
|
667
|
+
}
|
|
668
|
+
if (input.project !== void 0) {
|
|
669
|
+
const projectName = (input.project ?? "").trim();
|
|
670
|
+
if (isAbleTimeNoneToken(projectName)) {
|
|
671
|
+
throw new Error("AbleTime tasks stay in a project \u2014 pass a project name or id");
|
|
672
|
+
}
|
|
673
|
+
const project = await resolveAbleTimeProject(projectName, opts);
|
|
674
|
+
updateArgs.project = project.id;
|
|
675
|
+
updateArgs.project_id = project.id;
|
|
676
|
+
}
|
|
677
|
+
if (input.parent !== void 0) {
|
|
678
|
+
if (isAbleTimeNoneToken(input.parent)) {
|
|
679
|
+
updateArgs.parent = null;
|
|
680
|
+
updateArgs.parent_id = null;
|
|
681
|
+
updateArgs.related_task_id = null;
|
|
682
|
+
} else {
|
|
683
|
+
const parent = (input.parent ?? "").trim();
|
|
684
|
+
updateArgs.parent = parent;
|
|
685
|
+
updateArgs.parent_id = parent;
|
|
686
|
+
updateArgs.related_task_id = parent;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
const hasUpdateFields = Boolean(title) || description !== void 0 || input.labels !== void 0 || input.project !== void 0 || input.parent !== void 0;
|
|
690
|
+
if (!hasUpdateFields && !state && !addRelations && !removeRelations) {
|
|
691
|
+
throw new Error(
|
|
692
|
+
"abletime_update_task needs at least one of title, description, state, labels, project, parent, relations, removeRelations"
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
if (state) {
|
|
696
|
+
await callAbleTimeTool(
|
|
697
|
+
"set_task_state",
|
|
698
|
+
{ ...taskIdArgs(id), state },
|
|
699
|
+
opts
|
|
700
|
+
).catch(async () => {
|
|
701
|
+
await callAbleTimeTool(
|
|
702
|
+
"update_task",
|
|
703
|
+
{ ...updateArgs, state },
|
|
704
|
+
opts
|
|
705
|
+
);
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
if (hasUpdateFields) {
|
|
709
|
+
await callAbleTimeTool("update_task", updateArgs, opts);
|
|
710
|
+
}
|
|
711
|
+
for (const rel of removeRelations ?? []) {
|
|
712
|
+
const related = rel.issue.trim() || id;
|
|
713
|
+
const target = ableTimeDependencyTarget(id, rel.type, related);
|
|
714
|
+
await setAbleTimeTaskDependency(target.taskId, null, opts);
|
|
715
|
+
}
|
|
716
|
+
for (const rel of addRelations ?? []) {
|
|
717
|
+
const related = rel.issue.trim();
|
|
718
|
+
if (!related) throw new Error("AbleTime relations.issue is required (id or CRM-232)");
|
|
719
|
+
const target = ableTimeDependencyTarget(id, rel.type, related);
|
|
720
|
+
if (!target.dependsOn) {
|
|
721
|
+
await setAbleTimeTaskDependency(target.taskId, null, opts);
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
await setAbleTimeTaskDependency(target.taskId, target.dependsOn, opts);
|
|
725
|
+
}
|
|
726
|
+
return getAbleTimeTask(id, opts);
|
|
727
|
+
}
|
|
728
|
+
async function resolveAbleTimeProject(projectId, opts) {
|
|
729
|
+
const wanted = projectId?.trim().toLowerCase();
|
|
730
|
+
const fromOrientation = (await getAbleTimeOrientation(opts).catch(() => null))?.projects ?? [];
|
|
731
|
+
const listed = fromOrientation.length > 0 ? fromOrientation : await listAbleTimeProjects(opts);
|
|
732
|
+
if (listed.length === 0) {
|
|
733
|
+
throw new Error(
|
|
734
|
+
rewriteAbleTimeError(
|
|
735
|
+
"AbleTime has no projects to create a task in \u2014 create a project in AbleTime first."
|
|
736
|
+
)
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
if (!wanted) return listed[0];
|
|
740
|
+
const found = listed.find(
|
|
741
|
+
(project) => project.id.toLowerCase() === wanted || project.name.toLowerCase() === wanted
|
|
742
|
+
);
|
|
743
|
+
if (!found) {
|
|
744
|
+
const names = listed.map((project) => project.name).join(", ") || "(none)";
|
|
745
|
+
throw new Error(`AbleTime project not found: ${projectId}. Known: ${names}`);
|
|
746
|
+
}
|
|
747
|
+
return found;
|
|
748
|
+
}
|
|
749
|
+
function resolveAbleTimeCategory(project, categoryId) {
|
|
750
|
+
if (project.categories.length === 0) return void 0;
|
|
751
|
+
const wanted = categoryId?.trim().toLowerCase();
|
|
752
|
+
if (!wanted) return project.categories[0];
|
|
753
|
+
return project.categories.find(
|
|
754
|
+
(category) => category.id.toLowerCase() === wanted || category.name.toLowerCase() === wanted
|
|
755
|
+
) ?? project.categories[0];
|
|
756
|
+
}
|
|
757
|
+
function titlesMatch(a, b) {
|
|
758
|
+
return a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
759
|
+
}
|
|
760
|
+
async function ensureAbleTimeTask(input, opts) {
|
|
761
|
+
const title = input.title.trim();
|
|
762
|
+
if (!title) throw new Error("AbleTime task title is required");
|
|
763
|
+
const searched = await searchAbleTimeTasks(title, opts).catch(() => []);
|
|
764
|
+
const exact = searched.find((task) => titlesMatch(task.identifier, title)) || searched.find((task) => titlesMatch(task.title, title));
|
|
765
|
+
if (exact) return { ...exact, created: false };
|
|
766
|
+
const created = await createAbleTimeTask(
|
|
767
|
+
{
|
|
768
|
+
title,
|
|
769
|
+
description: input.description,
|
|
770
|
+
projectId: input.projectId,
|
|
771
|
+
categoryId: input.categoryId,
|
|
772
|
+
state: "todo"
|
|
773
|
+
},
|
|
774
|
+
opts
|
|
775
|
+
);
|
|
776
|
+
return { ...created, created: true };
|
|
777
|
+
}
|
|
778
|
+
async function listAbleTimeAssignedIssues(opts) {
|
|
779
|
+
if (!opts?.token && !isAbleTimeConnected()) {
|
|
780
|
+
throw new Error("AbleTime is not connected \u2014 paste a personal access token in Account settings");
|
|
781
|
+
}
|
|
782
|
+
const [orientation, listed] = await Promise.all([
|
|
783
|
+
getAbleTimeOrientation(opts).catch(() => null),
|
|
784
|
+
listAbleTimeTasks(opts)
|
|
785
|
+
]);
|
|
786
|
+
const issues = listed.map(toAbleTimeIssueInfo);
|
|
787
|
+
return {
|
|
788
|
+
viewer: orientation?.viewer ?? {},
|
|
789
|
+
issues
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
async function verifyAbleTimeConnection(input) {
|
|
793
|
+
const token = assertAbleTimeCredential(input.token);
|
|
794
|
+
const host = input.host?.trim() || void 0;
|
|
795
|
+
const orientation = await getAbleTimeOrientation({ token, host });
|
|
796
|
+
saveAbleTimeConnection({
|
|
797
|
+
accessToken: token,
|
|
798
|
+
host,
|
|
799
|
+
viewerName: orientation.viewer.name
|
|
800
|
+
});
|
|
801
|
+
return orientation.viewer;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
export {
|
|
805
|
+
issueAttachmentWorktreePath,
|
|
806
|
+
hostMatches,
|
|
807
|
+
looksLikeHttpUrl,
|
|
808
|
+
extractHttpUrls,
|
|
809
|
+
mergeIssueAttachments,
|
|
810
|
+
downloadAuthenticatedFile,
|
|
811
|
+
downloadAbleTimeAttachment,
|
|
812
|
+
taskUrl,
|
|
813
|
+
mapAbleTimeTask,
|
|
814
|
+
toAbleTimeIssueInfo,
|
|
815
|
+
issueAttachmentForAbleTimeTask,
|
|
816
|
+
getAbleTimeOrientation,
|
|
817
|
+
listAbleTimeProjects,
|
|
818
|
+
listAbleTimeTasks,
|
|
819
|
+
searchAbleTimeTasks,
|
|
820
|
+
getAbleTimeTask,
|
|
821
|
+
createAbleTimeTask,
|
|
822
|
+
commentAbleTimeTask,
|
|
823
|
+
ableTimeDependencyTarget,
|
|
824
|
+
updateAbleTimeTask,
|
|
825
|
+
ensureAbleTimeTask,
|
|
826
|
+
listAbleTimeAssignedIssues,
|
|
827
|
+
verifyAbleTimeConnection
|
|
828
|
+
};
|