@sideboard-ai/core 0.1.202 → 0.1.204

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.
Files changed (46) hide show
  1. package/dist/{abletime-KGPD3UUJ.js → abletime-S3UHQZVW.js} +4 -2
  2. package/dist/{abletime-345SE3B4.js → abletime-TONBQP4O.js} +4 -2
  3. package/dist/abletime-oauth-AVCEO2DY.js +314 -0
  4. package/dist/abletime-oauth-SD4SZY5U.js +52 -0
  5. package/dist/abletime-rest-6ODSXV7G.js +25 -0
  6. package/dist/abletime-rest-J2OBB4VA.js +24 -0
  7. package/dist/agents/cursor-runner.js +1 -1
  8. package/dist/{agents-573ZDIX6.js → agents-67K4BQFJ.js} +4 -4
  9. package/dist/{agents-OXX3ONVY.js → agents-BVOJNHV6.js} +5 -5
  10. package/dist/{app-settings-V6IHESEQ.js → app-settings-LGHMNHSP.js} +1 -1
  11. package/dist/{app-settings-AXCURTZJ.js → app-settings-YEPF46IJ.js} +1 -1
  12. package/dist/chunk-3K257PCS.js +393 -0
  13. package/dist/{chunk-54RSAMBH.js → chunk-5SMS6QTO.js} +2 -2
  14. package/dist/chunk-6Q7DFVDQ.js +309 -0
  15. package/dist/chunk-CYLDJWRK.js +391 -0
  16. package/dist/{chunk-PGQGP4FD.js → chunk-FO2NTJ7L.js} +37 -219
  17. package/dist/{chunk-7NKY2OX7.js → chunk-FO45DMJ5.js} +125 -109
  18. package/dist/{chunk-CQ5R3VPP.js → chunk-KZ7PI7FE.js} +18 -0
  19. package/dist/{chunk-7AHAABKN.js → chunk-QAVWH2RQ.js} +37 -229
  20. package/dist/{chunk-CXFW5LIH.js → chunk-QFBBUFD4.js} +38 -3
  21. package/dist/{chunk-RPX6VDW7.js → chunk-QUW5D62C.js} +34 -20
  22. package/dist/chunk-SGUVAK66.js +262 -0
  23. package/dist/{chunk-27GGXX27.js → chunk-ULZLWQBS.js} +128 -112
  24. package/dist/{chunk-LJHC62LZ.js → chunk-VIPA75ZA.js} +2 -2
  25. package/dist/chunk-W6VM36YO.js +272 -0
  26. package/dist/{chunk-NIVF5NDN.js → chunk-WF33LOP4.js} +44 -6
  27. package/dist/{chunk-STPZQR5B.js → chunk-WVATAIF4.js} +34 -20
  28. package/dist/{chunk-ISV5IFH3.js → chunk-X743YN5Y.js} +38 -3
  29. package/dist/{chunk-A2TAKYSG.js → chunk-YOSTWSNC.js} +27 -7
  30. package/dist/{coordinator-prompt-BH4EHFOO.js → coordinator-prompt-NPT555BO.js} +3 -3
  31. package/dist/{coordinator-prompt-TY5FHAXC.js → coordinator-prompt-WEPXXLLX.js} +3 -3
  32. package/dist/{global-workspace-NLLAPQJI.js → global-workspace-2I2T7FL5.js} +3 -3
  33. package/dist/{global-workspace-5AZEEQRU.js → global-workspace-NSILADOM.js} +3 -3
  34. package/dist/index.cjs +1137 -213
  35. package/dist/index.d.cts +87 -13
  36. package/dist/index.d.ts +87 -13
  37. package/dist/index.js +94 -19
  38. package/dist/mcp/run-stdio.cjs +1023 -180
  39. package/dist/mcp/run-stdio.js +22 -20
  40. package/dist/{orchestrator-JNSQDRZI.js → orchestrator-CMAVNELS.js} +6 -6
  41. package/dist/{orchestrator-NCYKEI3E.js → orchestrator-I7GFXLHF.js} +6 -6
  42. package/dist/{workspaces-RP76JH3J.js → workspaces-BOC6M7BW.js} +3 -3
  43. package/dist/{workspaces-PEWYWVHZ.js → workspaces-RFGZBUAR.js} +3 -3
  44. package/dist/{worktree-72TZWUSH.js → worktree-G3ZLM4DC.js} +4 -2
  45. package/dist/{worktree-ZQEUKSNE.js → worktree-WSUQMMK4.js} +4 -2
  46. package/package.json +1 -1
