@sideboard-ai/core 0.1.202 → 0.1.203
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-KGPD3UUJ.js → abletime-JFGZBDHH.js} +4 -2
- package/dist/{abletime-345SE3B4.js → abletime-SISJEPJ3.js} +4 -2
- package/dist/abletime-oauth-7TD5Z2QV.js +265 -0
- package/dist/abletime-oauth-OSQYFB3E.js +40 -0
- package/dist/abletime-rest-QPJTDLKX.js +25 -0
- package/dist/abletime-rest-ZJGWYALM.js +24 -0
- package/dist/{agents-OXX3ONVY.js → agents-3755WYQM.js} +4 -4
- package/dist/{agents-573ZDIX6.js → agents-ZMWL43SP.js} +4 -4
- package/dist/{app-settings-V6IHESEQ.js → app-settings-LGHMNHSP.js} +1 -1
- package/dist/{app-settings-AXCURTZJ.js → app-settings-YEPF46IJ.js} +1 -1
- package/dist/{chunk-7NKY2OX7.js → chunk-2DVCCLII.js} +78 -62
- package/dist/chunk-3PYH2RLA.js +388 -0
- package/dist/{chunk-A2TAKYSG.js → chunk-5IXJTUST.js} +4 -4
- package/dist/chunk-6VEIKW5C.js +390 -0
- package/dist/chunk-BFEBFGCS.js +250 -0
- package/dist/{chunk-STPZQR5B.js → chunk-CMGRV36E.js} +1 -1
- package/dist/{chunk-PGQGP4FD.js → chunk-FZ6IBUKY.js} +36 -218
- package/dist/{chunk-7AHAABKN.js → chunk-FZYYEMET.js} +36 -228
- package/dist/{chunk-54RSAMBH.js → chunk-LCU6GBZP.js} +2 -2
- package/dist/chunk-MHPINQKY.js +260 -0
- package/dist/chunk-PJHFNFXA.js +243 -0
- package/dist/{chunk-CXFW5LIH.js → chunk-QFBBUFD4.js} +38 -3
- package/dist/{chunk-RPX6VDW7.js → chunk-QSEF2YVQ.js} +1 -1
- package/dist/{chunk-LJHC62LZ.js → chunk-TNDUTGDV.js} +2 -2
- package/dist/{chunk-27GGXX27.js → chunk-TZNJKZ2A.js} +82 -66
- package/dist/{chunk-NIVF5NDN.js → chunk-X2TFUFIS.js} +4 -4
- package/dist/{chunk-ISV5IFH3.js → chunk-X743YN5Y.js} +38 -3
- package/dist/{coordinator-prompt-TY5FHAXC.js → coordinator-prompt-2B2JA2IE.js} +3 -3
- package/dist/{coordinator-prompt-BH4EHFOO.js → coordinator-prompt-U4PEZDAW.js} +3 -3
- package/dist/{global-workspace-NLLAPQJI.js → global-workspace-JPJ3FKKY.js} +3 -3
- package/dist/{global-workspace-5AZEEQRU.js → global-workspace-PU7ZS3LS.js} +3 -3
- package/dist/index.cjs +912 -142
- package/dist/index.d.cts +61 -9
- package/dist/index.d.ts +61 -9
- package/dist/index.js +63 -18
- package/dist/mcp/run-stdio.cjs +843 -114
- package/dist/mcp/run-stdio.js +22 -20
- package/dist/{orchestrator-JNSQDRZI.js → orchestrator-3A6KUTNI.js} +5 -5
- package/dist/{orchestrator-NCYKEI3E.js → orchestrator-Q33M24ZJ.js} +6 -6
- package/dist/{workspaces-RP76JH3J.js → workspaces-IMG7ZRUQ.js} +3 -3
- package/dist/{workspaces-PEWYWVHZ.js → workspaces-U6P6DB44.js} +3 -3
- package/dist/{worktree-ZQEUKSNE.js → worktree-FL3UIUKQ.js} +2 -2
- package/dist/{worktree-72TZWUSH.js → worktree-JSCDJPBU.js} +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_ABLETIME_HOST,
|
|
5
|
+
ableTimeCredentialKind,
|
|
6
|
+
normalizeAbleTimeCredential,
|
|
7
|
+
normalizeAbleTimeHost,
|
|
8
|
+
rewriteAbleTimeError
|
|
9
|
+
} from "./chunk-PJHFNFXA.js";
|
|
10
|
+
import {
|
|
11
|
+
httpFetch
|
|
12
|
+
} from "./chunk-VKQGAWHY.js";
|
|
13
|
+
import {
|
|
14
|
+
getAbleTimeAccessToken,
|
|
15
|
+
getAbleTimeHost
|
|
16
|
+
} from "./chunk-X743YN5Y.js";
|
|
17
|
+
|
|
18
|
+
// src/integrations/abletime-rest.ts
|
|
19
|
+
var ABLETIME_REST_PREFIX = "/api/public/v2";
|
|
20
|
+
var ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/i;
|
|
21
|
+
function asRecord(value) {
|
|
22
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
23
|
+
}
|
|
24
|
+
function asString(value) {
|
|
25
|
+
return typeof value === "string" ? value.trim() : "";
|
|
26
|
+
}
|
|
27
|
+
function asList(value) {
|
|
28
|
+
if (Array.isArray(value)) return value;
|
|
29
|
+
const record = asRecord(value);
|
|
30
|
+
if (!record) return [];
|
|
31
|
+
for (const key of ["data", "tasks", "projects", "users", "items", "results"]) {
|
|
32
|
+
if (Array.isArray(record[key])) return record[key];
|
|
33
|
+
}
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
function ableTimeDocFromText(text) {
|
|
37
|
+
const trimmed = text.trim();
|
|
38
|
+
if (!trimmed) return { type: "doc", content: [] };
|
|
39
|
+
return {
|
|
40
|
+
type: "doc",
|
|
41
|
+
content: [{ type: "paragraph", content: [{ type: "text", text: trimmed }] }]
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function textFromAbleTimeDoc(value) {
|
|
45
|
+
if (typeof value === "string") return value.trim();
|
|
46
|
+
const record = asRecord(value);
|
|
47
|
+
if (!record) return "";
|
|
48
|
+
if (typeof record.text === "string" && record.text.trim()) return record.text.trim();
|
|
49
|
+
const chunks = [];
|
|
50
|
+
const walk = (node) => {
|
|
51
|
+
const rec = asRecord(node);
|
|
52
|
+
if (!rec) return;
|
|
53
|
+
if (typeof rec.text === "string" && rec.text) chunks.push(rec.text);
|
|
54
|
+
if (Array.isArray(rec.content)) rec.content.forEach(walk);
|
|
55
|
+
};
|
|
56
|
+
walk(record);
|
|
57
|
+
return chunks.join("").trim();
|
|
58
|
+
}
|
|
59
|
+
function abletimeRestUrl(path, host, query) {
|
|
60
|
+
const base = `${normalizeAbleTimeHost(host)}${ABLETIME_REST_PREFIX}`;
|
|
61
|
+
const suffix = path.startsWith("/") ? path : `/${path}`;
|
|
62
|
+
const url = new URL(`${base}${suffix}`);
|
|
63
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
64
|
+
if (value) url.searchParams.set(key, value);
|
|
65
|
+
}
|
|
66
|
+
return url.href;
|
|
67
|
+
}
|
|
68
|
+
function looksLikeAbleTimeUlid(id) {
|
|
69
|
+
return ULID_RE.test(id.trim());
|
|
70
|
+
}
|
|
71
|
+
async function abletimeRestRequest(path, opts = {}) {
|
|
72
|
+
const raw = opts.token ?? getAbleTimeAccessToken();
|
|
73
|
+
const token = normalizeAbleTimeCredential(raw ?? "");
|
|
74
|
+
if (!token) {
|
|
75
|
+
throw new Error("AbleTime is not connected \u2014 paste an API key or personal access token in Account settings");
|
|
76
|
+
}
|
|
77
|
+
const host = opts.host ?? (opts.token ? DEFAULT_ABLETIME_HOST : getAbleTimeHost());
|
|
78
|
+
const url = abletimeRestUrl(path, host, opts.query);
|
|
79
|
+
const method = (opts.method ?? "GET").toUpperCase();
|
|
80
|
+
const res = await httpFetch(url, {
|
|
81
|
+
method,
|
|
82
|
+
headers: {
|
|
83
|
+
Accept: "application/json",
|
|
84
|
+
Authorization: `Bearer ${token}`,
|
|
85
|
+
...opts.body !== void 0 ? { "Content-Type": "application/json" } : {}
|
|
86
|
+
},
|
|
87
|
+
...opts.body !== void 0 ? { body: JSON.stringify(opts.body) } : {}
|
|
88
|
+
});
|
|
89
|
+
const text = await res.text().catch(() => "");
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
rewriteAbleTimeError(
|
|
93
|
+
`AbleTime API error ${res.status}${text ? `: ${text.slice(0, 240)}` : ""}`
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
if (!text.trim()) return null;
|
|
98
|
+
try {
|
|
99
|
+
return JSON.parse(text);
|
|
100
|
+
} catch {
|
|
101
|
+
throw new Error(`AbleTime API returned a non-JSON response: ${text.slice(0, 200)}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function restPages(path, opts, maxPages = 20) {
|
|
105
|
+
const items = [];
|
|
106
|
+
let cursor;
|
|
107
|
+
for (let i = 0; i < maxPages; i += 1) {
|
|
108
|
+
const raw = await abletimeRestRequest(path, {
|
|
109
|
+
...opts,
|
|
110
|
+
query: { ...opts.query, ...cursor ? { cursor } : {} }
|
|
111
|
+
});
|
|
112
|
+
items.push(...asList(raw));
|
|
113
|
+
const page = asRecord(asRecord(raw)?.page);
|
|
114
|
+
const next = page ? asString(page.nextCursor) : "";
|
|
115
|
+
if (!next) break;
|
|
116
|
+
cursor = next;
|
|
117
|
+
}
|
|
118
|
+
return items;
|
|
119
|
+
}
|
|
120
|
+
function userDisplayName(raw) {
|
|
121
|
+
const rec = asRecord(raw);
|
|
122
|
+
if (!rec) return null;
|
|
123
|
+
const id = asString(rec.userId) || asString(rec.id);
|
|
124
|
+
const name = [asString(rec.firstName), asString(rec.lastName)].filter(Boolean).join(" ") || asString(rec.username) || asString(rec.email) || asString(rec.name);
|
|
125
|
+
if (!name) return null;
|
|
126
|
+
return { id: id || void 0, name };
|
|
127
|
+
}
|
|
128
|
+
function restTaskToRaw(raw, users) {
|
|
129
|
+
const rec = asRecord(raw);
|
|
130
|
+
if (!rec) return null;
|
|
131
|
+
const id = asString(rec.timeflowTaskId) || asString(rec.taskId) || asString(rec.id);
|
|
132
|
+
const identifier = asString(rec.taskRef) || asString(rec.reference) || id;
|
|
133
|
+
const title = asString(rec.title);
|
|
134
|
+
if (!id && !identifier && !title) return null;
|
|
135
|
+
const assigned = asString(rec.assignedUserId);
|
|
136
|
+
const assignee = assigned ? users.get(assigned) : void 0;
|
|
137
|
+
const comments = asList(rec.comments).map((item) => {
|
|
138
|
+
const comment = asRecord(item) ?? {};
|
|
139
|
+
const user = userDisplayName(comment);
|
|
140
|
+
return {
|
|
141
|
+
id: asString(comment.commentId) || asString(comment.id),
|
|
142
|
+
body: textFromAbleTimeDoc(comment.content) || asString(comment.contentSanitized) || asString(comment.body),
|
|
143
|
+
createdAt: asString(comment.createdAt) || asString(comment.lastUpdate),
|
|
144
|
+
user: user?.name || asString(comment.username)
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
return {
|
|
148
|
+
id,
|
|
149
|
+
taskId: id,
|
|
150
|
+
timeflowTaskId: id,
|
|
151
|
+
reference: identifier,
|
|
152
|
+
taskRef: identifier,
|
|
153
|
+
title: title || identifier,
|
|
154
|
+
state: asString(rec.taskState) || asString(rec.state),
|
|
155
|
+
description: textFromAbleTimeDoc(rec.description) || asString(rec.descriptionSanitized),
|
|
156
|
+
projectId: asString(rec.projectId),
|
|
157
|
+
categoryId: asString(rec.projectCategoryId) || asString(rec.categoryId),
|
|
158
|
+
tags: rec.tags,
|
|
159
|
+
comments,
|
|
160
|
+
createdAt: asString(rec.dateCreated) || asString(rec.createdAt),
|
|
161
|
+
updatedAt: asString(rec.lastUpdate) || asString(rec.updatedAt),
|
|
162
|
+
...assignee ? { assignee } : {}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function restProjectToRaw(raw) {
|
|
166
|
+
const rec = asRecord(raw);
|
|
167
|
+
if (!rec) return null;
|
|
168
|
+
const id = asString(rec.projectId) || asString(rec.id);
|
|
169
|
+
const name = asString(rec.projectName) || asString(rec.name);
|
|
170
|
+
if (!id && !name) return null;
|
|
171
|
+
const categories = asList(rec.categories).map((item) => {
|
|
172
|
+
const cat = asRecord(item) ?? {};
|
|
173
|
+
return {
|
|
174
|
+
id: asString(cat.projectCategoryId) || asString(cat.categoryId) || asString(cat.id),
|
|
175
|
+
name: asString(cat.categoryName) || asString(cat.name)
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
return { id, name, projectId: id, projectName: name, categories };
|
|
179
|
+
}
|
|
180
|
+
async function listUsers(opts) {
|
|
181
|
+
const users = /* @__PURE__ */ new Map();
|
|
182
|
+
for (const item of await restPages("/users", opts).catch(() => [])) {
|
|
183
|
+
const user = userDisplayName(item);
|
|
184
|
+
const rec = asRecord(item);
|
|
185
|
+
const id = user?.id || (rec ? asString(rec.userId) : "");
|
|
186
|
+
if (user && id) users.set(id, user);
|
|
187
|
+
}
|
|
188
|
+
return users;
|
|
189
|
+
}
|
|
190
|
+
async function listProjectsRaw(opts) {
|
|
191
|
+
const items = await restPages("/projects", { ...opts, query: { expand: "categories" } });
|
|
192
|
+
return items.map(restProjectToRaw).filter((item) => Boolean(item));
|
|
193
|
+
}
|
|
194
|
+
async function listTasksRaw(opts) {
|
|
195
|
+
const [users, items] = await Promise.all([
|
|
196
|
+
listUsers(opts),
|
|
197
|
+
restPages("/tasks", {
|
|
198
|
+
...opts,
|
|
199
|
+
query: {
|
|
200
|
+
...opts.projectId ? { projectId: opts.projectId } : {}
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
]);
|
|
204
|
+
let tasks = items.map((item) => restTaskToRaw(item, users)).filter((item) => Boolean(item));
|
|
205
|
+
const needle = opts.query?.trim().toLowerCase();
|
|
206
|
+
if (needle) {
|
|
207
|
+
tasks = tasks.filter((task) => {
|
|
208
|
+
const hay = [task.title, task.reference, task.taskRef, task.id].map((value) => String(value ?? "").toLowerCase()).join(" ");
|
|
209
|
+
const tags = Array.isArray(task.tags) ? task.tags.join(" ").toLowerCase() : "";
|
|
210
|
+
return hay.includes(needle) || tags.includes(needle);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return tasks;
|
|
214
|
+
}
|
|
215
|
+
async function resolveTaskId(id, opts) {
|
|
216
|
+
const wanted = id.trim();
|
|
217
|
+
if (!wanted) throw new Error("AbleTime task id is required");
|
|
218
|
+
if (looksLikeAbleTimeUlid(wanted)) return wanted;
|
|
219
|
+
const tasks = await listTasksRaw(opts);
|
|
220
|
+
const match = tasks.find((task) => {
|
|
221
|
+
const ref = String(task.reference ?? task.taskRef ?? "").toLowerCase();
|
|
222
|
+
const taskId = String(task.id ?? "").toLowerCase();
|
|
223
|
+
return ref === wanted.toLowerCase() || taskId === wanted.toLowerCase();
|
|
224
|
+
});
|
|
225
|
+
if (!match?.id) throw new Error(`AbleTime task not found: ${id}`);
|
|
226
|
+
return String(match.id);
|
|
227
|
+
}
|
|
228
|
+
async function getTaskRaw(id, opts) {
|
|
229
|
+
const taskId = await resolveTaskId(id, opts);
|
|
230
|
+
const [users, raw] = await Promise.all([
|
|
231
|
+
listUsers(opts),
|
|
232
|
+
abletimeRestRequest(`/tasks/${encodeURIComponent(taskId)}`, {
|
|
233
|
+
...opts,
|
|
234
|
+
query: { expand: "comments" }
|
|
235
|
+
})
|
|
236
|
+
]);
|
|
237
|
+
const task = restTaskToRaw(raw, users);
|
|
238
|
+
if (!task) throw new Error(`AbleTime task not found: ${id}`);
|
|
239
|
+
return task;
|
|
240
|
+
}
|
|
241
|
+
function firstStringArg(args, keys) {
|
|
242
|
+
for (const key of keys) {
|
|
243
|
+
const value = asString(args[key]);
|
|
244
|
+
if (value) return value;
|
|
245
|
+
}
|
|
246
|
+
return "";
|
|
247
|
+
}
|
|
248
|
+
async function callAbleTimeRestTool(name, args = {}, opts) {
|
|
249
|
+
const token = normalizeAbleTimeCredential(opts?.token ?? getAbleTimeAccessToken() ?? "");
|
|
250
|
+
if (ableTimeCredentialKind(token) !== "org") {
|
|
251
|
+
throw new Error("AbleTime REST transport is for organization API keys (atk_\u2026)");
|
|
252
|
+
}
|
|
253
|
+
const requestOpts = { ...opts, token };
|
|
254
|
+
switch (name) {
|
|
255
|
+
case "orientation": {
|
|
256
|
+
const [users, projects, tasks] = await Promise.all([
|
|
257
|
+
listUsers(requestOpts),
|
|
258
|
+
listProjectsRaw(requestOpts),
|
|
259
|
+
listTasksRaw(requestOpts)
|
|
260
|
+
]);
|
|
261
|
+
const viewer = [...users.values()].find((user) => user.name) ?? [...users.values()][0] ?? { name: "AbleTime" };
|
|
262
|
+
return { viewer, projects, tasks };
|
|
263
|
+
}
|
|
264
|
+
case "list_projects":
|
|
265
|
+
return { data: await listProjectsRaw(requestOpts) };
|
|
266
|
+
case "list_users":
|
|
267
|
+
return { data: [...(await listUsers(requestOpts)).values()] };
|
|
268
|
+
case "list_tasks":
|
|
269
|
+
return {
|
|
270
|
+
data: await listTasksRaw({
|
|
271
|
+
...requestOpts,
|
|
272
|
+
projectId: firstStringArg(args, ["project_id", "projectId", "project"])
|
|
273
|
+
})
|
|
274
|
+
};
|
|
275
|
+
case "search_tasks":
|
|
276
|
+
return {
|
|
277
|
+
data: await listTasksRaw({
|
|
278
|
+
...requestOpts,
|
|
279
|
+
query: firstStringArg(args, ["query", "q"])
|
|
280
|
+
})
|
|
281
|
+
};
|
|
282
|
+
case "get_task":
|
|
283
|
+
return await getTaskRaw(firstStringArg(args, ["id", "task_id", "task"]), requestOpts);
|
|
284
|
+
case "create_task": {
|
|
285
|
+
const title = firstStringArg(args, ["title"]);
|
|
286
|
+
if (!title) throw new Error("AbleTime task title is required");
|
|
287
|
+
const projectId = firstStringArg(args, ["project_id", "projectId", "project"]);
|
|
288
|
+
if (!projectId) throw new Error("AbleTime project is required");
|
|
289
|
+
const projects = await listProjectsRaw(requestOpts);
|
|
290
|
+
const project = projects.find(
|
|
291
|
+
(item) => String(item.id).toLowerCase() === projectId.toLowerCase() || String(item.name).toLowerCase() === projectId.toLowerCase()
|
|
292
|
+
) ?? projects[0];
|
|
293
|
+
if (!project) throw new Error("AbleTime has no projects to create a task in");
|
|
294
|
+
const categories = asList(project.categories);
|
|
295
|
+
const wantedCategory = firstStringArg(args, ["category_id", "categoryId", "category"]);
|
|
296
|
+
const category = categories.map((item) => asRecord(item)).find((item) => {
|
|
297
|
+
if (!item || !wantedCategory) return false;
|
|
298
|
+
return asString(item.id).toLowerCase() === wantedCategory.toLowerCase() || asString(item.name).toLowerCase() === wantedCategory.toLowerCase();
|
|
299
|
+
}) ?? asRecord(categories[0]);
|
|
300
|
+
const created = await abletimeRestRequest("/tasks", {
|
|
301
|
+
...requestOpts,
|
|
302
|
+
method: "POST",
|
|
303
|
+
body: {
|
|
304
|
+
projectId: project.id,
|
|
305
|
+
projectCategoryId: category ? asString(category.id) : void 0,
|
|
306
|
+
title
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
const state = firstStringArg(args, ["state"]);
|
|
310
|
+
const createdId = asString(asRecord(created)?.timeflowTaskId);
|
|
311
|
+
const description = asString(args.description);
|
|
312
|
+
if (createdId && description) {
|
|
313
|
+
await abletimeRestRequest(`/tasks/${encodeURIComponent(createdId)}`, {
|
|
314
|
+
...requestOpts,
|
|
315
|
+
method: "PATCH",
|
|
316
|
+
body: { description: ableTimeDocFromText(description) }
|
|
317
|
+
}).catch(() => void 0);
|
|
318
|
+
}
|
|
319
|
+
if (createdId && state && state !== "todo") {
|
|
320
|
+
await abletimeRestRequest(`/tasks/${encodeURIComponent(createdId)}/state`, {
|
|
321
|
+
...requestOpts,
|
|
322
|
+
method: "POST",
|
|
323
|
+
body: { taskState: state }
|
|
324
|
+
}).catch(() => void 0);
|
|
325
|
+
}
|
|
326
|
+
return createdId ? await getTaskRaw(createdId, requestOpts) : restTaskToRaw(created, await listUsers(requestOpts));
|
|
327
|
+
}
|
|
328
|
+
case "update_task": {
|
|
329
|
+
const id = await resolveTaskId(firstStringArg(args, ["id", "task_id", "task"]), requestOpts);
|
|
330
|
+
const patch = {};
|
|
331
|
+
const title = firstStringArg(args, ["title"]);
|
|
332
|
+
if (title) patch.title = title;
|
|
333
|
+
if (args.description !== void 0) {
|
|
334
|
+
patch.description = args.description == null || args.description === "" ? null : ableTimeDocFromText(String(args.description));
|
|
335
|
+
}
|
|
336
|
+
if (args.tags !== void 0 || args.labels !== void 0) {
|
|
337
|
+
patch.tags = args.tags ?? args.labels ?? args.tag_names;
|
|
338
|
+
}
|
|
339
|
+
if (Object.keys(patch).length > 0) {
|
|
340
|
+
await abletimeRestRequest(`/tasks/${encodeURIComponent(id)}`, {
|
|
341
|
+
...requestOpts,
|
|
342
|
+
method: "PATCH",
|
|
343
|
+
body: patch
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return await getTaskRaw(id, requestOpts);
|
|
347
|
+
}
|
|
348
|
+
case "set_task_state": {
|
|
349
|
+
const id = await resolveTaskId(firstStringArg(args, ["id", "task_id", "task"]), requestOpts);
|
|
350
|
+
const state = firstStringArg(args, ["state", "taskState"]);
|
|
351
|
+
if (!state) throw new Error("AbleTime task state is required");
|
|
352
|
+
await abletimeRestRequest(`/tasks/${encodeURIComponent(id)}/state`, {
|
|
353
|
+
...requestOpts,
|
|
354
|
+
method: "POST",
|
|
355
|
+
body: { taskState: state }
|
|
356
|
+
});
|
|
357
|
+
return await getTaskRaw(id, requestOpts);
|
|
358
|
+
}
|
|
359
|
+
case "create_comment": {
|
|
360
|
+
const id = await resolveTaskId(firstStringArg(args, ["id", "task_id", "task"]), requestOpts);
|
|
361
|
+
const body = firstStringArg(args, ["body", "comment", "text"]);
|
|
362
|
+
if (!body) throw new Error("AbleTime comment body is required");
|
|
363
|
+
const created = await abletimeRestRequest(`/tasks/${encodeURIComponent(id)}/comments`, {
|
|
364
|
+
...requestOpts,
|
|
365
|
+
method: "POST",
|
|
366
|
+
body: { content: ableTimeDocFromText(body) }
|
|
367
|
+
});
|
|
368
|
+
const rec = asRecord(created) ?? {};
|
|
369
|
+
return {
|
|
370
|
+
id: asString(rec.commentId) || void 0,
|
|
371
|
+
body: textFromAbleTimeDoc(rec.content) || body
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
case "set_task_dependency":
|
|
375
|
+
throw new Error(
|
|
376
|
+
"AbleTime organization API keys cannot set task dependencies on REST yet \u2014 use a personal access token (apt_\u2026) for MCP project-management tools."
|
|
377
|
+
);
|
|
378
|
+
default:
|
|
379
|
+
throw new Error(`AbleTime organization API keys do not support ${name} on REST`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export {
|
|
384
|
+
ABLETIME_REST_PREFIX,
|
|
385
|
+
ableTimeDocFromText,
|
|
386
|
+
textFromAbleTimeDoc,
|
|
387
|
+
abletimeRestUrl,
|
|
388
|
+
looksLikeAbleTimeUlid,
|
|
389
|
+
callAbleTimeRestTool
|
|
390
|
+
};
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import {
|
|
2
|
+
httpFetch
|
|
3
|
+
} from "./chunk-N7WZ6NIN.js";
|
|
4
|
+
import {
|
|
5
|
+
getAbleTimeHost,
|
|
6
|
+
isAbleTimeConnected
|
|
7
|
+
} from "./chunk-QFBBUFD4.js";
|
|
8
|
+
|
|
9
|
+
// src/integrations/abletime-mcp.ts
|
|
10
|
+
var DEFAULT_ABLETIME_HOST = "https://track.abletime.com";
|
|
11
|
+
var ABLETIME_MCP_PATH = "/api/public/v2/mcp";
|
|
12
|
+
var ABLETIME_MCP_PM_PATH = "/api/public/v2/mcp/pm";
|
|
13
|
+
var MCP_PROTOCOL_VERSION = "2025-03-26";
|
|
14
|
+
var ABLETIME_PM_TOOL_NAMES = [
|
|
15
|
+
"set_task_assignee",
|
|
16
|
+
"set_task_priority",
|
|
17
|
+
"set_task_stage",
|
|
18
|
+
"set_task_dependency",
|
|
19
|
+
"set_task_blocked",
|
|
20
|
+
"set_task_locked",
|
|
21
|
+
"set_task_schedule",
|
|
22
|
+
"clear_task_schedule",
|
|
23
|
+
"archive_task",
|
|
24
|
+
"delete_task"
|
|
25
|
+
];
|
|
26
|
+
function abletimeToolUsesPm(name) {
|
|
27
|
+
return ABLETIME_PM_TOOL_NAMES.includes(name);
|
|
28
|
+
}
|
|
29
|
+
function normalizeAbleTimeHost(raw) {
|
|
30
|
+
const trimmed = raw?.trim() || DEFAULT_ABLETIME_HOST;
|
|
31
|
+
const withScheme = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
32
|
+
return withScheme.replace(/\/+$/, "");
|
|
33
|
+
}
|
|
34
|
+
function abletimeMcpUrl(host, opts) {
|
|
35
|
+
const path = opts?.pm ? ABLETIME_MCP_PM_PATH : ABLETIME_MCP_PATH;
|
|
36
|
+
return `${normalizeAbleTimeHost(host)}${path}`;
|
|
37
|
+
}
|
|
38
|
+
var ABLETIME_PAT_PREFIX = "apt_";
|
|
39
|
+
var ABLETIME_ORG_KEY_PREFIX = "atk_";
|
|
40
|
+
var ORG_KEY_HINT = "AbleTime MCP needs a personal access token (apt_\u2026) from Edit Profile \u2192 API Access, not an organization API key (atk_\u2026) from Settings \u2192 Integrations \u2192 API Keys.";
|
|
41
|
+
var REJECTED_CREDENTIAL_HINT = "AbleTime rejected this credential (unknown, revoked, or expired). Use a personal access token (apt_\u2026) from Edit Profile \u2192 API Access \u2014 organization API keys (atk_\u2026) cannot call MCP. Tokens are shown once; rotating replaces the previous one.";
|
|
42
|
+
var REWRITE_APPLIED = /— Enable Agent access \(MCP\)|— AbleTime MCP needs a personal access token|— AbleTime plan does not include API access|— AbleTime rejected this credential/;
|
|
43
|
+
function normalizeAbleTimeCredential(raw) {
|
|
44
|
+
return raw.trim().replace(/^bearer\s+/i, "").trim();
|
|
45
|
+
}
|
|
46
|
+
function ableTimeCredentialKind(raw) {
|
|
47
|
+
const token = normalizeAbleTimeCredential(raw).toLowerCase();
|
|
48
|
+
if (token.startsWith(ABLETIME_PAT_PREFIX)) return "pat";
|
|
49
|
+
if (token.startsWith(ABLETIME_ORG_KEY_PREFIX)) return "org";
|
|
50
|
+
return "unknown";
|
|
51
|
+
}
|
|
52
|
+
function assertAbleTimeMcpCredential(raw) {
|
|
53
|
+
const token = normalizeAbleTimeCredential(raw);
|
|
54
|
+
if (!token) {
|
|
55
|
+
throw new Error("AbleTime personal access token is required");
|
|
56
|
+
}
|
|
57
|
+
if (ableTimeCredentialKind(token) === "org") {
|
|
58
|
+
throw new Error(ORG_KEY_HINT);
|
|
59
|
+
}
|
|
60
|
+
return token;
|
|
61
|
+
}
|
|
62
|
+
function assertAbleTimeCredential(raw) {
|
|
63
|
+
const token = normalizeAbleTimeCredential(raw);
|
|
64
|
+
if (!token) {
|
|
65
|
+
throw new Error("AbleTime API key or personal access token is required");
|
|
66
|
+
}
|
|
67
|
+
return token;
|
|
68
|
+
}
|
|
69
|
+
function rewriteAbleTimeError(message) {
|
|
70
|
+
if (REWRITE_APPLIED.test(message)) return message;
|
|
71
|
+
if (/INTEGRATION_AGENT_ACCESS_DISABLED/i.test(message)) {
|
|
72
|
+
return `${message} \u2014 Enable Agent access (MCP) in AbleTime Settings \u2192 Integrations \u2192 API Keys.`;
|
|
73
|
+
}
|
|
74
|
+
if (/INTEGRATION_PAT_REQUIRED/i.test(message)) {
|
|
75
|
+
return `${message} \u2014 ${ORG_KEY_HINT}`;
|
|
76
|
+
}
|
|
77
|
+
if (/INTEGRATION_PLAN_REQUIRED/i.test(message)) {
|
|
78
|
+
return `${message} \u2014 AbleTime plan does not include API access.`;
|
|
79
|
+
}
|
|
80
|
+
if (/INTEGRATION_KEY_INVALID|INTEGRATION_KEY_MISSING|401|unauthorized|invalid token/i.test(message)) {
|
|
81
|
+
return `${message} \u2014 ${REJECTED_CREDENTIAL_HINT}`;
|
|
82
|
+
}
|
|
83
|
+
return message;
|
|
84
|
+
}
|
|
85
|
+
function asRecord(value) {
|
|
86
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
87
|
+
}
|
|
88
|
+
function parseSseJson(body) {
|
|
89
|
+
const blocks = body.split(/\n\n+/);
|
|
90
|
+
let last;
|
|
91
|
+
for (const block of blocks) {
|
|
92
|
+
const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).filter((line) => line && line !== "[DONE]").join("\n");
|
|
93
|
+
if (!data) continue;
|
|
94
|
+
try {
|
|
95
|
+
last = JSON.parse(data);
|
|
96
|
+
} catch {
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (last !== void 0) return last;
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(body);
|
|
102
|
+
} catch {
|
|
103
|
+
throw new Error(`AbleTime MCP returned a non-JSON response: ${body.slice(0, 200)}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function unwrapToolResult(result) {
|
|
107
|
+
const record = asRecord(result);
|
|
108
|
+
if (!record) return result;
|
|
109
|
+
const content = record.content;
|
|
110
|
+
if (Array.isArray(content)) {
|
|
111
|
+
const texts = content.map((part) => {
|
|
112
|
+
const item = asRecord(part);
|
|
113
|
+
if (!item) return "";
|
|
114
|
+
return typeof item.text === "string" ? item.text : "";
|
|
115
|
+
}).filter(Boolean);
|
|
116
|
+
if (texts.length === 1) {
|
|
117
|
+
const text = texts[0];
|
|
118
|
+
try {
|
|
119
|
+
return JSON.parse(text);
|
|
120
|
+
} catch {
|
|
121
|
+
return text;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (texts.length > 1) return texts.join("\n");
|
|
125
|
+
}
|
|
126
|
+
if (record.structuredContent != null) return record.structuredContent;
|
|
127
|
+
if (record.isError === true) {
|
|
128
|
+
const message = typeof record.message === "string" ? record.message : textsFromUnknown(result) || "AbleTime tool failed";
|
|
129
|
+
throw new Error(message);
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
function textsFromUnknown(value) {
|
|
134
|
+
const record = asRecord(value);
|
|
135
|
+
if (!record) return "";
|
|
136
|
+
if (typeof record.message === "string") return record.message;
|
|
137
|
+
if (typeof record.error === "string") return record.error;
|
|
138
|
+
return "";
|
|
139
|
+
}
|
|
140
|
+
async function abletimeMcpRequest(method, params, opts) {
|
|
141
|
+
const rawToken = opts?.token ?? await import("./abletime-oauth-OSQYFB3E.js").then((m) => m.ensureAbleTimeAccessToken());
|
|
142
|
+
if (!rawToken?.trim()) {
|
|
143
|
+
throw new Error("AbleTime is not connected \u2014 paste a personal access token in Account settings");
|
|
144
|
+
}
|
|
145
|
+
const token = assertAbleTimeMcpCredential(rawToken);
|
|
146
|
+
const url = abletimeMcpUrl(
|
|
147
|
+
opts?.host ?? (opts?.token ? DEFAULT_ABLETIME_HOST : getAbleTimeHost()),
|
|
148
|
+
{ pm: opts?.pm }
|
|
149
|
+
);
|
|
150
|
+
const res = await httpFetch(url, {
|
|
151
|
+
method: "POST",
|
|
152
|
+
headers: {
|
|
153
|
+
"Content-Type": "application/json",
|
|
154
|
+
Accept: "application/json, text/event-stream",
|
|
155
|
+
Authorization: `Bearer ${token}`,
|
|
156
|
+
"MCP-Protocol-Version": MCP_PROTOCOL_VERSION
|
|
157
|
+
},
|
|
158
|
+
body: JSON.stringify({
|
|
159
|
+
jsonrpc: "2.0",
|
|
160
|
+
id: 1,
|
|
161
|
+
method,
|
|
162
|
+
...params === void 0 ? {} : { params }
|
|
163
|
+
})
|
|
164
|
+
});
|
|
165
|
+
const body = await res.text().catch(() => "");
|
|
166
|
+
if (!res.ok) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
rewriteAbleTimeError(
|
|
169
|
+
`AbleTime MCP error ${res.status}${body ? `: ${body.slice(0, 240)}` : ""}`
|
|
170
|
+
)
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
174
|
+
const parsed = contentType.includes("text/event-stream") ? parseSseJson(body) : (() => {
|
|
175
|
+
try {
|
|
176
|
+
return JSON.parse(body);
|
|
177
|
+
} catch {
|
|
178
|
+
return parseSseJson(body);
|
|
179
|
+
}
|
|
180
|
+
})();
|
|
181
|
+
const rpc = asRecord(parsed);
|
|
182
|
+
if (rpc?.error) {
|
|
183
|
+
const detail = typeof rpc.error.message === "string" && rpc.error.message.trim() ? rpc.error.message : `AbleTime MCP error ${rpc.error.code ?? ""}`.trim();
|
|
184
|
+
throw new Error(rewriteAbleTimeError(detail));
|
|
185
|
+
}
|
|
186
|
+
if (rpc && "result" in rpc) return rpc.result;
|
|
187
|
+
return parsed;
|
|
188
|
+
}
|
|
189
|
+
async function initializeIfNeeded(opts) {
|
|
190
|
+
try {
|
|
191
|
+
await abletimeMcpRequest(
|
|
192
|
+
"initialize",
|
|
193
|
+
{
|
|
194
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
195
|
+
capabilities: {},
|
|
196
|
+
clientInfo: { name: "sideboard", version: "1.0.0" }
|
|
197
|
+
},
|
|
198
|
+
opts
|
|
199
|
+
);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
202
|
+
if (/already initialized|not (required|supported)|method not found|-32601/i.test(message)) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
throw err;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async function callAbleTimeTool(name, args = {}, opts) {
|
|
209
|
+
if (!opts?.token && !isAbleTimeConnected()) {
|
|
210
|
+
throw new Error("AbleTime is not connected \u2014 paste a personal access token in Account settings");
|
|
211
|
+
}
|
|
212
|
+
const rawToken = opts?.token ?? await import("./abletime-oauth-OSQYFB3E.js").then((m) => m.ensureAbleTimeAccessToken());
|
|
213
|
+
if (rawToken && ableTimeCredentialKind(rawToken) === "org") {
|
|
214
|
+
const { callAbleTimeRestTool } = await import("./abletime-rest-ZJGWYALM.js");
|
|
215
|
+
return callAbleTimeRestTool(name, args, { ...opts, token: rawToken });
|
|
216
|
+
}
|
|
217
|
+
const requestOpts = { ...opts, pm: opts?.pm ?? abletimeToolUsesPm(name) };
|
|
218
|
+
const call = () => abletimeMcpRequest("tools/call", { name, arguments: args }, requestOpts).then((result) => {
|
|
219
|
+
return unwrapToolResult(result);
|
|
220
|
+
});
|
|
221
|
+
try {
|
|
222
|
+
return await call();
|
|
223
|
+
} catch (err) {
|
|
224
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
225
|
+
if (/initialize|session|not initialized|-32000/i.test(message)) {
|
|
226
|
+
await initializeIfNeeded(requestOpts);
|
|
227
|
+
return await call();
|
|
228
|
+
}
|
|
229
|
+
throw err instanceof Error ? new Error(rewriteAbleTimeError(err.message)) : err;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export {
|
|
234
|
+
DEFAULT_ABLETIME_HOST,
|
|
235
|
+
ABLETIME_MCP_PATH,
|
|
236
|
+
ABLETIME_MCP_PM_PATH,
|
|
237
|
+
ABLETIME_PM_TOOL_NAMES,
|
|
238
|
+
abletimeToolUsesPm,
|
|
239
|
+
normalizeAbleTimeHost,
|
|
240
|
+
abletimeMcpUrl,
|
|
241
|
+
ABLETIME_PAT_PREFIX,
|
|
242
|
+
ABLETIME_ORG_KEY_PREFIX,
|
|
243
|
+
normalizeAbleTimeCredential,
|
|
244
|
+
ableTimeCredentialKind,
|
|
245
|
+
assertAbleTimeMcpCredential,
|
|
246
|
+
assertAbleTimeCredential,
|
|
247
|
+
rewriteAbleTimeError,
|
|
248
|
+
abletimeMcpRequest,
|
|
249
|
+
callAbleTimeTool
|
|
250
|
+
};
|