@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
|
@@ -1,198 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
} from "./chunk-
|
|
2
|
+
textFromAbleTimeDoc
|
|
3
|
+
} from "./chunk-3PYH2RLA.js";
|
|
4
|
+
import {
|
|
5
|
+
assertAbleTimeCredential,
|
|
6
|
+
callAbleTimeTool,
|
|
7
|
+
normalizeAbleTimeHost,
|
|
8
|
+
rewriteAbleTimeError
|
|
9
|
+
} from "./chunk-BFEBFGCS.js";
|
|
4
10
|
import {
|
|
5
|
-
getAbleTimeAccessToken,
|
|
6
|
-
getAbleTimeHost,
|
|
7
11
|
isAbleTimeConnected,
|
|
8
12
|
saveAbleTimeConnection
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
|
|
11
|
-
// src/integrations/abletime-mcp.ts
|
|
12
|
-
var DEFAULT_ABLETIME_HOST = "https://track.abletime.com";
|
|
13
|
-
var ABLETIME_MCP_PATH = "/api/public/v2/mcp";
|
|
14
|
-
var ABLETIME_MCP_PM_PATH = "/api/public/v2/mcp/pm";
|
|
15
|
-
var MCP_PROTOCOL_VERSION = "2025-03-26";
|
|
16
|
-
var ABLETIME_PM_TOOL_NAMES = [
|
|
17
|
-
"set_task_assignee",
|
|
18
|
-
"set_task_priority",
|
|
19
|
-
"set_task_stage",
|
|
20
|
-
"set_task_dependency",
|
|
21
|
-
"set_task_blocked",
|
|
22
|
-
"set_task_locked",
|
|
23
|
-
"set_task_schedule",
|
|
24
|
-
"clear_task_schedule",
|
|
25
|
-
"archive_task",
|
|
26
|
-
"delete_task"
|
|
27
|
-
];
|
|
28
|
-
function abletimeToolUsesPm(name) {
|
|
29
|
-
return ABLETIME_PM_TOOL_NAMES.includes(name);
|
|
30
|
-
}
|
|
31
|
-
function normalizeAbleTimeHost(raw) {
|
|
32
|
-
const trimmed = raw?.trim() || DEFAULT_ABLETIME_HOST;
|
|
33
|
-
const withScheme = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
34
|
-
return withScheme.replace(/\/+$/, "");
|
|
35
|
-
}
|
|
36
|
-
function abletimeMcpUrl(host, opts) {
|
|
37
|
-
const path = opts?.pm ? ABLETIME_MCP_PM_PATH : ABLETIME_MCP_PATH;
|
|
38
|
-
return `${normalizeAbleTimeHost(host)}${path}`;
|
|
39
|
-
}
|
|
40
|
-
function rewriteAbleTimeError(message) {
|
|
41
|
-
if (/INTEGRATION_AGENT_ACCESS_DISABLED|agent access/i.test(message)) {
|
|
42
|
-
return `${message} \u2014 Enable Agent access (MCP) in AbleTime Settings \u2192 Integrations \u2192 API Keys.`;
|
|
43
|
-
}
|
|
44
|
-
if (/INTEGRATION_PAT_REQUIRED|organization api key/i.test(message)) {
|
|
45
|
-
return `${message} \u2014 AbleTime MCP needs a personal access token (apt_\u2026), not an organization API key.`;
|
|
46
|
-
}
|
|
47
|
-
if (/INTEGRATION_PLAN_REQUIRED/i.test(message)) {
|
|
48
|
-
return `${message} \u2014 AbleTime plan does not include API access.`;
|
|
49
|
-
}
|
|
50
|
-
if (/INTEGRATION_KEY_INVALID|INTEGRATION_KEY_MISSING|401|unauthorized|invalid token/i.test(message)) {
|
|
51
|
-
return `${message} \u2014 Reconnect AbleTime from Account settings (Profile \u2192 API Access).`;
|
|
52
|
-
}
|
|
53
|
-
return message;
|
|
54
|
-
}
|
|
55
|
-
function asRecord(value) {
|
|
56
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
57
|
-
}
|
|
58
|
-
function parseSseJson(body) {
|
|
59
|
-
const blocks = body.split(/\n\n+/);
|
|
60
|
-
let last;
|
|
61
|
-
for (const block of blocks) {
|
|
62
|
-
const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).filter((line) => line && line !== "[DONE]").join("\n");
|
|
63
|
-
if (!data) continue;
|
|
64
|
-
try {
|
|
65
|
-
last = JSON.parse(data);
|
|
66
|
-
} catch {
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
if (last !== void 0) return last;
|
|
70
|
-
try {
|
|
71
|
-
return JSON.parse(body);
|
|
72
|
-
} catch {
|
|
73
|
-
throw new Error(`AbleTime MCP returned a non-JSON response: ${body.slice(0, 200)}`);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
function unwrapToolResult(result) {
|
|
77
|
-
const record = asRecord(result);
|
|
78
|
-
if (!record) return result;
|
|
79
|
-
const content = record.content;
|
|
80
|
-
if (Array.isArray(content)) {
|
|
81
|
-
const texts = content.map((part) => {
|
|
82
|
-
const item = asRecord(part);
|
|
83
|
-
if (!item) return "";
|
|
84
|
-
return typeof item.text === "string" ? item.text : "";
|
|
85
|
-
}).filter(Boolean);
|
|
86
|
-
if (texts.length === 1) {
|
|
87
|
-
const text = texts[0];
|
|
88
|
-
try {
|
|
89
|
-
return JSON.parse(text);
|
|
90
|
-
} catch {
|
|
91
|
-
return text;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
if (texts.length > 1) return texts.join("\n");
|
|
95
|
-
}
|
|
96
|
-
if (record.structuredContent != null) return record.structuredContent;
|
|
97
|
-
if (record.isError === true) {
|
|
98
|
-
const message = typeof record.message === "string" ? record.message : textsFromUnknown(result) || "AbleTime tool failed";
|
|
99
|
-
throw new Error(message);
|
|
100
|
-
}
|
|
101
|
-
return result;
|
|
102
|
-
}
|
|
103
|
-
function textsFromUnknown(value) {
|
|
104
|
-
const record = asRecord(value);
|
|
105
|
-
if (!record) return "";
|
|
106
|
-
if (typeof record.message === "string") return record.message;
|
|
107
|
-
if (typeof record.error === "string") return record.error;
|
|
108
|
-
return "";
|
|
109
|
-
}
|
|
110
|
-
async function abletimeMcpRequest(method, params, opts) {
|
|
111
|
-
const token = (opts?.token ?? getAbleTimeAccessToken())?.trim();
|
|
112
|
-
if (!token) {
|
|
113
|
-
throw new Error("AbleTime is not connected \u2014 paste a personal access token in Account settings");
|
|
114
|
-
}
|
|
115
|
-
const url = abletimeMcpUrl(
|
|
116
|
-
opts?.host ?? (opts?.token ? DEFAULT_ABLETIME_HOST : getAbleTimeHost()),
|
|
117
|
-
{ pm: opts?.pm }
|
|
118
|
-
);
|
|
119
|
-
const res = await httpFetch(url, {
|
|
120
|
-
method: "POST",
|
|
121
|
-
headers: {
|
|
122
|
-
"Content-Type": "application/json",
|
|
123
|
-
Accept: "application/json, text/event-stream",
|
|
124
|
-
Authorization: `Bearer ${token}`,
|
|
125
|
-
"MCP-Protocol-Version": MCP_PROTOCOL_VERSION
|
|
126
|
-
},
|
|
127
|
-
body: JSON.stringify({
|
|
128
|
-
jsonrpc: "2.0",
|
|
129
|
-
id: 1,
|
|
130
|
-
method,
|
|
131
|
-
...params === void 0 ? {} : { params }
|
|
132
|
-
})
|
|
133
|
-
});
|
|
134
|
-
const body = await res.text().catch(() => "");
|
|
135
|
-
if (!res.ok) {
|
|
136
|
-
throw new Error(
|
|
137
|
-
rewriteAbleTimeError(
|
|
138
|
-
`AbleTime MCP error ${res.status}${body ? `: ${body.slice(0, 240)}` : ""}`
|
|
139
|
-
)
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
143
|
-
const parsed = contentType.includes("text/event-stream") ? parseSseJson(body) : (() => {
|
|
144
|
-
try {
|
|
145
|
-
return JSON.parse(body);
|
|
146
|
-
} catch {
|
|
147
|
-
return parseSseJson(body);
|
|
148
|
-
}
|
|
149
|
-
})();
|
|
150
|
-
const rpc = asRecord(parsed);
|
|
151
|
-
if (rpc?.error) {
|
|
152
|
-
const detail = typeof rpc.error.message === "string" && rpc.error.message.trim() ? rpc.error.message : `AbleTime MCP error ${rpc.error.code ?? ""}`.trim();
|
|
153
|
-
throw new Error(rewriteAbleTimeError(detail));
|
|
154
|
-
}
|
|
155
|
-
if (rpc && "result" in rpc) return rpc.result;
|
|
156
|
-
return parsed;
|
|
157
|
-
}
|
|
158
|
-
async function initializeIfNeeded(opts) {
|
|
159
|
-
try {
|
|
160
|
-
await abletimeMcpRequest(
|
|
161
|
-
"initialize",
|
|
162
|
-
{
|
|
163
|
-
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
164
|
-
capabilities: {},
|
|
165
|
-
clientInfo: { name: "sideboard", version: "1.0.0" }
|
|
166
|
-
},
|
|
167
|
-
opts
|
|
168
|
-
);
|
|
169
|
-
} catch (err) {
|
|
170
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
171
|
-
if (/already initialized|not (required|supported)|method not found|-32601/i.test(message)) {
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
throw err;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
async function callAbleTimeTool(name, args = {}, opts) {
|
|
178
|
-
if (!opts?.token && !isAbleTimeConnected()) {
|
|
179
|
-
throw new Error("AbleTime is not connected \u2014 paste a personal access token in Account settings");
|
|
180
|
-
}
|
|
181
|
-
const requestOpts = { ...opts, pm: opts?.pm ?? abletimeToolUsesPm(name) };
|
|
182
|
-
const call = () => abletimeMcpRequest("tools/call", { name, arguments: args }, requestOpts).then((result) => {
|
|
183
|
-
return unwrapToolResult(result);
|
|
184
|
-
});
|
|
185
|
-
try {
|
|
186
|
-
return await call();
|
|
187
|
-
} catch (err) {
|
|
188
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
189
|
-
if (/initialize|session|not initialized|-32000/i.test(message)) {
|
|
190
|
-
await initializeIfNeeded(requestOpts);
|
|
191
|
-
return await call();
|
|
192
|
-
}
|
|
193
|
-
throw err instanceof Error ? new Error(rewriteAbleTimeError(err.message)) : err;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
13
|
+
} from "./chunk-QFBBUFD4.js";
|
|
196
14
|
|
|
197
15
|
// src/integrations/abletime.ts
|
|
198
16
|
var CLOSED_STATES = /* @__PURE__ */ new Set([
|
|
@@ -203,7 +21,7 @@ var CLOSED_STATES = /* @__PURE__ */ new Set([
|
|
|
203
21
|
"archived",
|
|
204
22
|
"closed"
|
|
205
23
|
]);
|
|
206
|
-
function
|
|
24
|
+
function asRecord(value) {
|
|
207
25
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
208
26
|
}
|
|
209
27
|
function asString(value) {
|
|
@@ -220,14 +38,14 @@ function firstString(record, keys) {
|
|
|
220
38
|
function firstRecord(record, keys) {
|
|
221
39
|
if (!record) return null;
|
|
222
40
|
for (const key of keys) {
|
|
223
|
-
const value =
|
|
41
|
+
const value = asRecord(record[key]);
|
|
224
42
|
if (value) return value;
|
|
225
43
|
}
|
|
226
44
|
return null;
|
|
227
45
|
}
|
|
228
46
|
function asList(value) {
|
|
229
47
|
if (Array.isArray(value)) return value;
|
|
230
|
-
const record =
|
|
48
|
+
const record = asRecord(value);
|
|
231
49
|
if (!record) return [];
|
|
232
50
|
for (const key of ["data", "tasks", "projects", "items", "results", "nodes"]) {
|
|
233
51
|
if (Array.isArray(record[key])) return record[key];
|
|
@@ -239,7 +57,7 @@ function labelsOf(record) {
|
|
|
239
57
|
if (!Array.isArray(raw)) return [];
|
|
240
58
|
return raw.map((item) => {
|
|
241
59
|
if (typeof item === "string") return item.trim();
|
|
242
|
-
const rec =
|
|
60
|
+
const rec = asRecord(item);
|
|
243
61
|
return rec ? firstString(rec, ["name", "title", "label"]) : "";
|
|
244
62
|
}).filter(Boolean);
|
|
245
63
|
}
|
|
@@ -247,13 +65,13 @@ function commentsOf(record) {
|
|
|
247
65
|
const raw = record.comments ?? record.notes ?? record.discussion;
|
|
248
66
|
const out = [];
|
|
249
67
|
for (const item of asList(raw)) {
|
|
250
|
-
const rec =
|
|
68
|
+
const rec = asRecord(item);
|
|
251
69
|
if (!rec) continue;
|
|
252
|
-
const body = firstString(rec, ["body", "text", "comment", "
|
|
70
|
+
const body = firstString(rec, ["body", "text", "comment", "message", "contentSanitized"]) || textFromAbleTimeDoc(rec.content);
|
|
253
71
|
if (!body) continue;
|
|
254
|
-
const user = firstString(
|
|
72
|
+
const user = firstString(asRecord(rec.user) ?? asRecord(rec.author), ["name", "display_name", "username"]) || firstString(rec, ["user_name", "author", "username"]);
|
|
255
73
|
const comment = { body };
|
|
256
|
-
const id = firstString(rec, ["id", "comment_id"]);
|
|
74
|
+
const id = firstString(rec, ["id", "comment_id", "commentId"]);
|
|
257
75
|
if (id) comment.id = id;
|
|
258
76
|
const url = firstString(rec, ["url", "permalink"]);
|
|
259
77
|
if (url) comment.url = url;
|
|
@@ -277,11 +95,11 @@ function taskUrl(task, host) {
|
|
|
277
95
|
return `${base}/tasks/${slug}`;
|
|
278
96
|
}
|
|
279
97
|
function mapAbleTimeTask(raw, host) {
|
|
280
|
-
const record =
|
|
98
|
+
const record = asRecord(raw);
|
|
281
99
|
if (!record) return null;
|
|
282
100
|
const nested = firstRecord(record, ["task", "data"]) ?? record;
|
|
283
|
-
const id = firstString(nested, ["id", "task_id", "taskId"]);
|
|
284
|
-
const identifier = firstString(nested, ["reference", "ref", "identifier", "key", "code", "number"]) || id;
|
|
101
|
+
const id = firstString(nested, ["id", "task_id", "taskId", "timeflowTaskId"]);
|
|
102
|
+
const identifier = firstString(nested, ["reference", "ref", "identifier", "key", "code", "number", "taskRef"]) || id;
|
|
285
103
|
const title = firstString(nested, ["title", "name"]);
|
|
286
104
|
if (!id && !identifier && !title) return null;
|
|
287
105
|
const resolvedId = id || identifier;
|
|
@@ -292,21 +110,22 @@ function mapAbleTimeTask(raw, host) {
|
|
|
292
110
|
identifier: resolvedIdentifier,
|
|
293
111
|
title: title || resolvedIdentifier,
|
|
294
112
|
url,
|
|
295
|
-
description: firstString(nested, ["description", "body", "details"]) || void 0,
|
|
296
|
-
state: firstString(nested, ["state", "board_state", "boardState", "status"]) || void 0,
|
|
297
|
-
projectId: firstString(nested, ["project_id", "projectId"]) || firstString(
|
|
298
|
-
categoryId: firstString(nested, ["category_id", "categoryId"]) || firstString(
|
|
113
|
+
description: firstString(nested, ["description", "body", "details", "descriptionSanitized"]) || textFromAbleTimeDoc(nested.description) || void 0,
|
|
114
|
+
state: firstString(nested, ["state", "board_state", "boardState", "status", "taskState"]) || void 0,
|
|
115
|
+
projectId: firstString(nested, ["project_id", "projectId"]) || firstString(asRecord(nested.project), ["id"]) || void 0,
|
|
116
|
+
categoryId: firstString(nested, ["category_id", "categoryId", "projectCategoryId"]) || firstString(asRecord(nested.category), ["id"]) || void 0,
|
|
299
117
|
assignee: assigneeOf(nested),
|
|
300
118
|
labels: labelsOf(nested),
|
|
301
119
|
comments: commentsOf(nested),
|
|
302
|
-
createdAt: firstString(nested, ["created_at", "createdAt", "created"]) || void 0,
|
|
120
|
+
createdAt: firstString(nested, ["created_at", "createdAt", "created", "dateCreated"]) || void 0,
|
|
303
121
|
updatedAt: firstString(nested, [
|
|
304
122
|
"updated_at",
|
|
305
123
|
"updatedAt",
|
|
306
124
|
"modified_at",
|
|
307
125
|
"modifiedAt",
|
|
308
126
|
"last_activity_at",
|
|
309
|
-
"lastActivityAt"
|
|
127
|
+
"lastActivityAt",
|
|
128
|
+
"lastUpdate"
|
|
310
129
|
]) || void 0
|
|
311
130
|
};
|
|
312
131
|
}
|
|
@@ -337,16 +156,16 @@ function issueAttachmentForAbleTimeTask(task) {
|
|
|
337
156
|
};
|
|
338
157
|
}
|
|
339
158
|
function mapProject(raw) {
|
|
340
|
-
const record =
|
|
159
|
+
const record = asRecord(raw);
|
|
341
160
|
if (!record) return null;
|
|
342
|
-
const id = firstString(record, ["id", "project_id"]);
|
|
343
|
-
const name = firstString(record, ["name", "title"]);
|
|
161
|
+
const id = firstString(record, ["id", "project_id", "projectId"]);
|
|
162
|
+
const name = firstString(record, ["name", "title", "projectName"]);
|
|
344
163
|
if (!id && !name) return null;
|
|
345
164
|
const categories = asList(record.categories ?? record.category).map((item) => {
|
|
346
|
-
const rec =
|
|
165
|
+
const rec = asRecord(item);
|
|
347
166
|
if (!rec) return null;
|
|
348
|
-
const categoryId = firstString(rec, ["id", "category_id"]);
|
|
349
|
-
const categoryName = firstString(rec, ["name", "title"]);
|
|
167
|
+
const categoryId = firstString(rec, ["id", "category_id", "projectCategoryId", "categoryId"]);
|
|
168
|
+
const categoryName = firstString(rec, ["name", "title", "categoryName"]);
|
|
350
169
|
if (!categoryId && !categoryName) return null;
|
|
351
170
|
return { id: categoryId || categoryName, name: categoryName || categoryId };
|
|
352
171
|
}).filter((item) => Boolean(item));
|
|
@@ -358,7 +177,7 @@ function isOpenTask(task) {
|
|
|
358
177
|
}
|
|
359
178
|
async function getAbleTimeOrientation(opts) {
|
|
360
179
|
const raw = await callAbleTimeTool("orientation", {}, opts);
|
|
361
|
-
const record =
|
|
180
|
+
const record = asRecord(raw);
|
|
362
181
|
const viewerRec = firstRecord(record, ["viewer", "user", "actor", "me", "person"]) ?? record;
|
|
363
182
|
const viewer = {
|
|
364
183
|
id: firstString(viewerRec, ["id", "user_id"]) || void 0,
|
|
@@ -437,7 +256,7 @@ async function commentAbleTimeTask(input, opts) {
|
|
|
437
256
|
{ id, task_id: id, task: id, body, comment: body, text: body },
|
|
438
257
|
opts
|
|
439
258
|
);
|
|
440
|
-
const rec =
|
|
259
|
+
const rec = asRecord(raw);
|
|
441
260
|
return {
|
|
442
261
|
id: rec ? firstString(rec, ["id", "comment_id"]) || void 0 : void 0,
|
|
443
262
|
body: rec ? firstString(rec, ["body", "text", "comment"]) || body : body
|
|
@@ -627,8 +446,7 @@ async function listAbleTimeAssignedIssues(opts) {
|
|
|
627
446
|
};
|
|
628
447
|
}
|
|
629
448
|
async function verifyAbleTimeConnection(input) {
|
|
630
|
-
const token = input.token
|
|
631
|
-
if (!token) throw new Error("AbleTime personal access token is required");
|
|
449
|
+
const token = assertAbleTimeCredential(input.token);
|
|
632
450
|
const host = input.host?.trim() || void 0;
|
|
633
451
|
const orientation = await getAbleTimeOrientation({ token, host });
|
|
634
452
|
saveAbleTimeConnection({
|
|
@@ -640,16 +458,6 @@ async function verifyAbleTimeConnection(input) {
|
|
|
640
458
|
}
|
|
641
459
|
|
|
642
460
|
export {
|
|
643
|
-
DEFAULT_ABLETIME_HOST,
|
|
644
|
-
ABLETIME_MCP_PATH,
|
|
645
|
-
ABLETIME_MCP_PM_PATH,
|
|
646
|
-
ABLETIME_PM_TOOL_NAMES,
|
|
647
|
-
abletimeToolUsesPm,
|
|
648
|
-
normalizeAbleTimeHost,
|
|
649
|
-
abletimeMcpUrl,
|
|
650
|
-
rewriteAbleTimeError,
|
|
651
|
-
abletimeMcpRequest,
|
|
652
|
-
callAbleTimeTool,
|
|
653
461
|
taskUrl,
|
|
654
462
|
mapAbleTimeTask,
|
|
655
463
|
toAbleTimeIssueInfo,
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
resolveRepoRoot,
|
|
8
8
|
takenSlugsFromThread,
|
|
9
9
|
teamSlugFromName
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-QSEF2YVQ.js";
|
|
11
11
|
import {
|
|
12
12
|
ATTACHMENTS_DIR,
|
|
13
13
|
attachmentsGitignoreBody
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
resolveThreadDefaults,
|
|
31
31
|
resolveViewerProfile,
|
|
32
32
|
resolveViewerProfileForRepo
|
|
33
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-X743YN5Y.js";
|
|
34
34
|
import {
|
|
35
35
|
appDataDir,
|
|
36
36
|
globalAgentCwd,
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_ABLETIME_HOST,
|
|
3
|
+
normalizeAbleTimeHost
|
|
4
|
+
} from "./chunk-BFEBFGCS.js";
|
|
5
|
+
import {
|
|
6
|
+
httpFetch
|
|
7
|
+
} from "./chunk-N7WZ6NIN.js";
|
|
8
|
+
import {
|
|
9
|
+
disconnectAbleTimeConnection,
|
|
10
|
+
getAbleTimeHost,
|
|
11
|
+
loadAppSettings,
|
|
12
|
+
saveAbleTimeConnection
|
|
13
|
+
} from "./chunk-QFBBUFD4.js";
|
|
14
|
+
|
|
15
|
+
// src/integrations/abletime-oauth.ts
|
|
16
|
+
import { createHash, randomBytes } from "crypto";
|
|
17
|
+
import { createServer } from "http";
|
|
18
|
+
var ABLETIME_OAUTH_PORT = 19849;
|
|
19
|
+
var ABLETIME_OAUTH_REDIRECT = `http://127.0.0.1:${ABLETIME_OAUTH_PORT}/callback`;
|
|
20
|
+
var BAKED_ABLETIME_OAUTH_CLIENT_ID = "https://www.sideboard.cloud/oauth/abletime-client.json";
|
|
21
|
+
var ABLETIME_OAUTH_RESOURCE = `${DEFAULT_ABLETIME_HOST}/api/public/v2/mcp`;
|
|
22
|
+
var REFRESH_SKEW_MS = 5 * 6e4;
|
|
23
|
+
function hasBakedAbleTimeOAuth() {
|
|
24
|
+
return Boolean(BAKED_ABLETIME_OAUTH_CLIENT_ID.trim());
|
|
25
|
+
}
|
|
26
|
+
function ableTimeOAuthClientId() {
|
|
27
|
+
return process.env.SIDEBOARD_ABLETIME_OAUTH_CLIENT_ID?.trim() || BAKED_ABLETIME_OAUTH_CLIENT_ID.trim();
|
|
28
|
+
}
|
|
29
|
+
function ableTimeOAuthResource(host) {
|
|
30
|
+
return `${normalizeAbleTimeHost(host)}/api/public/v2/mcp`;
|
|
31
|
+
}
|
|
32
|
+
function createAbleTimePkce() {
|
|
33
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
34
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
35
|
+
return { verifier, challenge };
|
|
36
|
+
}
|
|
37
|
+
function ableTimeOAuthAuthorizeUrl(clientId, state, codeChallenge, host) {
|
|
38
|
+
const base = normalizeAbleTimeHost(host);
|
|
39
|
+
const params = new URLSearchParams({
|
|
40
|
+
response_type: "code",
|
|
41
|
+
client_id: clientId,
|
|
42
|
+
redirect_uri: ABLETIME_OAUTH_REDIRECT,
|
|
43
|
+
code_challenge: codeChallenge,
|
|
44
|
+
code_challenge_method: "S256",
|
|
45
|
+
state,
|
|
46
|
+
resource: ableTimeOAuthResource(host)
|
|
47
|
+
});
|
|
48
|
+
return `${base}/oauth/authorize?${params.toString()}`;
|
|
49
|
+
}
|
|
50
|
+
function htmlPage(title, body) {
|
|
51
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title>
|
|
52
|
+
<style>body{font-family:ui-sans-serif,system-ui,sans-serif;padding:48px 24px;max-width:36rem;margin:0 auto;color:#1a1a1a}
|
|
53
|
+
h1{font-size:1.25rem}p{line-height:1.5;color:#444}</style></head><body>${body}</body></html>`;
|
|
54
|
+
}
|
|
55
|
+
async function exchangeAbleTimeToken(body, host) {
|
|
56
|
+
const url = `${normalizeAbleTimeHost(host)}/api/oauth/token`;
|
|
57
|
+
const res = await httpFetch(url, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: {
|
|
60
|
+
Accept: "application/json",
|
|
61
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
62
|
+
},
|
|
63
|
+
body
|
|
64
|
+
});
|
|
65
|
+
const data = await res.json().catch(() => ({}));
|
|
66
|
+
if (!res.ok || data.error || !data.access_token?.trim()) {
|
|
67
|
+
const detail = data.error_description || data.error || `HTTP ${res.status}`;
|
|
68
|
+
throw new Error(`AbleTime OAuth token exchange failed: ${detail}`);
|
|
69
|
+
}
|
|
70
|
+
return data;
|
|
71
|
+
}
|
|
72
|
+
function persistAbleTimeTokens(data, input) {
|
|
73
|
+
return saveAbleTimeConnection({
|
|
74
|
+
accessToken: data.access_token.trim(),
|
|
75
|
+
refreshToken: data.refresh_token?.trim(),
|
|
76
|
+
expiresIn: data.expires_in,
|
|
77
|
+
host: input?.host,
|
|
78
|
+
viewerName: input?.viewerName
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
var refreshInFlight = null;
|
|
82
|
+
async function refreshAbleTimeAccessToken(refreshToken) {
|
|
83
|
+
const host = getAbleTimeHost();
|
|
84
|
+
const body = new URLSearchParams({
|
|
85
|
+
grant_type: "refresh_token",
|
|
86
|
+
refresh_token: refreshToken,
|
|
87
|
+
client_id: ableTimeOAuthClientId(),
|
|
88
|
+
resource: ableTimeOAuthResource(host)
|
|
89
|
+
});
|
|
90
|
+
try {
|
|
91
|
+
const data = await exchangeAbleTimeToken(body, host);
|
|
92
|
+
persistAbleTimeTokens(data, { host });
|
|
93
|
+
return data.access_token.trim();
|
|
94
|
+
} catch (err) {
|
|
95
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
96
|
+
if (/invalid_grant|invalid_token|unauthorized/i.test(message)) {
|
|
97
|
+
disconnectAbleTimeConnection();
|
|
98
|
+
throw new Error("AbleTime sign-in expired \u2014 connect again in Account settings");
|
|
99
|
+
}
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function ensureAbleTimeAccessToken() {
|
|
104
|
+
const settings = loadAppSettings();
|
|
105
|
+
const access = settings.integrations.abletimeAccessToken?.trim() || "";
|
|
106
|
+
const refresh = settings.integrations.abletimeRefreshToken?.trim() || "";
|
|
107
|
+
const expiresAt = settings.integrations.abletimeTokenExpiresAt ?? 0;
|
|
108
|
+
if (access && (!refresh || Date.now() < expiresAt - REFRESH_SKEW_MS)) {
|
|
109
|
+
return access;
|
|
110
|
+
}
|
|
111
|
+
if (refresh) {
|
|
112
|
+
if (!refreshInFlight) {
|
|
113
|
+
refreshInFlight = refreshAbleTimeAccessToken(refresh).finally(() => {
|
|
114
|
+
refreshInFlight = null;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return refreshInFlight;
|
|
118
|
+
}
|
|
119
|
+
return access || null;
|
|
120
|
+
}
|
|
121
|
+
var ABLETIME_OAUTH_CANCELLED = "AbleTime sign-in cancelled";
|
|
122
|
+
var AbleTimeOAuthCancelledError = class extends Error {
|
|
123
|
+
constructor(message = ABLETIME_OAUTH_CANCELLED) {
|
|
124
|
+
super(message);
|
|
125
|
+
this.name = "AbleTimeOAuthCancelledError";
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
function isAbleTimeOAuthCancelled(err) {
|
|
129
|
+
if (err instanceof AbleTimeOAuthCancelledError) return true;
|
|
130
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
131
|
+
return message.includes(ABLETIME_OAUTH_CANCELLED);
|
|
132
|
+
}
|
|
133
|
+
async function startAbleTimeOAuth(opts) {
|
|
134
|
+
if (opts?.signal?.aborted) {
|
|
135
|
+
throw new AbleTimeOAuthCancelledError();
|
|
136
|
+
}
|
|
137
|
+
const clientId = ableTimeOAuthClientId();
|
|
138
|
+
if (!clientId) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`AbleTime browser sign-in needs a Client ID Metadata Document URL. Host ${BAKED_ABLETIME_OAUTH_CLIENT_ID} (see site/oauth/abletime-client.json), or set SIDEBOARD_ABLETIME_OAUTH_CLIENT_ID.`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
const host = opts?.host?.trim() || void 0;
|
|
144
|
+
const state = randomBytes(16).toString("hex");
|
|
145
|
+
const pkce = createAbleTimePkce();
|
|
146
|
+
const authorizeUrl = ableTimeOAuthAuthorizeUrl(clientId, state, pkce.challenge, host);
|
|
147
|
+
const timeoutMs = opts?.timeoutMs ?? 5 * 6e4;
|
|
148
|
+
const code = await new Promise((resolve, reject) => {
|
|
149
|
+
let settled = false;
|
|
150
|
+
const finish = (err, value) => {
|
|
151
|
+
if (settled) return;
|
|
152
|
+
settled = true;
|
|
153
|
+
cleanup();
|
|
154
|
+
if (err) reject(err);
|
|
155
|
+
else resolve(value);
|
|
156
|
+
};
|
|
157
|
+
const server = createServer((req, res) => {
|
|
158
|
+
try {
|
|
159
|
+
const url = new URL(req.url || "/", `http://127.0.0.1:${ABLETIME_OAUTH_PORT}`);
|
|
160
|
+
if (url.pathname !== "/callback") {
|
|
161
|
+
res.writeHead(404);
|
|
162
|
+
res.end("Not found");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const err = url.searchParams.get("error");
|
|
166
|
+
const gotState = url.searchParams.get("state");
|
|
167
|
+
const gotCode = url.searchParams.get("code");
|
|
168
|
+
if (err) {
|
|
169
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
170
|
+
res.end(htmlPage("AbleTime", `<h1>Authorization cancelled</h1><p>${err}</p>`));
|
|
171
|
+
finish(new AbleTimeOAuthCancelledError(`AbleTime OAuth: ${err}`));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (gotState !== state || !gotCode) {
|
|
175
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
176
|
+
res.end(htmlPage("AbleTime", "<h1>Invalid callback</h1><p>State or code missing.</p>"));
|
|
177
|
+
finish(new Error("AbleTime OAuth callback was invalid"));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
181
|
+
res.end(
|
|
182
|
+
htmlPage(
|
|
183
|
+
"AbleTime connected",
|
|
184
|
+
"<h1>AbleTime connected</h1><p>You can close this tab and return to Sideboard.</p>"
|
|
185
|
+
)
|
|
186
|
+
);
|
|
187
|
+
finish(void 0, gotCode);
|
|
188
|
+
} catch (e) {
|
|
189
|
+
finish(e instanceof Error ? e : new Error(String(e)));
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
const timer = setTimeout(() => {
|
|
193
|
+
finish(new Error("AbleTime sign-in timed out \u2014 try again"));
|
|
194
|
+
}, timeoutMs);
|
|
195
|
+
const onAbort = () => finish(new AbleTimeOAuthCancelledError());
|
|
196
|
+
const cleanup = () => {
|
|
197
|
+
clearTimeout(timer);
|
|
198
|
+
opts?.signal?.removeEventListener("abort", onAbort);
|
|
199
|
+
server.close();
|
|
200
|
+
};
|
|
201
|
+
opts?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
202
|
+
if (opts?.signal?.aborted) {
|
|
203
|
+
onAbort();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
server.on("error", (e) => {
|
|
207
|
+
finish(
|
|
208
|
+
e instanceof Error && e.code === "EADDRINUSE" ? new Error(
|
|
209
|
+
`Port ${ABLETIME_OAUTH_PORT} is in use. Close whatever is bound there, or paste an AbleTime token instead.`
|
|
210
|
+
) : e instanceof Error ? e : new Error(String(e))
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
server.listen(ABLETIME_OAUTH_PORT, "127.0.0.1", () => {
|
|
214
|
+
void Promise.resolve(opts?.openUrl?.(authorizeUrl)).catch((e) => {
|
|
215
|
+
finish(e instanceof Error ? e : new Error(String(e)));
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
const body = new URLSearchParams({
|
|
220
|
+
grant_type: "authorization_code",
|
|
221
|
+
code,
|
|
222
|
+
redirect_uri: ABLETIME_OAUTH_REDIRECT,
|
|
223
|
+
client_id: clientId,
|
|
224
|
+
code_verifier: pkce.verifier,
|
|
225
|
+
resource: ableTimeOAuthResource(host)
|
|
226
|
+
});
|
|
227
|
+
const data = await exchangeAbleTimeToken(body, host);
|
|
228
|
+
persistAbleTimeTokens(data, { host });
|
|
229
|
+
const token = data.access_token.trim();
|
|
230
|
+
try {
|
|
231
|
+
const { getAbleTimeOrientation } = await import("./abletime-SISJEPJ3.js");
|
|
232
|
+
const orientation = await getAbleTimeOrientation({ token, host });
|
|
233
|
+
return saveAbleTimeConnection({
|
|
234
|
+
accessToken: token,
|
|
235
|
+
refreshToken: data.refresh_token?.trim(),
|
|
236
|
+
expiresIn: data.expires_in,
|
|
237
|
+
host,
|
|
238
|
+
viewerName: orientation.viewer.name
|
|
239
|
+
});
|
|
240
|
+
} catch {
|
|
241
|
+
return loadAppSettings();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export {
|
|
246
|
+
ABLETIME_OAUTH_PORT,
|
|
247
|
+
ABLETIME_OAUTH_REDIRECT,
|
|
248
|
+
BAKED_ABLETIME_OAUTH_CLIENT_ID,
|
|
249
|
+
ABLETIME_OAUTH_RESOURCE,
|
|
250
|
+
hasBakedAbleTimeOAuth,
|
|
251
|
+
ableTimeOAuthClientId,
|
|
252
|
+
ableTimeOAuthResource,
|
|
253
|
+
createAbleTimePkce,
|
|
254
|
+
ableTimeOAuthAuthorizeUrl,
|
|
255
|
+
ensureAbleTimeAccessToken,
|
|
256
|
+
ABLETIME_OAUTH_CANCELLED,
|
|
257
|
+
AbleTimeOAuthCancelledError,
|
|
258
|
+
isAbleTimeOAuthCancelled,
|
|
259
|
+
startAbleTimeOAuth
|
|
260
|
+
};
|