@@ -0,0 +1,309 @@
1
+ import {
2
+ DEFAULT_ABLETIME_HOST,
3
+ normalizeAbleTimeHost
4
+ } from "./chunk-W6VM36YO.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_CALLBACK_PATH = "/oauth/abletime/callback";
19
+ var ABLETIME_OAUTH_PORT = 19849;
20
+ var ABLETIME_OAUTH_LOCAL_CALLBACK = `http://127.0.0.1:${ABLETIME_OAUTH_PORT}/callback`;
21
+ var ABLETIME_OAUTH_REDIRECT = `https://www.sideboard.cloud${ABLETIME_OAUTH_CALLBACK_PATH}`;
22
+ function ableTimeOAuthRedirectUri() {
23
+ return process.env.SIDEBOARD_ABLETIME_OAUTH_REDIRECT?.trim() || ABLETIME_OAUTH_REDIRECT;
24
+ }
25
+ var BOUNCE_QUERY_KEYS = ["code", "state", "error", "error_description"];
26
+ function parseAbleTimeOAuthCallbackUrl(reqUrl) {
27
+ let url;
28
+ try {
29
+ url = new URL(reqUrl, ABLETIME_OAUTH_REDIRECT);
30
+ } catch {
31
+ return null;
32
+ }
33
+ if (url.pathname !== ABLETIME_OAUTH_CALLBACK_PATH) return null;
34
+ return url;
35
+ }
36
+ function ableTimeOAuthLocalBounceUrl(reqUrl) {
37
+ const url = parseAbleTimeOAuthCallbackUrl(reqUrl);
38
+ if (!url) return null;
39
+ const dest = new URL(ABLETIME_OAUTH_LOCAL_CALLBACK);
40
+ for (const key of BOUNCE_QUERY_KEYS) {
41
+ const value = url.searchParams.get(key);
42
+ if (value) dest.searchParams.set(key, value);
43
+ }
44
+ return dest.toString();
45
+ }
46
+ function escapeHtml(value) {
47
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
48
+ }
49
+ function ableTimeOAuthBouncePage(localUrl) {
50
+ const safe = escapeHtml(localUrl);
51
+ return `<!doctype html><html><head><meta charset="utf-8">
52
+ <meta http-equiv="refresh" content="0;url=${safe}">
53
+ <title>Returning to Sideboard</title>
54
+ <script>location.replace(${JSON.stringify(localUrl)})</script>
55
+ <style>body{font-family:ui-sans-serif,system-ui,sans-serif;padding:48px 24px;max-width:36rem;margin:0 auto;color:#1a1a1a}
56
+ h1{font-size:1.25rem}p{line-height:1.5;color:#444}a{color:#0f7ed4}</style>
57
+ </head><body>
58
+ <h1>Returning to Sideboard</h1>
59
+ <p>AbleTime authorized. Opening the app\u2026</p>
60
+ <p><a href="${safe}">Open Sideboard</a> if it does not appear.</p>
61
+ </body></html>`;
62
+ }
63
+ var BAKED_ABLETIME_OAUTH_CLIENT_ID = "https://www.sideboard.cloud/oauth/abletime-client.json";
64
+ var ABLETIME_OAUTH_RESOURCE = `${DEFAULT_ABLETIME_HOST}/api/public/v2/mcp`;
65
+ var REFRESH_SKEW_MS = 5 * 6e4;
66
+ function hasBakedAbleTimeOAuth() {
67
+ return Boolean(BAKED_ABLETIME_OAUTH_CLIENT_ID.trim());
68
+ }
69
+ function ableTimeOAuthClientId() {
70
+ return process.env.SIDEBOARD_ABLETIME_OAUTH_CLIENT_ID?.trim() || BAKED_ABLETIME_OAUTH_CLIENT_ID.trim();
71
+ }
72
+ function ableTimeOAuthResource(host) {
73
+ return `${normalizeAbleTimeHost(host)}/api/public/v2/mcp`;
74
+ }
75
+ function createAbleTimePkce() {
76
+ const verifier = randomBytes(32).toString("base64url");
77
+ const challenge = createHash("sha256").update(verifier).digest("base64url");
78
+ return { verifier, challenge };
79
+ }
80
+ function ableTimeOAuthAuthorizeUrl(clientId, state, codeChallenge, host) {
81
+ const base = normalizeAbleTimeHost(host);
82
+ const params = new URLSearchParams({
83
+ response_type: "code",
84
+ client_id: clientId,
85
+ redirect_uri: ableTimeOAuthRedirectUri(),
86
+ code_challenge: codeChallenge,
87
+ code_challenge_method: "S256",
88
+ state,
89
+ resource: ableTimeOAuthResource(host)
90
+ });
91
+ return `${base}/oauth/authorize?${params.toString()}`;
92
+ }
93
+ function htmlPage(title, body) {
94
+ return `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title>
95
+ <style>body{font-family:ui-sans-serif,system-ui,sans-serif;padding:48px 24px;max-width:36rem;margin:0 auto;color:#1a1a1a}
96
+ h1{font-size:1.25rem}p{line-height:1.5;color:#444}</style></head><body>${body}</body></html>`;
97
+ }
98
+ async function exchangeAbleTimeToken(body, host) {
99
+ const url = `${normalizeAbleTimeHost(host)}/api/oauth/token`;
100
+ const res = await httpFetch(url, {
101
+ method: "POST",
102
+ headers: {
103
+ Accept: "application/json",
104
+ "Content-Type": "application/x-www-form-urlencoded"
105
+ },
106
+ body
107
+ });
108
+ const data = await res.json().catch(() => ({}));
109
+ if (!res.ok || data.error || !data.access_token?.trim()) {
110
+ const detail = data.error_description || data.error || `HTTP ${res.status}`;
111
+ throw new Error(`AbleTime OAuth token exchange failed: ${detail}`);
112
+ }
113
+ return data;
114
+ }
115
+ function persistAbleTimeTokens(data, input) {
116
+ return saveAbleTimeConnection({
117
+ accessToken: data.access_token.trim(),
118
+ refreshToken: data.refresh_token?.trim(),
119
+ expiresIn: data.expires_in,
120
+ host: input?.host,
121
+ viewerName: input?.viewerName
122
+ });
123
+ }
124
+ var refreshInFlight = null;
125
+ async function refreshAbleTimeAccessToken(refreshToken) {
126
+ const host = getAbleTimeHost();
127
+ const body = new URLSearchParams({
128
+ grant_type: "refresh_token",
129
+ refresh_token: refreshToken,
130
+ client_id: ableTimeOAuthClientId(),
131
+ resource: ableTimeOAuthResource(host)
132
+ });
133
+ try {
134
+ const data = await exchangeAbleTimeToken(body, host);
135
+ persistAbleTimeTokens(data, { host });
136
+ return data.access_token.trim();
137
+ } catch (err) {
138
+ const message = err instanceof Error ? err.message : String(err);
139
+ if (/invalid_grant|invalid_token|unauthorized/i.test(message)) {
140
+ disconnectAbleTimeConnection();
141
+ throw new Error("AbleTime sign-in expired \u2014 connect again in Account settings");
142
+ }
143
+ throw err;
144
+ }
145
+ }
146
+ async function ensureAbleTimeAccessToken() {
147
+ const settings = loadAppSettings();
148
+ const access = settings.integrations.abletimeAccessToken?.trim() || "";
149
+ const refresh = settings.integrations.abletimeRefreshToken?.trim() || "";
150
+ const expiresAt = settings.integrations.abletimeTokenExpiresAt ?? 0;
151
+ if (access && (!refresh || Date.now() < expiresAt - REFRESH_SKEW_MS)) {
152
+ return access;
153
+ }
154
+ if (refresh) {
155
+ if (!refreshInFlight) {
156
+ refreshInFlight = refreshAbleTimeAccessToken(refresh).finally(() => {
157
+ refreshInFlight = null;
158
+ });
159
+ }
160
+ return refreshInFlight;
161
+ }
162
+ return access || null;
163
+ }
164
+ var ABLETIME_OAUTH_CANCELLED = "AbleTime sign-in cancelled";
165
+ var AbleTimeOAuthCancelledError = class extends Error {
166
+ constructor(message = ABLETIME_OAUTH_CANCELLED) {
167
+ super(message);
168
+ this.name = "AbleTimeOAuthCancelledError";
169
+ }
170
+ };
171
+ function isAbleTimeOAuthCancelled(err) {
172
+ if (err instanceof AbleTimeOAuthCancelledError) return true;
173
+ const message = err instanceof Error ? err.message : String(err);
174
+ return message.includes(ABLETIME_OAUTH_CANCELLED);
175
+ }
176
+ async function startAbleTimeOAuth(opts) {
177
+ if (opts?.signal?.aborted) {
178
+ throw new AbleTimeOAuthCancelledError();
179
+ }
180
+ const clientId = ableTimeOAuthClientId();
181
+ if (!clientId) {
182
+ throw new Error(
183
+ `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.`
184
+ );
185
+ }
186
+ const host = opts?.host?.trim() || void 0;
187
+ const state = randomBytes(16).toString("hex");
188
+ const pkce = createAbleTimePkce();
189
+ const authorizeUrl = ableTimeOAuthAuthorizeUrl(clientId, state, pkce.challenge, host);
190
+ const timeoutMs = opts?.timeoutMs ?? 5 * 6e4;
191
+ const code = await new Promise((resolve, reject) => {
192
+ let settled = false;
193
+ const finish = (err, value) => {
194
+ if (settled) return;
195
+ settled = true;
196
+ cleanup();
197
+ if (err) reject(err);
198
+ else resolve(value);
199
+ };
200
+ const server = createServer((req, res) => {
201
+ try {
202
+ const url = new URL(req.url || "/", `http://127.0.0.1:${ABLETIME_OAUTH_PORT}`);
203
+ if (url.pathname !== "/callback") {
204
+ res.writeHead(404);
205
+ res.end("Not found");
206
+ return;
207
+ }
208
+ const err = url.searchParams.get("error");
209
+ const gotState = url.searchParams.get("state");
210
+ const gotCode = url.searchParams.get("code");
211
+ if (err) {
212
+ res.writeHead(400, { "Content-Type": "text/html" });
213
+ res.end(htmlPage("AbleTime", `<h1>Authorization cancelled</h1><p>${err}</p>`));
214
+ finish(new AbleTimeOAuthCancelledError(`AbleTime OAuth: ${err}`));
215
+ return;
216
+ }
217
+ if (gotState !== state || !gotCode) {
218
+ res.writeHead(400, { "Content-Type": "text/html" });
219
+ res.end(htmlPage("AbleTime", "<h1>Invalid callback</h1><p>State or code missing.</p>"));
220
+ finish(new Error("AbleTime OAuth callback was invalid"));
221
+ return;
222
+ }
223
+ res.writeHead(200, { "Content-Type": "text/html" });
224
+ res.end(
225
+ htmlPage(
226
+ "AbleTime connected",
227
+ "<h1>AbleTime connected</h1><p>You can close this tab and return to Sideboard.</p>"
228
+ )
229
+ );
230
+ finish(void 0, gotCode);
231
+ } catch (e) {
232
+ finish(e instanceof Error ? e : new Error(String(e)));
233
+ }
234
+ });
235
+ const timer = setTimeout(() => {
236
+ finish(new Error("AbleTime sign-in timed out \u2014 try again"));
237
+ }, timeoutMs);
238
+ const onAbort = () => finish(new AbleTimeOAuthCancelledError());
239
+ const cleanup = () => {
240
+ clearTimeout(timer);
241
+ opts?.signal?.removeEventListener("abort", onAbort);
242
+ server.close();
243
+ };
244
+ opts?.signal?.addEventListener("abort", onAbort, { once: true });
245
+ if (opts?.signal?.aborted) {
246
+ onAbort();
247
+ return;
248
+ }
249
+ server.on("error", (e) => {
250
+ finish(
251
+ e instanceof Error && e.code === "EADDRINUSE" ? new Error(
252
+ `Port ${ABLETIME_OAUTH_PORT} is in use. Close whatever is bound there, or paste an AbleTime token instead.`
253
+ ) : e instanceof Error ? e : new Error(String(e))
254
+ );
255
+ });
256
+ server.listen(ABLETIME_OAUTH_PORT, "127.0.0.1", () => {
257
+ void Promise.resolve(opts?.openUrl?.(authorizeUrl)).catch((e) => {
258
+ finish(e instanceof Error ? e : new Error(String(e)));
259
+ });
260
+ });
261
+ });
262
+ const body = new URLSearchParams({
263
+ grant_type: "authorization_code",
264
+ code,
265
+ redirect_uri: ableTimeOAuthRedirectUri(),
266
+ client_id: clientId,
267
+ code_verifier: pkce.verifier,
268
+ resource: ableTimeOAuthResource(host)
269
+ });
270
+ const data = await exchangeAbleTimeToken(body, host);
271
+ persistAbleTimeTokens(data, { host });
272
+ const token = data.access_token.trim();
273
+ try {
274
+ const { getAbleTimeOrientation } = await import("./abletime-TONBQP4O.js");
275
+ const orientation = await getAbleTimeOrientation({ token, host });
276
+ return saveAbleTimeConnection({
277
+ accessToken: token,
278
+ refreshToken: data.refresh_token?.trim(),
279
+ expiresIn: data.expires_in,
280
+ host,
281
+ viewerName: orientation.viewer.name
282
+ });
283
+ } catch {
284
+ return loadAppSettings();
285
+ }
286
+ }
287
+
288
+ export {
289
+ ABLETIME_OAUTH_CALLBACK_PATH,
290
+ ABLETIME_OAUTH_PORT,
291
+ ABLETIME_OAUTH_LOCAL_CALLBACK,
292
+ ABLETIME_OAUTH_REDIRECT,
293
+ ableTimeOAuthRedirectUri,
294
+ parseAbleTimeOAuthCallbackUrl,
295
+ ableTimeOAuthLocalBounceUrl,
296
+ ableTimeOAuthBouncePage,
297
+ BAKED_ABLETIME_OAUTH_CLIENT_ID,
298
+ ABLETIME_OAUTH_RESOURCE,
299
+ hasBakedAbleTimeOAuth,
300
+ ableTimeOAuthClientId,
301
+ ableTimeOAuthResource,
302
+ createAbleTimePkce,
303
+ ableTimeOAuthAuthorizeUrl,
304
+ ensureAbleTimeAccessToken,
305
+ ABLETIME_OAUTH_CANCELLED,
306
+ AbleTimeOAuthCancelledError,
307
+ isAbleTimeOAuthCancelled,
308
+ startAbleTimeOAuth
309
+ };
@@ -0,0 +1,391 @@
1
+ import {
2
+ DEFAULT_ABLETIME_HOST,
3
+ ableTimeCredentialKind,
4
+ normalizeAbleTimeCredential,
5
+ normalizeAbleTimeHost,
6
+ rewriteAbleTimeError
7
+ } from "./chunk-W6VM36YO.js";
8
+ import {
9
+ httpFetch
10
+ } from "./chunk-N7WZ6NIN.js";
11
+ import {
12
+ getAbleTimeAccessToken,
13
+ getAbleTimeHost
14
+ } from "./chunk-QFBBUFD4.js";
15
+
16
+ // src/integrations/abletime-rest.ts
17
+ var ABLETIME_REST_PREFIX = "/api/public/v2";
18
+ var ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/i;
19
+ function asRecord(value) {
20
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
21
+ }
22
+ function asString(value) {
23
+ return typeof value === "string" ? value.trim() : "";
24
+ }
25
+ function asList(value) {
26
+ if (Array.isArray(value)) return value;
27
+ const record = asRecord(value);
28
+ if (!record) return [];
29
+ for (const key of ["data", "tasks", "projects", "users", "items", "results"]) {
30
+ if (Array.isArray(record[key])) return record[key];
31
+ }
32
+ return [];
33
+ }
34
+ function ableTimeDocFromText(text) {
35
+ const trimmed = text.trim();
36
+ if (!trimmed) return { type: "doc", content: [] };
37
+ return {
38
+ type: "doc",
39
+ content: [{ type: "paragraph", content: [{ type: "text", text: trimmed }] }]
40
+ };
41
+ }
42
+ function textFromAbleTimeDoc(value) {
43
+ if (typeof value === "string") return value.trim();
44
+ const record = asRecord(value);
45
+ if (!record) return "";
46
+ if (typeof record.text === "string" && record.text.trim()) return record.text.trim();
47
+ const chunks = [];
48
+ const walk = (node) => {
49
+ const rec = asRecord(node);
50
+ if (!rec) return;
51
+ if (typeof rec.text === "string" && rec.text) chunks.push(rec.text);
52
+ if (Array.isArray(rec.content)) rec.content.forEach(walk);
53
+ };
54
+ walk(record);
55
+ return chunks.join("").trim();
56
+ }
57
+ function abletimeRestUrl(path, host, query) {
58
+ const base = `${normalizeAbleTimeHost(host)}${ABLETIME_REST_PREFIX}`;
59
+ const suffix = path.startsWith("/") ? path : `/${path}`;
60
+ const url = new URL(`${base}${suffix}`);
61
+ for (const [key, value] of Object.entries(query ?? {})) {
62
+ if (value) url.searchParams.set(key, value);
63
+ }
64
+ return url.href;
65
+ }
66
+ function looksLikeAbleTimeUlid(id) {
67
+ return ULID_RE.test(id.trim());
68
+ }
69
+ async function abletimeRestRequest(path, opts = {}) {
70
+ const raw = opts.token ?? getAbleTimeAccessToken();
71
+ const token = normalizeAbleTimeCredential(raw ?? "");
72
+ if (!token) {
73
+ throw new Error("AbleTime is not connected \u2014 paste an API key or personal access token in Account settings");
74
+ }
75
+ const host = opts.host ?? (opts.token ? DEFAULT_ABLETIME_HOST : getAbleTimeHost());
76
+ const url = abletimeRestUrl(path, host, opts.query);
77
+ const method = (opts.method ?? "GET").toUpperCase();
78
+ const res = await httpFetch(url, {
79
+ method,
80
+ headers: {
81
+ Accept: "application/json",
82
+ Authorization: `Bearer ${token}`,
83
+ ...opts.body !== void 0 ? { "Content-Type": "application/json" } : {}
84
+ },
85
+ ...opts.body !== void 0 ? { body: JSON.stringify(opts.body) } : {}
86
+ });
87
+ const text = await res.text().catch(() => "");
88
+ if (!res.ok) {
89
+ throw new Error(
90
+ rewriteAbleTimeError(
91
+ `AbleTime API error ${res.status}${text ? `: ${text.slice(0, 240)}` : ""}`
92
+ )
93
+ );
94
+ }
95
+ if (!text.trim()) return null;
96
+ try {
97
+ return JSON.parse(text);
98
+ } catch {
99
+ throw new Error(`AbleTime API returned a non-JSON response: ${text.slice(0, 200)}`);
100
+ }
101
+ }
102
+ async function restPages(path, opts, maxPages = 20) {
103
+ const items = [];
104
+ let cursor;
105
+ for (let i = 0; i < maxPages; i += 1) {
106
+ const raw = await abletimeRestRequest(path, {
107
+ ...opts,
108
+ query: { ...opts.query, ...cursor ? { cursor } : {} }
109
+ });
110
+ items.push(...asList(raw));
111
+ const page = asRecord(asRecord(raw)?.page);
112
+ const next = page ? asString(page.nextCursor) : "";
113
+ if (!next) break;
114
+ cursor = next;
115
+ }
116
+ return items;
117
+ }
118
+ function userDisplayName(raw) {
119
+ const rec = asRecord(raw);
120
+ if (!rec) return null;
121
+ const id = asString(rec.userId) || asString(rec.id);
122
+ const name = [asString(rec.firstName), asString(rec.lastName)].filter(Boolean).join(" ") || asString(rec.username) || asString(rec.email) || asString(rec.name);
123
+ if (!name) return null;
124
+ return { id: id || void 0, name };
125
+ }
126
+ function restTaskToRaw(raw, users) {
127
+ const rec = asRecord(raw);
128
+ if (!rec) return null;
129
+ const id = asString(rec.timeflowTaskId) || asString(rec.taskId) || asString(rec.id);
130
+ const identifier = asString(rec.taskRef) || asString(rec.reference) || id;
131
+ const title = asString(rec.title);
132
+ if (!id && !identifier && !title) return null;
133
+ const assigned = asString(rec.assignedUserId);
134
+ const assignee = assigned ? users.get(assigned) : void 0;
135
+ const comments = asList(rec.comments).map((item) => {
136
+ const comment = asRecord(item) ?? {};
137
+ const user = userDisplayName(comment);
138
+ return {
139
+ id: asString(comment.commentId) || asString(comment.id),
140
+ body: textFromAbleTimeDoc(comment.content) || asString(comment.contentSanitized) || asString(comment.body),
141
+ createdAt: asString(comment.createdAt) || asString(comment.lastUpdate),
142
+ user: user?.name || asString(comment.username)
143
+ };
144
+ });
145
+ return {
146
+ id,
147
+ taskId: id,
148
+ timeflowTaskId: id,
149
+ reference: identifier,
150
+ taskRef: identifier,
151
+ title: title || identifier,
152
+ state: asString(rec.taskState) || asString(rec.state),
153
+ description: textFromAbleTimeDoc(rec.description) || asString(rec.descriptionSanitized),
154
+ projectId: asString(rec.projectId),
155
+ categoryId: asString(rec.projectCategoryId) || asString(rec.categoryId),
156
+ tags: rec.tags,
157
+ comments,
158
+ createdAt: asString(rec.dateCreated) || asString(rec.createdAt),
159
+ updatedAt: asString(rec.lastUpdate) || asString(rec.updatedAt),
160
+ ...assignee ? { assignee } : {}
161
+ };
162
+ }
163
+ function restProjectToRaw(raw) {
164
+ const rec = asRecord(raw);
165
+ if (!rec) return null;
166
+ const id = asString(rec.projectId) || asString(rec.id);
167
+ const name = asString(rec.projectName) || asString(rec.name);
168
+ if (!id && !name) return null;
169
+ const categories = asList(rec.categories).map((item) => {
170
+ const cat = asRecord(item) ?? {};
171
+ return {
172
+ id: asString(cat.projectCategoryId) || asString(cat.categoryId) || asString(cat.id),
173
+ name: asString(cat.categoryName) || asString(cat.name)
174
+ };
175
+ });
176
+ return { id, name, projectId: id, projectName: name, categories };
177
+ }
178
+ async function listUsers(opts) {
179
+ const users = /* @__PURE__ */ new Map();
180
+ for (const item of await restPages("/users", opts).catch(() => [])) {
181
+ const user = userDisplayName(item);
182
+ const rec = asRecord(item);
183
+ const id = user?.id || (rec ? asString(rec.userId) : "");
184
+ if (user && id) users.set(id, user);
185
+ }
186
+ return users;
187
+ }
188
+ async function listProjectsRaw(opts) {
189
+ const items = await restPages("/projects", { ...opts, query: { expand: "categories" } });
190
+ return items.map(restProjectToRaw).filter((item) => Boolean(item));
191
+ }
192
+ async function listTasksRaw(opts) {
193
+ const [users, items] = await Promise.all([
194
+ listUsers(opts),
195
+ restPages("/tasks", {
196
+ ...opts,
197
+ query: {
198
+ ...opts.projectId ? { projectId: opts.projectId } : {}
199
+ }
200
+ })
201
+ ]);
202
+ let tasks = items.map((item) => restTaskToRaw(item, users)).filter((item) => Boolean(item));
203
+ const needle = opts.query?.trim().toLowerCase();
204
+ if (needle) {
205
+ tasks = tasks.filter((task) => {
206
+ const hay = [task.title, task.reference, task.taskRef, task.id].map((value) => String(value ?? "").toLowerCase()).join(" ");
207
+ const tags = Array.isArray(task.tags) ? task.tags.join(" ").toLowerCase() : "";
208
+ return hay.includes(needle) || tags.includes(needle);
209
+ });
210
+ }
211
+ return tasks;
212
+ }
213
+ async function resolveTaskId(id, opts) {
214
+ const wanted = id.trim();
215
+ if (!wanted) throw new Error("AbleTime task id is required");
216
+ if (looksLikeAbleTimeUlid(wanted)) return wanted;
217
+ const tasks = await listTasksRaw(opts);
218
+ const match = tasks.find((task) => {
219
+ const ref = String(task.reference ?? task.taskRef ?? "").toLowerCase();
220
+ const taskId = String(task.id ?? "").toLowerCase();
221
+ return ref === wanted.toLowerCase() || taskId === wanted.toLowerCase();
222
+ });
223
+ if (!match?.id) throw new Error(`AbleTime task not found: ${id}`);
224
+ return String(match.id);
225
+ }
226
+ async function getTaskRaw(id, opts) {
227
+ const taskId = await resolveTaskId(id, opts);
228
+ const [users, raw] = await Promise.all([
229
+ listUsers(opts),
230
+ abletimeRestRequest(`/tasks/${encodeURIComponent(taskId)}`, {
231
+ ...opts,
232
+ query: { expand: "comments" }
233
+ })
234
+ ]);
235
+ const task = restTaskToRaw(raw, users);
236
+ if (!task) throw new Error(`AbleTime task not found: ${id}`);
237
+ return task;
238
+ }
239
+ function firstStringArg(args, keys) {
240
+ for (const key of keys) {
241
+ const value = asString(args[key]);
242
+ if (value) return value;
243
+ }
244
+ return "";
245
+ }
246
+ async function callAbleTimeRestTool(name, args = {}, opts) {
247
+ const token = normalizeAbleTimeCredential(opts?.token ?? getAbleTimeAccessToken() ?? "");
248
+ if (!token) {
249
+ throw new Error("AbleTime is not connected \u2014 paste an API key or connect via browser");
250
+ }
251
+ if (ableTimeCredentialKind(token) === "pat") {
252
+ throw new Error("AbleTime REST transport is for organization keys and OAuth tokens, not apt_\u2026");
253
+ }
254
+ const requestOpts = { ...opts, token };
255
+ switch (name) {
256
+ case "orientation": {
257
+ const [users, projects, tasks] = await Promise.all([
258
+ listUsers(requestOpts),
259
+ listProjectsRaw(requestOpts),
260
+ listTasksRaw(requestOpts)
261
+ ]);
262
+ const viewer = [...users.values()].find((user) => user.name) ?? [...users.values()][0] ?? { name: "AbleTime" };
263
+ return { viewer, projects, tasks };
264
+ }
265
+ case "list_projects":
266
+ return { data: await listProjectsRaw(requestOpts) };
267
+ case "list_users":
268
+ return { data: [...(await listUsers(requestOpts)).values()] };
269
+ case "list_tasks":
270
+ return {
271
+ data: await listTasksRaw({
272
+ ...requestOpts,
273
+ projectId: firstStringArg(args, ["project_id", "projectId", "project"])
274
+ })
275
+ };
276
+ case "search_tasks":
277
+ return {
278
+ data: await listTasksRaw({
279
+ ...requestOpts,
280
+ query: firstStringArg(args, ["query", "q"])
281
+ })
282
+ };
283
+ case "get_task":
284
+ return await getTaskRaw(firstStringArg(args, ["id", "task_id", "task"]), requestOpts);
285
+ case "create_task": {
286
+ const title = firstStringArg(args, ["title"]);
287
+ if (!title) throw new Error("AbleTime task title is required");
288
+ const projectId = firstStringArg(args, ["project_id", "projectId", "project"]);
289
+ if (!projectId) throw new Error("AbleTime project is required");
290
+ const projects = await listProjectsRaw(requestOpts);
291
+ const project = projects.find(
292
+ (item) => String(item.id).toLowerCase() === projectId.toLowerCase() || String(item.name).toLowerCase() === projectId.toLowerCase()
293
+ ) ?? projects[0];
294
+ if (!project) throw new Error("AbleTime has no projects to create a task in");
295
+ const categories = asList(project.categories);
296
+ const wantedCategory = firstStringArg(args, ["category_id", "categoryId", "category"]);
297
+ const category = categories.map((item) => asRecord(item)).find((item) => {
298
+ if (!item || !wantedCategory) return false;
299
+ return asString(item.id).toLowerCase() === wantedCategory.toLowerCase() || asString(item.name).toLowerCase() === wantedCategory.toLowerCase();
300
+ }) ?? asRecord(categories[0]);
301
+ const created = await abletimeRestRequest("/tasks", {
302
+ ...requestOpts,
303
+ method: "POST",
304
+ body: {
305
+ projectId: project.id,
306
+ projectCategoryId: category ? asString(category.id) : void 0,
307
+ title
308
+ }
309
+ });
310
+ const state = firstStringArg(args, ["state"]);
311
+ const createdId = asString(asRecord(created)?.timeflowTaskId);
312
+ const description = asString(args.description);
313
+ if (createdId && description) {
314
+ await abletimeRestRequest(`/tasks/${encodeURIComponent(createdId)}`, {
315
+ ...requestOpts,
316
+ method: "PATCH",
317
+ body: { description: ableTimeDocFromText(description) }
318
+ }).catch(() => void 0);
319
+ }
320
+ if (createdId && state && state !== "todo") {
321
+ await abletimeRestRequest(`/tasks/${encodeURIComponent(createdId)}/state`, {
322
+ ...requestOpts,
323
+ method: "POST",
324
+ body: { taskState: state }
325
+ }).catch(() => void 0);
326
+ }
327
+ return createdId ? await getTaskRaw(createdId, requestOpts) : restTaskToRaw(created, await listUsers(requestOpts));
328
+ }
329
+ case "update_task": {
330
+ const id = await resolveTaskId(firstStringArg(args, ["id", "task_id", "task"]), requestOpts);
331
+ const patch = {};
332
+ const title = firstStringArg(args, ["title"]);
333
+ if (title) patch.title = title;
334
+ if (args.description !== void 0) {
335
+ patch.description = args.description == null || args.description === "" ? null : ableTimeDocFromText(String(args.description));
336
+ }
337
+ if (args.tags !== void 0 || args.labels !== void 0) {
338
+ patch.tags = args.tags ?? args.labels ?? args.tag_names;
339
+ }
340
+ if (Object.keys(patch).length > 0) {
341
+ await abletimeRestRequest(`/tasks/${encodeURIComponent(id)}`, {
342
+ ...requestOpts,
343
+ method: "PATCH",
344
+ body: patch
345
+ });
346
+ }
347
+ return await getTaskRaw(id, requestOpts);
348
+ }
349
+ case "set_task_state": {
350
+ const id = await resolveTaskId(firstStringArg(args, ["id", "task_id", "task"]), requestOpts);
351
+ const state = firstStringArg(args, ["state", "taskState"]);
352
+ if (!state) throw new Error("AbleTime task state is required");
353
+ await abletimeRestRequest(`/tasks/${encodeURIComponent(id)}/state`, {
354
+ ...requestOpts,
355
+ method: "POST",
356
+ body: { taskState: state }
357
+ });
358
+ return await getTaskRaw(id, requestOpts);
359
+ }
360
+ case "create_comment": {
361
+ const id = await resolveTaskId(firstStringArg(args, ["id", "task_id", "task"]), requestOpts);
362
+ const body = firstStringArg(args, ["body", "comment", "text"]);
363
+ if (!body) throw new Error("AbleTime comment body is required");
364
+ const created = await abletimeRestRequest(`/tasks/${encodeURIComponent(id)}/comments`, {
365
+ ...requestOpts,
366
+ method: "POST",
367
+ body: { content: ableTimeDocFromText(body) }
368
+ });
369
+ const rec = asRecord(created) ?? {};
370
+ return {
371
+ id: asString(rec.commentId) || void 0,
372
+ body: textFromAbleTimeDoc(rec.content) || body
373
+ };
374
+ }
375
+ case "set_task_dependency":
376
+ throw new Error(
377
+ "AbleTime organization API keys cannot set task dependencies on REST yet \u2014 use a personal access token (apt_\u2026) for MCP project-management tools."
378
+ );
379
+ default:
380
+ throw new Error(`AbleTime organization API keys do not support ${name} on REST`);
381
+ }
382
+ }
383
+
384
+ export {
385
+ ABLETIME_REST_PREFIX,
386
+ ableTimeDocFromText,
387
+ textFromAbleTimeDoc,
388
+ abletimeRestUrl,
389
+ looksLikeAbleTimeUlid,
390
+ callAbleTimeRestTool
391
+ };