@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
package/dist/mcp/run-stdio.cjs
CHANGED
|
@@ -3424,6 +3424,9 @@ function normalizeVault(raw) {
|
|
|
3424
3424
|
if (typeof raw.abletimeAccessToken === "string" && raw.abletimeAccessToken.trim()) {
|
|
3425
3425
|
out.abletimeAccessToken = raw.abletimeAccessToken.trim();
|
|
3426
3426
|
}
|
|
3427
|
+
if (typeof raw.abletimeRefreshToken === "string" && raw.abletimeRefreshToken.trim()) {
|
|
3428
|
+
out.abletimeRefreshToken = raw.abletimeRefreshToken.trim();
|
|
3429
|
+
}
|
|
3427
3430
|
if (typeof raw.vercelToken === "string" && raw.vercelToken.trim()) {
|
|
3428
3431
|
out.vercelToken = raw.vercelToken.trim();
|
|
3429
3432
|
}
|
|
@@ -3629,6 +3632,7 @@ function emptyPublicIntegrations() {
|
|
|
3629
3632
|
hasSlackAppToken: false,
|
|
3630
3633
|
hasGithubPat: false,
|
|
3631
3634
|
hasAbleTimeToken: false,
|
|
3635
|
+
hasAbleTimeOAuth: false,
|
|
3632
3636
|
hasVercelToken: false,
|
|
3633
3637
|
hasSupabaseToken: false,
|
|
3634
3638
|
hasPosthogToken: false,
|
|
@@ -3653,6 +3657,7 @@ function toPublicAppSettings(settings) {
|
|
|
3653
3657
|
const slackAppToken = integrations.slackAppToken;
|
|
3654
3658
|
const githubPat = integrations.githubPat;
|
|
3655
3659
|
const abletimeAccessToken = integrations.abletimeAccessToken;
|
|
3660
|
+
const abletimeRefreshToken = integrations.abletimeRefreshToken;
|
|
3656
3661
|
const vercelToken = integrations.vercelToken;
|
|
3657
3662
|
const supabaseAccessToken = integrations.supabaseAccessToken;
|
|
3658
3663
|
const posthogPersonalApiKey = integrations.posthogPersonalApiKey;
|
|
@@ -3665,6 +3670,7 @@ function toPublicAppSettings(settings) {
|
|
|
3665
3670
|
delete integrations.slackAppToken;
|
|
3666
3671
|
delete integrations.githubPat;
|
|
3667
3672
|
delete integrations.abletimeAccessToken;
|
|
3673
|
+
delete integrations.abletimeRefreshToken;
|
|
3668
3674
|
delete integrations.vercelToken;
|
|
3669
3675
|
delete integrations.supabaseAccessToken;
|
|
3670
3676
|
delete integrations.posthogPersonalApiKey;
|
|
@@ -3679,7 +3685,8 @@ function toPublicAppSettings(settings) {
|
|
|
3679
3685
|
hasSlackClientSecret: Boolean(slackClientSecret?.trim()),
|
|
3680
3686
|
hasSlackAppToken: Boolean(slackAppToken?.trim()),
|
|
3681
3687
|
hasGithubPat: Boolean(githubPat?.trim()),
|
|
3682
|
-
hasAbleTimeToken: Boolean(abletimeAccessToken?.trim()),
|
|
3688
|
+
hasAbleTimeToken: Boolean(abletimeAccessToken?.trim() || abletimeRefreshToken?.trim()),
|
|
3689
|
+
hasAbleTimeOAuth: Boolean(abletimeRefreshToken?.trim()),
|
|
3683
3690
|
hasVercelToken: Boolean(vercelToken?.trim()),
|
|
3684
3691
|
hasSupabaseToken: Boolean(supabaseAccessToken?.trim()),
|
|
3685
3692
|
hasPosthogToken: Boolean(posthogPersonalApiKey?.trim()),
|
|
@@ -3817,6 +3824,16 @@ function normalizeIntegrations(raw) {
|
|
|
3817
3824
|
const token = source.abletimeAccessToken.trim();
|
|
3818
3825
|
if (token) out.abletimeAccessToken = token;
|
|
3819
3826
|
}
|
|
3827
|
+
if (typeof source.abletimeRefreshToken === "string") {
|
|
3828
|
+
const token = source.abletimeRefreshToken.trim();
|
|
3829
|
+
if (token) out.abletimeRefreshToken = token;
|
|
3830
|
+
}
|
|
3831
|
+
if (typeof source.abletimeTokenExpiresAt === "number" && Number.isFinite(source.abletimeTokenExpiresAt)) {
|
|
3832
|
+
out.abletimeTokenExpiresAt = source.abletimeTokenExpiresAt;
|
|
3833
|
+
} else if (typeof source.abletimeTokenExpiresAt === "string") {
|
|
3834
|
+
const n = Number(source.abletimeTokenExpiresAt);
|
|
3835
|
+
if (Number.isFinite(n)) out.abletimeTokenExpiresAt = n;
|
|
3836
|
+
}
|
|
3820
3837
|
if (typeof source.abletimeHost === "string") {
|
|
3821
3838
|
const host = source.abletimeHost.trim();
|
|
3822
3839
|
if (host) out.abletimeHost = host;
|
|
@@ -4094,7 +4111,7 @@ function readSettingsFile() {
|
|
|
4094
4111
|
}
|
|
4095
4112
|
function diskHoldsSecrets(disk) {
|
|
4096
4113
|
return Boolean(
|
|
4097
|
-
disk.integrations.linearApiKey || disk.integrations.linearAccessToken || disk.integrations.linearRefreshToken || disk.integrations.linearClientSecret || disk.integrations.slackClientSecret || disk.integrations.slackAppToken || disk.integrations.githubPat || disk.integrations.abletimeAccessToken || disk.integrations.vercelToken || disk.integrations.supabaseAccessToken || disk.integrations.posthogPersonalApiKey || disk.integrations.sentryAuthToken || Object.keys(disk.environment).length > 0
|
|
4114
|
+
disk.integrations.linearApiKey || disk.integrations.linearAccessToken || disk.integrations.linearRefreshToken || disk.integrations.linearClientSecret || disk.integrations.slackClientSecret || disk.integrations.slackAppToken || disk.integrations.githubPat || disk.integrations.abletimeAccessToken || disk.integrations.abletimeRefreshToken || disk.integrations.vercelToken || disk.integrations.supabaseAccessToken || disk.integrations.posthogPersonalApiKey || disk.integrations.sentryAuthToken || Object.keys(disk.environment).length > 0
|
|
4098
4115
|
);
|
|
4099
4116
|
}
|
|
4100
4117
|
function mergeVault(disk) {
|
|
@@ -4125,6 +4142,9 @@ function mergeVault(disk) {
|
|
|
4125
4142
|
if (vault.abletimeAccessToken && !integrations.abletimeAccessToken) {
|
|
4126
4143
|
integrations.abletimeAccessToken = vault.abletimeAccessToken;
|
|
4127
4144
|
}
|
|
4145
|
+
if (vault.abletimeRefreshToken && !integrations.abletimeRefreshToken) {
|
|
4146
|
+
integrations.abletimeRefreshToken = vault.abletimeRefreshToken;
|
|
4147
|
+
}
|
|
4128
4148
|
if (vault.vercelToken && !integrations.vercelToken) {
|
|
4129
4149
|
integrations.vercelToken = vault.vercelToken;
|
|
4130
4150
|
}
|
|
@@ -4149,6 +4169,7 @@ function persistSplit(settings) {
|
|
|
4149
4169
|
const slackAppToken = integrations.slackAppToken;
|
|
4150
4170
|
const githubPat = integrations.githubPat;
|
|
4151
4171
|
const abletimeAccessToken = integrations.abletimeAccessToken;
|
|
4172
|
+
const abletimeRefreshToken = integrations.abletimeRefreshToken;
|
|
4152
4173
|
const vercelToken = integrations.vercelToken;
|
|
4153
4174
|
const supabaseAccessToken = integrations.supabaseAccessToken;
|
|
4154
4175
|
const posthogPersonalApiKey = integrations.posthogPersonalApiKey;
|
|
@@ -4161,6 +4182,7 @@ function persistSplit(settings) {
|
|
|
4161
4182
|
delete integrations.slackAppToken;
|
|
4162
4183
|
delete integrations.githubPat;
|
|
4163
4184
|
delete integrations.abletimeAccessToken;
|
|
4185
|
+
delete integrations.abletimeRefreshToken;
|
|
4164
4186
|
delete integrations.vercelToken;
|
|
4165
4187
|
delete integrations.supabaseAccessToken;
|
|
4166
4188
|
delete integrations.posthogPersonalApiKey;
|
|
@@ -4179,6 +4201,7 @@ function persistSplit(settings) {
|
|
|
4179
4201
|
slackAppToken,
|
|
4180
4202
|
githubPat,
|
|
4181
4203
|
abletimeAccessToken,
|
|
4204
|
+
abletimeRefreshToken,
|
|
4182
4205
|
vercelToken,
|
|
4183
4206
|
supabaseAccessToken,
|
|
4184
4207
|
posthogPersonalApiKey,
|
|
@@ -4365,6 +4388,8 @@ function updateIntegrationsSettings(patch) {
|
|
|
4365
4388
|
if ("abletimeAccessToken" in patch) {
|
|
4366
4389
|
if (patch.abletimeAccessToken == null || patch.abletimeAccessToken.trim() === "") {
|
|
4367
4390
|
delete integrations.abletimeAccessToken;
|
|
4391
|
+
delete integrations.abletimeRefreshToken;
|
|
4392
|
+
delete integrations.abletimeTokenExpiresAt;
|
|
4368
4393
|
delete integrations.abletimeViewerName;
|
|
4369
4394
|
} else {
|
|
4370
4395
|
integrations.abletimeAccessToken = patch.abletimeAccessToken.trim();
|
|
@@ -4715,7 +4740,9 @@ function disconnectLinearConnection() {
|
|
|
4715
4740
|
return saveAppSettings({ ...current, integrations });
|
|
4716
4741
|
}
|
|
4717
4742
|
function isAbleTimeConnected(settings = loadAppSettings()) {
|
|
4718
|
-
return Boolean(
|
|
4743
|
+
return Boolean(
|
|
4744
|
+
settings.integrations.abletimeAccessToken?.trim() || settings.integrations.abletimeRefreshToken?.trim()
|
|
4745
|
+
);
|
|
4719
4746
|
}
|
|
4720
4747
|
function getAbleTimeAccessToken(settings = loadAppSettings()) {
|
|
4721
4748
|
const token = settings.integrations.abletimeAccessToken?.trim();
|
|
@@ -4728,6 +4755,12 @@ function saveAbleTimeConnection(input) {
|
|
|
4728
4755
|
const current = loadAppSettings();
|
|
4729
4756
|
const integrations = { ...current.integrations };
|
|
4730
4757
|
integrations.abletimeAccessToken = input.accessToken.trim();
|
|
4758
|
+
if (input.refreshToken?.trim()) {
|
|
4759
|
+
integrations.abletimeRefreshToken = input.refreshToken.trim();
|
|
4760
|
+
}
|
|
4761
|
+
if (typeof input.expiresIn === "number" && Number.isFinite(input.expiresIn)) {
|
|
4762
|
+
integrations.abletimeTokenExpiresAt = Date.now() + Math.max(0, input.expiresIn) * 1e3;
|
|
4763
|
+
}
|
|
4731
4764
|
if (input.host?.trim()) {
|
|
4732
4765
|
integrations.abletimeHost = input.host.trim();
|
|
4733
4766
|
}
|
|
@@ -4740,6 +4773,8 @@ function disconnectAbleTimeConnection() {
|
|
|
4740
4773
|
const current = loadAppSettings();
|
|
4741
4774
|
const integrations = { ...current.integrations };
|
|
4742
4775
|
delete integrations.abletimeAccessToken;
|
|
4776
|
+
delete integrations.abletimeRefreshToken;
|
|
4777
|
+
delete integrations.abletimeTokenExpiresAt;
|
|
4743
4778
|
delete integrations.abletimeViewerName;
|
|
4744
4779
|
return saveAppSettings({ ...current, integrations });
|
|
4745
4780
|
}
|
|
@@ -15373,6 +15408,645 @@ var init_home_board = __esm({
|
|
|
15373
15408
|
}
|
|
15374
15409
|
});
|
|
15375
15410
|
|
|
15411
|
+
// src/integrations/abletime-oauth.ts
|
|
15412
|
+
var abletime_oauth_exports = {};
|
|
15413
|
+
__export(abletime_oauth_exports, {
|
|
15414
|
+
ABLETIME_OAUTH_CANCELLED: () => ABLETIME_OAUTH_CANCELLED,
|
|
15415
|
+
ABLETIME_OAUTH_PORT: () => ABLETIME_OAUTH_PORT,
|
|
15416
|
+
ABLETIME_OAUTH_REDIRECT: () => ABLETIME_OAUTH_REDIRECT,
|
|
15417
|
+
ABLETIME_OAUTH_RESOURCE: () => ABLETIME_OAUTH_RESOURCE,
|
|
15418
|
+
AbleTimeOAuthCancelledError: () => AbleTimeOAuthCancelledError,
|
|
15419
|
+
BAKED_ABLETIME_OAUTH_CLIENT_ID: () => BAKED_ABLETIME_OAUTH_CLIENT_ID,
|
|
15420
|
+
ableTimeOAuthAuthorizeUrl: () => ableTimeOAuthAuthorizeUrl,
|
|
15421
|
+
ableTimeOAuthClientId: () => ableTimeOAuthClientId,
|
|
15422
|
+
ableTimeOAuthResource: () => ableTimeOAuthResource,
|
|
15423
|
+
createAbleTimePkce: () => createAbleTimePkce,
|
|
15424
|
+
ensureAbleTimeAccessToken: () => ensureAbleTimeAccessToken,
|
|
15425
|
+
hasBakedAbleTimeOAuth: () => hasBakedAbleTimeOAuth,
|
|
15426
|
+
isAbleTimeOAuthCancelled: () => isAbleTimeOAuthCancelled,
|
|
15427
|
+
startAbleTimeOAuth: () => startAbleTimeOAuth
|
|
15428
|
+
});
|
|
15429
|
+
function hasBakedAbleTimeOAuth() {
|
|
15430
|
+
return Boolean(BAKED_ABLETIME_OAUTH_CLIENT_ID.trim());
|
|
15431
|
+
}
|
|
15432
|
+
function ableTimeOAuthClientId() {
|
|
15433
|
+
return process.env.SIDEBOARD_ABLETIME_OAUTH_CLIENT_ID?.trim() || BAKED_ABLETIME_OAUTH_CLIENT_ID.trim();
|
|
15434
|
+
}
|
|
15435
|
+
function ableTimeOAuthResource(host) {
|
|
15436
|
+
return `${normalizeAbleTimeHost(host)}/api/public/v2/mcp`;
|
|
15437
|
+
}
|
|
15438
|
+
function createAbleTimePkce() {
|
|
15439
|
+
const verifier = (0, import_node_crypto9.randomBytes)(32).toString("base64url");
|
|
15440
|
+
const challenge = (0, import_node_crypto9.createHash)("sha256").update(verifier).digest("base64url");
|
|
15441
|
+
return { verifier, challenge };
|
|
15442
|
+
}
|
|
15443
|
+
function ableTimeOAuthAuthorizeUrl(clientId, state, codeChallenge, host) {
|
|
15444
|
+
const base = normalizeAbleTimeHost(host);
|
|
15445
|
+
const params = new URLSearchParams({
|
|
15446
|
+
response_type: "code",
|
|
15447
|
+
client_id: clientId,
|
|
15448
|
+
redirect_uri: ABLETIME_OAUTH_REDIRECT,
|
|
15449
|
+
code_challenge: codeChallenge,
|
|
15450
|
+
code_challenge_method: "S256",
|
|
15451
|
+
state,
|
|
15452
|
+
resource: ableTimeOAuthResource(host)
|
|
15453
|
+
});
|
|
15454
|
+
return `${base}/oauth/authorize?${params.toString()}`;
|
|
15455
|
+
}
|
|
15456
|
+
function htmlPage(title, body) {
|
|
15457
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title>
|
|
15458
|
+
<style>body{font-family:ui-sans-serif,system-ui,sans-serif;padding:48px 24px;max-width:36rem;margin:0 auto;color:#1a1a1a}
|
|
15459
|
+
h1{font-size:1.25rem}p{line-height:1.5;color:#444}</style></head><body>${body}</body></html>`;
|
|
15460
|
+
}
|
|
15461
|
+
async function exchangeAbleTimeToken(body, host) {
|
|
15462
|
+
const url = `${normalizeAbleTimeHost(host)}/api/oauth/token`;
|
|
15463
|
+
const res = await httpFetch(url, {
|
|
15464
|
+
method: "POST",
|
|
15465
|
+
headers: {
|
|
15466
|
+
Accept: "application/json",
|
|
15467
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
15468
|
+
},
|
|
15469
|
+
body
|
|
15470
|
+
});
|
|
15471
|
+
const data = await res.json().catch(() => ({}));
|
|
15472
|
+
if (!res.ok || data.error || !data.access_token?.trim()) {
|
|
15473
|
+
const detail = data.error_description || data.error || `HTTP ${res.status}`;
|
|
15474
|
+
throw new Error(`AbleTime OAuth token exchange failed: ${detail}`);
|
|
15475
|
+
}
|
|
15476
|
+
return data;
|
|
15477
|
+
}
|
|
15478
|
+
function persistAbleTimeTokens(data, input) {
|
|
15479
|
+
return saveAbleTimeConnection({
|
|
15480
|
+
accessToken: data.access_token.trim(),
|
|
15481
|
+
refreshToken: data.refresh_token?.trim(),
|
|
15482
|
+
expiresIn: data.expires_in,
|
|
15483
|
+
host: input?.host,
|
|
15484
|
+
viewerName: input?.viewerName
|
|
15485
|
+
});
|
|
15486
|
+
}
|
|
15487
|
+
async function refreshAbleTimeAccessToken(refreshToken) {
|
|
15488
|
+
const host = getAbleTimeHost();
|
|
15489
|
+
const body = new URLSearchParams({
|
|
15490
|
+
grant_type: "refresh_token",
|
|
15491
|
+
refresh_token: refreshToken,
|
|
15492
|
+
client_id: ableTimeOAuthClientId(),
|
|
15493
|
+
resource: ableTimeOAuthResource(host)
|
|
15494
|
+
});
|
|
15495
|
+
try {
|
|
15496
|
+
const data = await exchangeAbleTimeToken(body, host);
|
|
15497
|
+
persistAbleTimeTokens(data, { host });
|
|
15498
|
+
return data.access_token.trim();
|
|
15499
|
+
} catch (err) {
|
|
15500
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
15501
|
+
if (/invalid_grant|invalid_token|unauthorized/i.test(message)) {
|
|
15502
|
+
disconnectAbleTimeConnection();
|
|
15503
|
+
throw new Error("AbleTime sign-in expired \u2014 connect again in Account settings");
|
|
15504
|
+
}
|
|
15505
|
+
throw err;
|
|
15506
|
+
}
|
|
15507
|
+
}
|
|
15508
|
+
async function ensureAbleTimeAccessToken() {
|
|
15509
|
+
const settings = loadAppSettings();
|
|
15510
|
+
const access = settings.integrations.abletimeAccessToken?.trim() || "";
|
|
15511
|
+
const refresh = settings.integrations.abletimeRefreshToken?.trim() || "";
|
|
15512
|
+
const expiresAt = settings.integrations.abletimeTokenExpiresAt ?? 0;
|
|
15513
|
+
if (access && (!refresh || Date.now() < expiresAt - REFRESH_SKEW_MS2)) {
|
|
15514
|
+
return access;
|
|
15515
|
+
}
|
|
15516
|
+
if (refresh) {
|
|
15517
|
+
if (!refreshInFlight) {
|
|
15518
|
+
refreshInFlight = refreshAbleTimeAccessToken(refresh).finally(() => {
|
|
15519
|
+
refreshInFlight = null;
|
|
15520
|
+
});
|
|
15521
|
+
}
|
|
15522
|
+
return refreshInFlight;
|
|
15523
|
+
}
|
|
15524
|
+
return access || null;
|
|
15525
|
+
}
|
|
15526
|
+
function isAbleTimeOAuthCancelled(err) {
|
|
15527
|
+
if (err instanceof AbleTimeOAuthCancelledError) return true;
|
|
15528
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
15529
|
+
return message.includes(ABLETIME_OAUTH_CANCELLED);
|
|
15530
|
+
}
|
|
15531
|
+
async function startAbleTimeOAuth(opts) {
|
|
15532
|
+
if (opts?.signal?.aborted) {
|
|
15533
|
+
throw new AbleTimeOAuthCancelledError();
|
|
15534
|
+
}
|
|
15535
|
+
const clientId = ableTimeOAuthClientId();
|
|
15536
|
+
if (!clientId) {
|
|
15537
|
+
throw new Error(
|
|
15538
|
+
`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.`
|
|
15539
|
+
);
|
|
15540
|
+
}
|
|
15541
|
+
const host = opts?.host?.trim() || void 0;
|
|
15542
|
+
const state = (0, import_node_crypto9.randomBytes)(16).toString("hex");
|
|
15543
|
+
const pkce = createAbleTimePkce();
|
|
15544
|
+
const authorizeUrl = ableTimeOAuthAuthorizeUrl(clientId, state, pkce.challenge, host);
|
|
15545
|
+
const timeoutMs = opts?.timeoutMs ?? 5 * 6e4;
|
|
15546
|
+
const code = await new Promise((resolve, reject) => {
|
|
15547
|
+
let settled = false;
|
|
15548
|
+
const finish = (err, value) => {
|
|
15549
|
+
if (settled) return;
|
|
15550
|
+
settled = true;
|
|
15551
|
+
cleanup();
|
|
15552
|
+
if (err) reject(err);
|
|
15553
|
+
else resolve(value);
|
|
15554
|
+
};
|
|
15555
|
+
const server = (0, import_node_http.createServer)((req, res) => {
|
|
15556
|
+
try {
|
|
15557
|
+
const url = new URL(req.url || "/", `http://127.0.0.1:${ABLETIME_OAUTH_PORT}`);
|
|
15558
|
+
if (url.pathname !== "/callback") {
|
|
15559
|
+
res.writeHead(404);
|
|
15560
|
+
res.end("Not found");
|
|
15561
|
+
return;
|
|
15562
|
+
}
|
|
15563
|
+
const err = url.searchParams.get("error");
|
|
15564
|
+
const gotState = url.searchParams.get("state");
|
|
15565
|
+
const gotCode = url.searchParams.get("code");
|
|
15566
|
+
if (err) {
|
|
15567
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
15568
|
+
res.end(htmlPage("AbleTime", `<h1>Authorization cancelled</h1><p>${err}</p>`));
|
|
15569
|
+
finish(new AbleTimeOAuthCancelledError(`AbleTime OAuth: ${err}`));
|
|
15570
|
+
return;
|
|
15571
|
+
}
|
|
15572
|
+
if (gotState !== state || !gotCode) {
|
|
15573
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
15574
|
+
res.end(htmlPage("AbleTime", "<h1>Invalid callback</h1><p>State or code missing.</p>"));
|
|
15575
|
+
finish(new Error("AbleTime OAuth callback was invalid"));
|
|
15576
|
+
return;
|
|
15577
|
+
}
|
|
15578
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
15579
|
+
res.end(
|
|
15580
|
+
htmlPage(
|
|
15581
|
+
"AbleTime connected",
|
|
15582
|
+
"<h1>AbleTime connected</h1><p>You can close this tab and return to Sideboard.</p>"
|
|
15583
|
+
)
|
|
15584
|
+
);
|
|
15585
|
+
finish(void 0, gotCode);
|
|
15586
|
+
} catch (e) {
|
|
15587
|
+
finish(e instanceof Error ? e : new Error(String(e)));
|
|
15588
|
+
}
|
|
15589
|
+
});
|
|
15590
|
+
const timer = setTimeout(() => {
|
|
15591
|
+
finish(new Error("AbleTime sign-in timed out \u2014 try again"));
|
|
15592
|
+
}, timeoutMs);
|
|
15593
|
+
const onAbort = () => finish(new AbleTimeOAuthCancelledError());
|
|
15594
|
+
const cleanup = () => {
|
|
15595
|
+
clearTimeout(timer);
|
|
15596
|
+
opts?.signal?.removeEventListener("abort", onAbort);
|
|
15597
|
+
server.close();
|
|
15598
|
+
};
|
|
15599
|
+
opts?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
15600
|
+
if (opts?.signal?.aborted) {
|
|
15601
|
+
onAbort();
|
|
15602
|
+
return;
|
|
15603
|
+
}
|
|
15604
|
+
server.on("error", (e) => {
|
|
15605
|
+
finish(
|
|
15606
|
+
e instanceof Error && e.code === "EADDRINUSE" ? new Error(
|
|
15607
|
+
`Port ${ABLETIME_OAUTH_PORT} is in use. Close whatever is bound there, or paste an AbleTime token instead.`
|
|
15608
|
+
) : e instanceof Error ? e : new Error(String(e))
|
|
15609
|
+
);
|
|
15610
|
+
});
|
|
15611
|
+
server.listen(ABLETIME_OAUTH_PORT, "127.0.0.1", () => {
|
|
15612
|
+
void Promise.resolve(opts?.openUrl?.(authorizeUrl)).catch((e) => {
|
|
15613
|
+
finish(e instanceof Error ? e : new Error(String(e)));
|
|
15614
|
+
});
|
|
15615
|
+
});
|
|
15616
|
+
});
|
|
15617
|
+
const body = new URLSearchParams({
|
|
15618
|
+
grant_type: "authorization_code",
|
|
15619
|
+
code,
|
|
15620
|
+
redirect_uri: ABLETIME_OAUTH_REDIRECT,
|
|
15621
|
+
client_id: clientId,
|
|
15622
|
+
code_verifier: pkce.verifier,
|
|
15623
|
+
resource: ableTimeOAuthResource(host)
|
|
15624
|
+
});
|
|
15625
|
+
const data = await exchangeAbleTimeToken(body, host);
|
|
15626
|
+
persistAbleTimeTokens(data, { host });
|
|
15627
|
+
const token = data.access_token.trim();
|
|
15628
|
+
try {
|
|
15629
|
+
const { getAbleTimeOrientation: getAbleTimeOrientation2 } = await Promise.resolve().then(() => (init_abletime(), abletime_exports));
|
|
15630
|
+
const orientation = await getAbleTimeOrientation2({ token, host });
|
|
15631
|
+
return saveAbleTimeConnection({
|
|
15632
|
+
accessToken: token,
|
|
15633
|
+
refreshToken: data.refresh_token?.trim(),
|
|
15634
|
+
expiresIn: data.expires_in,
|
|
15635
|
+
host,
|
|
15636
|
+
viewerName: orientation.viewer.name
|
|
15637
|
+
});
|
|
15638
|
+
} catch {
|
|
15639
|
+
return loadAppSettings();
|
|
15640
|
+
}
|
|
15641
|
+
}
|
|
15642
|
+
var import_node_crypto9, import_node_http, ABLETIME_OAUTH_PORT, ABLETIME_OAUTH_REDIRECT, BAKED_ABLETIME_OAUTH_CLIENT_ID, ABLETIME_OAUTH_RESOURCE, REFRESH_SKEW_MS2, refreshInFlight, ABLETIME_OAUTH_CANCELLED, AbleTimeOAuthCancelledError;
|
|
15643
|
+
var init_abletime_oauth = __esm({
|
|
15644
|
+
"src/integrations/abletime-oauth.ts"() {
|
|
15645
|
+
"use strict";
|
|
15646
|
+
import_node_crypto9 = require("crypto");
|
|
15647
|
+
import_node_http = require("http");
|
|
15648
|
+
init_fetch();
|
|
15649
|
+
init_app_settings();
|
|
15650
|
+
init_abletime_mcp();
|
|
15651
|
+
ABLETIME_OAUTH_PORT = 19849;
|
|
15652
|
+
ABLETIME_OAUTH_REDIRECT = `http://127.0.0.1:${ABLETIME_OAUTH_PORT}/callback`;
|
|
15653
|
+
BAKED_ABLETIME_OAUTH_CLIENT_ID = "https://www.sideboard.cloud/oauth/abletime-client.json";
|
|
15654
|
+
ABLETIME_OAUTH_RESOURCE = `${DEFAULT_ABLETIME_HOST}/api/public/v2/mcp`;
|
|
15655
|
+
REFRESH_SKEW_MS2 = 5 * 6e4;
|
|
15656
|
+
refreshInFlight = null;
|
|
15657
|
+
ABLETIME_OAUTH_CANCELLED = "AbleTime sign-in cancelled";
|
|
15658
|
+
AbleTimeOAuthCancelledError = class extends Error {
|
|
15659
|
+
constructor(message = ABLETIME_OAUTH_CANCELLED) {
|
|
15660
|
+
super(message);
|
|
15661
|
+
this.name = "AbleTimeOAuthCancelledError";
|
|
15662
|
+
}
|
|
15663
|
+
};
|
|
15664
|
+
}
|
|
15665
|
+
});
|
|
15666
|
+
|
|
15667
|
+
// src/integrations/abletime-rest.ts
|
|
15668
|
+
var abletime_rest_exports = {};
|
|
15669
|
+
__export(abletime_rest_exports, {
|
|
15670
|
+
ABLETIME_REST_PREFIX: () => ABLETIME_REST_PREFIX,
|
|
15671
|
+
ableTimeDocFromText: () => ableTimeDocFromText,
|
|
15672
|
+
abletimeRestUrl: () => abletimeRestUrl,
|
|
15673
|
+
callAbleTimeRestTool: () => callAbleTimeRestTool,
|
|
15674
|
+
looksLikeAbleTimeUlid: () => looksLikeAbleTimeUlid,
|
|
15675
|
+
textFromAbleTimeDoc: () => textFromAbleTimeDoc
|
|
15676
|
+
});
|
|
15677
|
+
function asRecord4(value) {
|
|
15678
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
15679
|
+
}
|
|
15680
|
+
function asString(value) {
|
|
15681
|
+
return typeof value === "string" ? value.trim() : "";
|
|
15682
|
+
}
|
|
15683
|
+
function asList(value) {
|
|
15684
|
+
if (Array.isArray(value)) return value;
|
|
15685
|
+
const record = asRecord4(value);
|
|
15686
|
+
if (!record) return [];
|
|
15687
|
+
for (const key of ["data", "tasks", "projects", "users", "items", "results"]) {
|
|
15688
|
+
if (Array.isArray(record[key])) return record[key];
|
|
15689
|
+
}
|
|
15690
|
+
return [];
|
|
15691
|
+
}
|
|
15692
|
+
function ableTimeDocFromText(text7) {
|
|
15693
|
+
const trimmed = text7.trim();
|
|
15694
|
+
if (!trimmed) return { type: "doc", content: [] };
|
|
15695
|
+
return {
|
|
15696
|
+
type: "doc",
|
|
15697
|
+
content: [{ type: "paragraph", content: [{ type: "text", text: trimmed }] }]
|
|
15698
|
+
};
|
|
15699
|
+
}
|
|
15700
|
+
function textFromAbleTimeDoc(value) {
|
|
15701
|
+
if (typeof value === "string") return value.trim();
|
|
15702
|
+
const record = asRecord4(value);
|
|
15703
|
+
if (!record) return "";
|
|
15704
|
+
if (typeof record.text === "string" && record.text.trim()) return record.text.trim();
|
|
15705
|
+
const chunks = [];
|
|
15706
|
+
const walk = (node) => {
|
|
15707
|
+
const rec = asRecord4(node);
|
|
15708
|
+
if (!rec) return;
|
|
15709
|
+
if (typeof rec.text === "string" && rec.text) chunks.push(rec.text);
|
|
15710
|
+
if (Array.isArray(rec.content)) rec.content.forEach(walk);
|
|
15711
|
+
};
|
|
15712
|
+
walk(record);
|
|
15713
|
+
return chunks.join("").trim();
|
|
15714
|
+
}
|
|
15715
|
+
function abletimeRestUrl(path, host, query) {
|
|
15716
|
+
const base = `${normalizeAbleTimeHost(host)}${ABLETIME_REST_PREFIX}`;
|
|
15717
|
+
const suffix = path.startsWith("/") ? path : `/${path}`;
|
|
15718
|
+
const url = new URL(`${base}${suffix}`);
|
|
15719
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
15720
|
+
if (value) url.searchParams.set(key, value);
|
|
15721
|
+
}
|
|
15722
|
+
return url.href;
|
|
15723
|
+
}
|
|
15724
|
+
function looksLikeAbleTimeUlid(id) {
|
|
15725
|
+
return ULID_RE.test(id.trim());
|
|
15726
|
+
}
|
|
15727
|
+
async function abletimeRestRequest(path, opts = {}) {
|
|
15728
|
+
const raw = opts.token ?? getAbleTimeAccessToken();
|
|
15729
|
+
const token = normalizeAbleTimeCredential(raw ?? "");
|
|
15730
|
+
if (!token) {
|
|
15731
|
+
throw new Error("AbleTime is not connected \u2014 paste an API key or personal access token in Account settings");
|
|
15732
|
+
}
|
|
15733
|
+
const host = opts.host ?? (opts.token ? DEFAULT_ABLETIME_HOST : getAbleTimeHost());
|
|
15734
|
+
const url = abletimeRestUrl(path, host, opts.query);
|
|
15735
|
+
const method = (opts.method ?? "GET").toUpperCase();
|
|
15736
|
+
const res = await httpFetch(url, {
|
|
15737
|
+
method,
|
|
15738
|
+
headers: {
|
|
15739
|
+
Accept: "application/json",
|
|
15740
|
+
Authorization: `Bearer ${token}`,
|
|
15741
|
+
...opts.body !== void 0 ? { "Content-Type": "application/json" } : {}
|
|
15742
|
+
},
|
|
15743
|
+
...opts.body !== void 0 ? { body: JSON.stringify(opts.body) } : {}
|
|
15744
|
+
});
|
|
15745
|
+
const text7 = await res.text().catch(() => "");
|
|
15746
|
+
if (!res.ok) {
|
|
15747
|
+
throw new Error(
|
|
15748
|
+
rewriteAbleTimeError(
|
|
15749
|
+
`AbleTime API error ${res.status}${text7 ? `: ${text7.slice(0, 240)}` : ""}`
|
|
15750
|
+
)
|
|
15751
|
+
);
|
|
15752
|
+
}
|
|
15753
|
+
if (!text7.trim()) return null;
|
|
15754
|
+
try {
|
|
15755
|
+
return JSON.parse(text7);
|
|
15756
|
+
} catch {
|
|
15757
|
+
throw new Error(`AbleTime API returned a non-JSON response: ${text7.slice(0, 200)}`);
|
|
15758
|
+
}
|
|
15759
|
+
}
|
|
15760
|
+
async function restPages(path, opts, maxPages = 20) {
|
|
15761
|
+
const items = [];
|
|
15762
|
+
let cursor;
|
|
15763
|
+
for (let i = 0; i < maxPages; i += 1) {
|
|
15764
|
+
const raw = await abletimeRestRequest(path, {
|
|
15765
|
+
...opts,
|
|
15766
|
+
query: { ...opts.query, ...cursor ? { cursor } : {} }
|
|
15767
|
+
});
|
|
15768
|
+
items.push(...asList(raw));
|
|
15769
|
+
const page = asRecord4(asRecord4(raw)?.page);
|
|
15770
|
+
const next = page ? asString(page.nextCursor) : "";
|
|
15771
|
+
if (!next) break;
|
|
15772
|
+
cursor = next;
|
|
15773
|
+
}
|
|
15774
|
+
return items;
|
|
15775
|
+
}
|
|
15776
|
+
function userDisplayName(raw) {
|
|
15777
|
+
const rec = asRecord4(raw);
|
|
15778
|
+
if (!rec) return null;
|
|
15779
|
+
const id = asString(rec.userId) || asString(rec.id);
|
|
15780
|
+
const name = [asString(rec.firstName), asString(rec.lastName)].filter(Boolean).join(" ") || asString(rec.username) || asString(rec.email) || asString(rec.name);
|
|
15781
|
+
if (!name) return null;
|
|
15782
|
+
return { id: id || void 0, name };
|
|
15783
|
+
}
|
|
15784
|
+
function restTaskToRaw(raw, users) {
|
|
15785
|
+
const rec = asRecord4(raw);
|
|
15786
|
+
if (!rec) return null;
|
|
15787
|
+
const id = asString(rec.timeflowTaskId) || asString(rec.taskId) || asString(rec.id);
|
|
15788
|
+
const identifier = asString(rec.taskRef) || asString(rec.reference) || id;
|
|
15789
|
+
const title = asString(rec.title);
|
|
15790
|
+
if (!id && !identifier && !title) return null;
|
|
15791
|
+
const assigned = asString(rec.assignedUserId);
|
|
15792
|
+
const assignee = assigned ? users.get(assigned) : void 0;
|
|
15793
|
+
const comments = asList(rec.comments).map((item) => {
|
|
15794
|
+
const comment = asRecord4(item) ?? {};
|
|
15795
|
+
const user = userDisplayName(comment);
|
|
15796
|
+
return {
|
|
15797
|
+
id: asString(comment.commentId) || asString(comment.id),
|
|
15798
|
+
body: textFromAbleTimeDoc(comment.content) || asString(comment.contentSanitized) || asString(comment.body),
|
|
15799
|
+
createdAt: asString(comment.createdAt) || asString(comment.lastUpdate),
|
|
15800
|
+
user: user?.name || asString(comment.username)
|
|
15801
|
+
};
|
|
15802
|
+
});
|
|
15803
|
+
return {
|
|
15804
|
+
id,
|
|
15805
|
+
taskId: id,
|
|
15806
|
+
timeflowTaskId: id,
|
|
15807
|
+
reference: identifier,
|
|
15808
|
+
taskRef: identifier,
|
|
15809
|
+
title: title || identifier,
|
|
15810
|
+
state: asString(rec.taskState) || asString(rec.state),
|
|
15811
|
+
description: textFromAbleTimeDoc(rec.description) || asString(rec.descriptionSanitized),
|
|
15812
|
+
projectId: asString(rec.projectId),
|
|
15813
|
+
categoryId: asString(rec.projectCategoryId) || asString(rec.categoryId),
|
|
15814
|
+
tags: rec.tags,
|
|
15815
|
+
comments,
|
|
15816
|
+
createdAt: asString(rec.dateCreated) || asString(rec.createdAt),
|
|
15817
|
+
updatedAt: asString(rec.lastUpdate) || asString(rec.updatedAt),
|
|
15818
|
+
...assignee ? { assignee } : {}
|
|
15819
|
+
};
|
|
15820
|
+
}
|
|
15821
|
+
function restProjectToRaw(raw) {
|
|
15822
|
+
const rec = asRecord4(raw);
|
|
15823
|
+
if (!rec) return null;
|
|
15824
|
+
const id = asString(rec.projectId) || asString(rec.id);
|
|
15825
|
+
const name = asString(rec.projectName) || asString(rec.name);
|
|
15826
|
+
if (!id && !name) return null;
|
|
15827
|
+
const categories = asList(rec.categories).map((item) => {
|
|
15828
|
+
const cat = asRecord4(item) ?? {};
|
|
15829
|
+
return {
|
|
15830
|
+
id: asString(cat.projectCategoryId) || asString(cat.categoryId) || asString(cat.id),
|
|
15831
|
+
name: asString(cat.categoryName) || asString(cat.name)
|
|
15832
|
+
};
|
|
15833
|
+
});
|
|
15834
|
+
return { id, name, projectId: id, projectName: name, categories };
|
|
15835
|
+
}
|
|
15836
|
+
async function listUsers(opts) {
|
|
15837
|
+
const users = /* @__PURE__ */ new Map();
|
|
15838
|
+
for (const item of await restPages("/users", opts).catch(() => [])) {
|
|
15839
|
+
const user = userDisplayName(item);
|
|
15840
|
+
const rec = asRecord4(item);
|
|
15841
|
+
const id = user?.id || (rec ? asString(rec.userId) : "");
|
|
15842
|
+
if (user && id) users.set(id, user);
|
|
15843
|
+
}
|
|
15844
|
+
return users;
|
|
15845
|
+
}
|
|
15846
|
+
async function listProjectsRaw(opts) {
|
|
15847
|
+
const items = await restPages("/projects", { ...opts, query: { expand: "categories" } });
|
|
15848
|
+
return items.map(restProjectToRaw).filter((item) => Boolean(item));
|
|
15849
|
+
}
|
|
15850
|
+
async function listTasksRaw(opts) {
|
|
15851
|
+
const [users, items] = await Promise.all([
|
|
15852
|
+
listUsers(opts),
|
|
15853
|
+
restPages("/tasks", {
|
|
15854
|
+
...opts,
|
|
15855
|
+
query: {
|
|
15856
|
+
...opts.projectId ? { projectId: opts.projectId } : {}
|
|
15857
|
+
}
|
|
15858
|
+
})
|
|
15859
|
+
]);
|
|
15860
|
+
let tasks = items.map((item) => restTaskToRaw(item, users)).filter((item) => Boolean(item));
|
|
15861
|
+
const needle = opts.query?.trim().toLowerCase();
|
|
15862
|
+
if (needle) {
|
|
15863
|
+
tasks = tasks.filter((task) => {
|
|
15864
|
+
const hay = [task.title, task.reference, task.taskRef, task.id].map((value) => String(value ?? "").toLowerCase()).join(" ");
|
|
15865
|
+
const tags = Array.isArray(task.tags) ? task.tags.join(" ").toLowerCase() : "";
|
|
15866
|
+
return hay.includes(needle) || tags.includes(needle);
|
|
15867
|
+
});
|
|
15868
|
+
}
|
|
15869
|
+
return tasks;
|
|
15870
|
+
}
|
|
15871
|
+
async function resolveTaskId(id, opts) {
|
|
15872
|
+
const wanted = id.trim();
|
|
15873
|
+
if (!wanted) throw new Error("AbleTime task id is required");
|
|
15874
|
+
if (looksLikeAbleTimeUlid(wanted)) return wanted;
|
|
15875
|
+
const tasks = await listTasksRaw(opts);
|
|
15876
|
+
const match = tasks.find((task) => {
|
|
15877
|
+
const ref = String(task.reference ?? task.taskRef ?? "").toLowerCase();
|
|
15878
|
+
const taskId = String(task.id ?? "").toLowerCase();
|
|
15879
|
+
return ref === wanted.toLowerCase() || taskId === wanted.toLowerCase();
|
|
15880
|
+
});
|
|
15881
|
+
if (!match?.id) throw new Error(`AbleTime task not found: ${id}`);
|
|
15882
|
+
return String(match.id);
|
|
15883
|
+
}
|
|
15884
|
+
async function getTaskRaw(id, opts) {
|
|
15885
|
+
const taskId = await resolveTaskId(id, opts);
|
|
15886
|
+
const [users, raw] = await Promise.all([
|
|
15887
|
+
listUsers(opts),
|
|
15888
|
+
abletimeRestRequest(`/tasks/${encodeURIComponent(taskId)}`, {
|
|
15889
|
+
...opts,
|
|
15890
|
+
query: { expand: "comments" }
|
|
15891
|
+
})
|
|
15892
|
+
]);
|
|
15893
|
+
const task = restTaskToRaw(raw, users);
|
|
15894
|
+
if (!task) throw new Error(`AbleTime task not found: ${id}`);
|
|
15895
|
+
return task;
|
|
15896
|
+
}
|
|
15897
|
+
function firstStringArg(args, keys) {
|
|
15898
|
+
for (const key of keys) {
|
|
15899
|
+
const value = asString(args[key]);
|
|
15900
|
+
if (value) return value;
|
|
15901
|
+
}
|
|
15902
|
+
return "";
|
|
15903
|
+
}
|
|
15904
|
+
async function callAbleTimeRestTool(name, args = {}, opts) {
|
|
15905
|
+
const token = normalizeAbleTimeCredential(opts?.token ?? getAbleTimeAccessToken() ?? "");
|
|
15906
|
+
if (ableTimeCredentialKind(token) !== "org") {
|
|
15907
|
+
throw new Error("AbleTime REST transport is for organization API keys (atk_\u2026)");
|
|
15908
|
+
}
|
|
15909
|
+
const requestOpts = { ...opts, token };
|
|
15910
|
+
switch (name) {
|
|
15911
|
+
case "orientation": {
|
|
15912
|
+
const [users, projects, tasks] = await Promise.all([
|
|
15913
|
+
listUsers(requestOpts),
|
|
15914
|
+
listProjectsRaw(requestOpts),
|
|
15915
|
+
listTasksRaw(requestOpts)
|
|
15916
|
+
]);
|
|
15917
|
+
const viewer = [...users.values()].find((user) => user.name) ?? [...users.values()][0] ?? { name: "AbleTime" };
|
|
15918
|
+
return { viewer, projects, tasks };
|
|
15919
|
+
}
|
|
15920
|
+
case "list_projects":
|
|
15921
|
+
return { data: await listProjectsRaw(requestOpts) };
|
|
15922
|
+
case "list_users":
|
|
15923
|
+
return { data: [...(await listUsers(requestOpts)).values()] };
|
|
15924
|
+
case "list_tasks":
|
|
15925
|
+
return {
|
|
15926
|
+
data: await listTasksRaw({
|
|
15927
|
+
...requestOpts,
|
|
15928
|
+
projectId: firstStringArg(args, ["project_id", "projectId", "project"])
|
|
15929
|
+
})
|
|
15930
|
+
};
|
|
15931
|
+
case "search_tasks":
|
|
15932
|
+
return {
|
|
15933
|
+
data: await listTasksRaw({
|
|
15934
|
+
...requestOpts,
|
|
15935
|
+
query: firstStringArg(args, ["query", "q"])
|
|
15936
|
+
})
|
|
15937
|
+
};
|
|
15938
|
+
case "get_task":
|
|
15939
|
+
return await getTaskRaw(firstStringArg(args, ["id", "task_id", "task"]), requestOpts);
|
|
15940
|
+
case "create_task": {
|
|
15941
|
+
const title = firstStringArg(args, ["title"]);
|
|
15942
|
+
if (!title) throw new Error("AbleTime task title is required");
|
|
15943
|
+
const projectId = firstStringArg(args, ["project_id", "projectId", "project"]);
|
|
15944
|
+
if (!projectId) throw new Error("AbleTime project is required");
|
|
15945
|
+
const projects = await listProjectsRaw(requestOpts);
|
|
15946
|
+
const project = projects.find(
|
|
15947
|
+
(item) => String(item.id).toLowerCase() === projectId.toLowerCase() || String(item.name).toLowerCase() === projectId.toLowerCase()
|
|
15948
|
+
) ?? projects[0];
|
|
15949
|
+
if (!project) throw new Error("AbleTime has no projects to create a task in");
|
|
15950
|
+
const categories = asList(project.categories);
|
|
15951
|
+
const wantedCategory = firstStringArg(args, ["category_id", "categoryId", "category"]);
|
|
15952
|
+
const category = categories.map((item) => asRecord4(item)).find((item) => {
|
|
15953
|
+
if (!item || !wantedCategory) return false;
|
|
15954
|
+
return asString(item.id).toLowerCase() === wantedCategory.toLowerCase() || asString(item.name).toLowerCase() === wantedCategory.toLowerCase();
|
|
15955
|
+
}) ?? asRecord4(categories[0]);
|
|
15956
|
+
const created = await abletimeRestRequest("/tasks", {
|
|
15957
|
+
...requestOpts,
|
|
15958
|
+
method: "POST",
|
|
15959
|
+
body: {
|
|
15960
|
+
projectId: project.id,
|
|
15961
|
+
projectCategoryId: category ? asString(category.id) : void 0,
|
|
15962
|
+
title
|
|
15963
|
+
}
|
|
15964
|
+
});
|
|
15965
|
+
const state = firstStringArg(args, ["state"]);
|
|
15966
|
+
const createdId = asString(asRecord4(created)?.timeflowTaskId);
|
|
15967
|
+
const description = asString(args.description);
|
|
15968
|
+
if (createdId && description) {
|
|
15969
|
+
await abletimeRestRequest(`/tasks/${encodeURIComponent(createdId)}`, {
|
|
15970
|
+
...requestOpts,
|
|
15971
|
+
method: "PATCH",
|
|
15972
|
+
body: { description: ableTimeDocFromText(description) }
|
|
15973
|
+
}).catch(() => void 0);
|
|
15974
|
+
}
|
|
15975
|
+
if (createdId && state && state !== "todo") {
|
|
15976
|
+
await abletimeRestRequest(`/tasks/${encodeURIComponent(createdId)}/state`, {
|
|
15977
|
+
...requestOpts,
|
|
15978
|
+
method: "POST",
|
|
15979
|
+
body: { taskState: state }
|
|
15980
|
+
}).catch(() => void 0);
|
|
15981
|
+
}
|
|
15982
|
+
return createdId ? await getTaskRaw(createdId, requestOpts) : restTaskToRaw(created, await listUsers(requestOpts));
|
|
15983
|
+
}
|
|
15984
|
+
case "update_task": {
|
|
15985
|
+
const id = await resolveTaskId(firstStringArg(args, ["id", "task_id", "task"]), requestOpts);
|
|
15986
|
+
const patch = {};
|
|
15987
|
+
const title = firstStringArg(args, ["title"]);
|
|
15988
|
+
if (title) patch.title = title;
|
|
15989
|
+
if (args.description !== void 0) {
|
|
15990
|
+
patch.description = args.description == null || args.description === "" ? null : ableTimeDocFromText(String(args.description));
|
|
15991
|
+
}
|
|
15992
|
+
if (args.tags !== void 0 || args.labels !== void 0) {
|
|
15993
|
+
patch.tags = args.tags ?? args.labels ?? args.tag_names;
|
|
15994
|
+
}
|
|
15995
|
+
if (Object.keys(patch).length > 0) {
|
|
15996
|
+
await abletimeRestRequest(`/tasks/${encodeURIComponent(id)}`, {
|
|
15997
|
+
...requestOpts,
|
|
15998
|
+
method: "PATCH",
|
|
15999
|
+
body: patch
|
|
16000
|
+
});
|
|
16001
|
+
}
|
|
16002
|
+
return await getTaskRaw(id, requestOpts);
|
|
16003
|
+
}
|
|
16004
|
+
case "set_task_state": {
|
|
16005
|
+
const id = await resolveTaskId(firstStringArg(args, ["id", "task_id", "task"]), requestOpts);
|
|
16006
|
+
const state = firstStringArg(args, ["state", "taskState"]);
|
|
16007
|
+
if (!state) throw new Error("AbleTime task state is required");
|
|
16008
|
+
await abletimeRestRequest(`/tasks/${encodeURIComponent(id)}/state`, {
|
|
16009
|
+
...requestOpts,
|
|
16010
|
+
method: "POST",
|
|
16011
|
+
body: { taskState: state }
|
|
16012
|
+
});
|
|
16013
|
+
return await getTaskRaw(id, requestOpts);
|
|
16014
|
+
}
|
|
16015
|
+
case "create_comment": {
|
|
16016
|
+
const id = await resolveTaskId(firstStringArg(args, ["id", "task_id", "task"]), requestOpts);
|
|
16017
|
+
const body = firstStringArg(args, ["body", "comment", "text"]);
|
|
16018
|
+
if (!body) throw new Error("AbleTime comment body is required");
|
|
16019
|
+
const created = await abletimeRestRequest(`/tasks/${encodeURIComponent(id)}/comments`, {
|
|
16020
|
+
...requestOpts,
|
|
16021
|
+
method: "POST",
|
|
16022
|
+
body: { content: ableTimeDocFromText(body) }
|
|
16023
|
+
});
|
|
16024
|
+
const rec = asRecord4(created) ?? {};
|
|
16025
|
+
return {
|
|
16026
|
+
id: asString(rec.commentId) || void 0,
|
|
16027
|
+
body: textFromAbleTimeDoc(rec.content) || body
|
|
16028
|
+
};
|
|
16029
|
+
}
|
|
16030
|
+
case "set_task_dependency":
|
|
16031
|
+
throw new Error(
|
|
16032
|
+
"AbleTime organization API keys cannot set task dependencies on REST yet \u2014 use a personal access token (apt_\u2026) for MCP project-management tools."
|
|
16033
|
+
);
|
|
16034
|
+
default:
|
|
16035
|
+
throw new Error(`AbleTime organization API keys do not support ${name} on REST`);
|
|
16036
|
+
}
|
|
16037
|
+
}
|
|
16038
|
+
var ABLETIME_REST_PREFIX, ULID_RE;
|
|
16039
|
+
var init_abletime_rest = __esm({
|
|
16040
|
+
"src/integrations/abletime-rest.ts"() {
|
|
16041
|
+
"use strict";
|
|
16042
|
+
init_fetch();
|
|
16043
|
+
init_app_settings();
|
|
16044
|
+
init_abletime_mcp();
|
|
16045
|
+
ABLETIME_REST_PREFIX = "/api/public/v2";
|
|
16046
|
+
ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/i;
|
|
16047
|
+
}
|
|
16048
|
+
});
|
|
16049
|
+
|
|
15376
16050
|
// src/integrations/abletime-mcp.ts
|
|
15377
16051
|
function abletimeToolUsesPm(name) {
|
|
15378
16052
|
return ABLETIME_PM_TOOL_NAMES.includes(name);
|
|
@@ -15386,22 +16060,49 @@ function abletimeMcpUrl(host, opts) {
|
|
|
15386
16060
|
const path = opts?.pm ? ABLETIME_MCP_PM_PATH : ABLETIME_MCP_PATH;
|
|
15387
16061
|
return `${normalizeAbleTimeHost(host)}${path}`;
|
|
15388
16062
|
}
|
|
16063
|
+
function normalizeAbleTimeCredential(raw) {
|
|
16064
|
+
return raw.trim().replace(/^bearer\s+/i, "").trim();
|
|
16065
|
+
}
|
|
16066
|
+
function ableTimeCredentialKind(raw) {
|
|
16067
|
+
const token = normalizeAbleTimeCredential(raw).toLowerCase();
|
|
16068
|
+
if (token.startsWith(ABLETIME_PAT_PREFIX)) return "pat";
|
|
16069
|
+
if (token.startsWith(ABLETIME_ORG_KEY_PREFIX)) return "org";
|
|
16070
|
+
return "unknown";
|
|
16071
|
+
}
|
|
16072
|
+
function assertAbleTimeMcpCredential(raw) {
|
|
16073
|
+
const token = normalizeAbleTimeCredential(raw);
|
|
16074
|
+
if (!token) {
|
|
16075
|
+
throw new Error("AbleTime personal access token is required");
|
|
16076
|
+
}
|
|
16077
|
+
if (ableTimeCredentialKind(token) === "org") {
|
|
16078
|
+
throw new Error(ORG_KEY_HINT);
|
|
16079
|
+
}
|
|
16080
|
+
return token;
|
|
16081
|
+
}
|
|
16082
|
+
function assertAbleTimeCredential(raw) {
|
|
16083
|
+
const token = normalizeAbleTimeCredential(raw);
|
|
16084
|
+
if (!token) {
|
|
16085
|
+
throw new Error("AbleTime API key or personal access token is required");
|
|
16086
|
+
}
|
|
16087
|
+
return token;
|
|
16088
|
+
}
|
|
15389
16089
|
function rewriteAbleTimeError(message) {
|
|
15390
|
-
if (
|
|
16090
|
+
if (REWRITE_APPLIED.test(message)) return message;
|
|
16091
|
+
if (/INTEGRATION_AGENT_ACCESS_DISABLED/i.test(message)) {
|
|
15391
16092
|
return `${message} \u2014 Enable Agent access (MCP) in AbleTime Settings \u2192 Integrations \u2192 API Keys.`;
|
|
15392
16093
|
}
|
|
15393
|
-
if (/INTEGRATION_PAT_REQUIRED
|
|
15394
|
-
return `${message} \u2014
|
|
16094
|
+
if (/INTEGRATION_PAT_REQUIRED/i.test(message)) {
|
|
16095
|
+
return `${message} \u2014 ${ORG_KEY_HINT}`;
|
|
15395
16096
|
}
|
|
15396
16097
|
if (/INTEGRATION_PLAN_REQUIRED/i.test(message)) {
|
|
15397
16098
|
return `${message} \u2014 AbleTime plan does not include API access.`;
|
|
15398
16099
|
}
|
|
15399
16100
|
if (/INTEGRATION_KEY_INVALID|INTEGRATION_KEY_MISSING|401|unauthorized|invalid token/i.test(message)) {
|
|
15400
|
-
return `${message} \u2014
|
|
16101
|
+
return `${message} \u2014 ${REJECTED_CREDENTIAL_HINT}`;
|
|
15401
16102
|
}
|
|
15402
16103
|
return message;
|
|
15403
16104
|
}
|
|
15404
|
-
function
|
|
16105
|
+
function asRecord5(value) {
|
|
15405
16106
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
15406
16107
|
}
|
|
15407
16108
|
function parseSseJson(body) {
|
|
@@ -15423,12 +16124,12 @@ function parseSseJson(body) {
|
|
|
15423
16124
|
}
|
|
15424
16125
|
}
|
|
15425
16126
|
function unwrapToolResult(result) {
|
|
15426
|
-
const record =
|
|
16127
|
+
const record = asRecord5(result);
|
|
15427
16128
|
if (!record) return result;
|
|
15428
16129
|
const content = record.content;
|
|
15429
16130
|
if (Array.isArray(content)) {
|
|
15430
16131
|
const texts = content.map((part) => {
|
|
15431
|
-
const item =
|
|
16132
|
+
const item = asRecord5(part);
|
|
15432
16133
|
if (!item) return "";
|
|
15433
16134
|
return typeof item.text === "string" ? item.text : "";
|
|
15434
16135
|
}).filter(Boolean);
|
|
@@ -15450,17 +16151,18 @@ function unwrapToolResult(result) {
|
|
|
15450
16151
|
return result;
|
|
15451
16152
|
}
|
|
15452
16153
|
function textsFromUnknown(value) {
|
|
15453
|
-
const record =
|
|
16154
|
+
const record = asRecord5(value);
|
|
15454
16155
|
if (!record) return "";
|
|
15455
16156
|
if (typeof record.message === "string") return record.message;
|
|
15456
16157
|
if (typeof record.error === "string") return record.error;
|
|
15457
16158
|
return "";
|
|
15458
16159
|
}
|
|
15459
16160
|
async function abletimeMcpRequest(method, params, opts) {
|
|
15460
|
-
const
|
|
15461
|
-
if (!
|
|
16161
|
+
const rawToken = opts?.token ?? await Promise.resolve().then(() => (init_abletime_oauth(), abletime_oauth_exports)).then((m) => m.ensureAbleTimeAccessToken());
|
|
16162
|
+
if (!rawToken?.trim()) {
|
|
15462
16163
|
throw new Error("AbleTime is not connected \u2014 paste a personal access token in Account settings");
|
|
15463
16164
|
}
|
|
16165
|
+
const token = assertAbleTimeMcpCredential(rawToken);
|
|
15464
16166
|
const url = abletimeMcpUrl(
|
|
15465
16167
|
opts?.host ?? (opts?.token ? DEFAULT_ABLETIME_HOST : getAbleTimeHost()),
|
|
15466
16168
|
{ pm: opts?.pm }
|
|
@@ -15496,7 +16198,7 @@ async function abletimeMcpRequest(method, params, opts) {
|
|
|
15496
16198
|
return parseSseJson(body);
|
|
15497
16199
|
}
|
|
15498
16200
|
})();
|
|
15499
|
-
const rpc =
|
|
16201
|
+
const rpc = asRecord5(parsed);
|
|
15500
16202
|
if (rpc?.error) {
|
|
15501
16203
|
const detail = typeof rpc.error.message === "string" && rpc.error.message.trim() ? rpc.error.message : `AbleTime MCP error ${rpc.error.code ?? ""}`.trim();
|
|
15502
16204
|
throw new Error(rewriteAbleTimeError(detail));
|
|
@@ -15527,6 +16229,11 @@ async function callAbleTimeTool(name, args = {}, opts) {
|
|
|
15527
16229
|
if (!opts?.token && !isAbleTimeConnected()) {
|
|
15528
16230
|
throw new Error("AbleTime is not connected \u2014 paste a personal access token in Account settings");
|
|
15529
16231
|
}
|
|
16232
|
+
const rawToken = opts?.token ?? await Promise.resolve().then(() => (init_abletime_oauth(), abletime_oauth_exports)).then((m) => m.ensureAbleTimeAccessToken());
|
|
16233
|
+
if (rawToken && ableTimeCredentialKind(rawToken) === "org") {
|
|
16234
|
+
const { callAbleTimeRestTool: callAbleTimeRestTool2 } = await Promise.resolve().then(() => (init_abletime_rest(), abletime_rest_exports));
|
|
16235
|
+
return callAbleTimeRestTool2(name, args, { ...opts, token: rawToken });
|
|
16236
|
+
}
|
|
15530
16237
|
const requestOpts = { ...opts, pm: opts?.pm ?? abletimeToolUsesPm(name) };
|
|
15531
16238
|
const call = () => abletimeMcpRequest("tools/call", { name, arguments: args }, requestOpts).then((result) => {
|
|
15532
16239
|
return unwrapToolResult(result);
|
|
@@ -15542,7 +16249,7 @@ async function callAbleTimeTool(name, args = {}, opts) {
|
|
|
15542
16249
|
throw err instanceof Error ? new Error(rewriteAbleTimeError(err.message)) : err;
|
|
15543
16250
|
}
|
|
15544
16251
|
}
|
|
15545
|
-
var DEFAULT_ABLETIME_HOST, ABLETIME_MCP_PATH, ABLETIME_MCP_PM_PATH, MCP_PROTOCOL_VERSION, ABLETIME_PM_TOOL_NAMES;
|
|
16252
|
+
var DEFAULT_ABLETIME_HOST, ABLETIME_MCP_PATH, ABLETIME_MCP_PM_PATH, MCP_PROTOCOL_VERSION, ABLETIME_PM_TOOL_NAMES, ABLETIME_PAT_PREFIX, ABLETIME_ORG_KEY_PREFIX, ORG_KEY_HINT, REJECTED_CREDENTIAL_HINT, REWRITE_APPLIED;
|
|
15546
16253
|
var init_abletime_mcp = __esm({
|
|
15547
16254
|
"src/integrations/abletime-mcp.ts"() {
|
|
15548
16255
|
"use strict";
|
|
@@ -15564,6 +16271,11 @@ var init_abletime_mcp = __esm({
|
|
|
15564
16271
|
"archive_task",
|
|
15565
16272
|
"delete_task"
|
|
15566
16273
|
];
|
|
16274
|
+
ABLETIME_PAT_PREFIX = "apt_";
|
|
16275
|
+
ABLETIME_ORG_KEY_PREFIX = "atk_";
|
|
16276
|
+
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.";
|
|
16277
|
+
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.";
|
|
16278
|
+
REWRITE_APPLIED = /— Enable Agent access \(MCP\)|— AbleTime MCP needs a personal access token|— AbleTime plan does not include API access|— AbleTime rejected this credential/;
|
|
15567
16279
|
}
|
|
15568
16280
|
});
|
|
15569
16281
|
|
|
@@ -15587,16 +16299,16 @@ __export(abletime_exports, {
|
|
|
15587
16299
|
updateAbleTimeTask: () => updateAbleTimeTask,
|
|
15588
16300
|
verifyAbleTimeConnection: () => verifyAbleTimeConnection
|
|
15589
16301
|
});
|
|
15590
|
-
function
|
|
16302
|
+
function asRecord6(value) {
|
|
15591
16303
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
15592
16304
|
}
|
|
15593
|
-
function
|
|
16305
|
+
function asString2(value) {
|
|
15594
16306
|
return typeof value === "string" ? value.trim() : "";
|
|
15595
16307
|
}
|
|
15596
16308
|
function firstString(record, keys) {
|
|
15597
16309
|
if (!record) return "";
|
|
15598
16310
|
for (const key of keys) {
|
|
15599
|
-
const value =
|
|
16311
|
+
const value = asString2(record[key]);
|
|
15600
16312
|
if (value) return value;
|
|
15601
16313
|
}
|
|
15602
16314
|
return "";
|
|
@@ -15604,14 +16316,14 @@ function firstString(record, keys) {
|
|
|
15604
16316
|
function firstRecord(record, keys) {
|
|
15605
16317
|
if (!record) return null;
|
|
15606
16318
|
for (const key of keys) {
|
|
15607
|
-
const value =
|
|
16319
|
+
const value = asRecord6(record[key]);
|
|
15608
16320
|
if (value) return value;
|
|
15609
16321
|
}
|
|
15610
16322
|
return null;
|
|
15611
16323
|
}
|
|
15612
|
-
function
|
|
16324
|
+
function asList2(value) {
|
|
15613
16325
|
if (Array.isArray(value)) return value;
|
|
15614
|
-
const record =
|
|
16326
|
+
const record = asRecord6(value);
|
|
15615
16327
|
if (!record) return [];
|
|
15616
16328
|
for (const key of ["data", "tasks", "projects", "items", "results", "nodes"]) {
|
|
15617
16329
|
if (Array.isArray(record[key])) return record[key];
|
|
@@ -15623,21 +16335,21 @@ function labelsOf(record) {
|
|
|
15623
16335
|
if (!Array.isArray(raw)) return [];
|
|
15624
16336
|
return raw.map((item) => {
|
|
15625
16337
|
if (typeof item === "string") return item.trim();
|
|
15626
|
-
const rec =
|
|
16338
|
+
const rec = asRecord6(item);
|
|
15627
16339
|
return rec ? firstString(rec, ["name", "title", "label"]) : "";
|
|
15628
16340
|
}).filter(Boolean);
|
|
15629
16341
|
}
|
|
15630
16342
|
function commentsOf(record) {
|
|
15631
16343
|
const raw = record.comments ?? record.notes ?? record.discussion;
|
|
15632
16344
|
const out = [];
|
|
15633
|
-
for (const item of
|
|
15634
|
-
const rec =
|
|
16345
|
+
for (const item of asList2(raw)) {
|
|
16346
|
+
const rec = asRecord6(item);
|
|
15635
16347
|
if (!rec) continue;
|
|
15636
|
-
const body = firstString(rec, ["body", "text", "comment", "
|
|
16348
|
+
const body = firstString(rec, ["body", "text", "comment", "message", "contentSanitized"]) || textFromAbleTimeDoc(rec.content);
|
|
15637
16349
|
if (!body) continue;
|
|
15638
|
-
const user = firstString(
|
|
16350
|
+
const user = firstString(asRecord6(rec.user) ?? asRecord6(rec.author), ["name", "display_name", "username"]) || firstString(rec, ["user_name", "author", "username"]);
|
|
15639
16351
|
const comment = { body };
|
|
15640
|
-
const id = firstString(rec, ["id", "comment_id"]);
|
|
16352
|
+
const id = firstString(rec, ["id", "comment_id", "commentId"]);
|
|
15641
16353
|
if (id) comment.id = id;
|
|
15642
16354
|
const url = firstString(rec, ["url", "permalink"]);
|
|
15643
16355
|
if (url) comment.url = url;
|
|
@@ -15661,11 +16373,11 @@ function taskUrl(task, host) {
|
|
|
15661
16373
|
return `${base}/tasks/${slug}`;
|
|
15662
16374
|
}
|
|
15663
16375
|
function mapAbleTimeTask(raw, host) {
|
|
15664
|
-
const record =
|
|
16376
|
+
const record = asRecord6(raw);
|
|
15665
16377
|
if (!record) return null;
|
|
15666
16378
|
const nested = firstRecord(record, ["task", "data"]) ?? record;
|
|
15667
|
-
const id = firstString(nested, ["id", "task_id", "taskId"]);
|
|
15668
|
-
const identifier = firstString(nested, ["reference", "ref", "identifier", "key", "code", "number"]) || id;
|
|
16379
|
+
const id = firstString(nested, ["id", "task_id", "taskId", "timeflowTaskId"]);
|
|
16380
|
+
const identifier = firstString(nested, ["reference", "ref", "identifier", "key", "code", "number", "taskRef"]) || id;
|
|
15669
16381
|
const title = firstString(nested, ["title", "name"]);
|
|
15670
16382
|
if (!id && !identifier && !title) return null;
|
|
15671
16383
|
const resolvedId = id || identifier;
|
|
@@ -15676,21 +16388,22 @@ function mapAbleTimeTask(raw, host) {
|
|
|
15676
16388
|
identifier: resolvedIdentifier,
|
|
15677
16389
|
title: title || resolvedIdentifier,
|
|
15678
16390
|
url,
|
|
15679
|
-
description: firstString(nested, ["description", "body", "details"]) || void 0,
|
|
15680
|
-
state: firstString(nested, ["state", "board_state", "boardState", "status"]) || void 0,
|
|
15681
|
-
projectId: firstString(nested, ["project_id", "projectId"]) || firstString(
|
|
15682
|
-
categoryId: firstString(nested, ["category_id", "categoryId"]) || firstString(
|
|
16391
|
+
description: firstString(nested, ["description", "body", "details", "descriptionSanitized"]) || textFromAbleTimeDoc(nested.description) || void 0,
|
|
16392
|
+
state: firstString(nested, ["state", "board_state", "boardState", "status", "taskState"]) || void 0,
|
|
16393
|
+
projectId: firstString(nested, ["project_id", "projectId"]) || firstString(asRecord6(nested.project), ["id"]) || void 0,
|
|
16394
|
+
categoryId: firstString(nested, ["category_id", "categoryId", "projectCategoryId"]) || firstString(asRecord6(nested.category), ["id"]) || void 0,
|
|
15683
16395
|
assignee: assigneeOf(nested),
|
|
15684
16396
|
labels: labelsOf(nested),
|
|
15685
16397
|
comments: commentsOf(nested),
|
|
15686
|
-
createdAt: firstString(nested, ["created_at", "createdAt", "created"]) || void 0,
|
|
16398
|
+
createdAt: firstString(nested, ["created_at", "createdAt", "created", "dateCreated"]) || void 0,
|
|
15687
16399
|
updatedAt: firstString(nested, [
|
|
15688
16400
|
"updated_at",
|
|
15689
16401
|
"updatedAt",
|
|
15690
16402
|
"modified_at",
|
|
15691
16403
|
"modifiedAt",
|
|
15692
16404
|
"last_activity_at",
|
|
15693
|
-
"lastActivityAt"
|
|
16405
|
+
"lastActivityAt",
|
|
16406
|
+
"lastUpdate"
|
|
15694
16407
|
]) || void 0
|
|
15695
16408
|
};
|
|
15696
16409
|
}
|
|
@@ -15721,16 +16434,16 @@ function issueAttachmentForAbleTimeTask(task) {
|
|
|
15721
16434
|
};
|
|
15722
16435
|
}
|
|
15723
16436
|
function mapProject(raw) {
|
|
15724
|
-
const record =
|
|
16437
|
+
const record = asRecord6(raw);
|
|
15725
16438
|
if (!record) return null;
|
|
15726
|
-
const id = firstString(record, ["id", "project_id"]);
|
|
15727
|
-
const name = firstString(record, ["name", "title"]);
|
|
16439
|
+
const id = firstString(record, ["id", "project_id", "projectId"]);
|
|
16440
|
+
const name = firstString(record, ["name", "title", "projectName"]);
|
|
15728
16441
|
if (!id && !name) return null;
|
|
15729
|
-
const categories =
|
|
15730
|
-
const rec =
|
|
16442
|
+
const categories = asList2(record.categories ?? record.category).map((item) => {
|
|
16443
|
+
const rec = asRecord6(item);
|
|
15731
16444
|
if (!rec) return null;
|
|
15732
|
-
const categoryId = firstString(rec, ["id", "category_id"]);
|
|
15733
|
-
const categoryName = firstString(rec, ["name", "title"]);
|
|
16445
|
+
const categoryId = firstString(rec, ["id", "category_id", "projectCategoryId", "categoryId"]);
|
|
16446
|
+
const categoryName = firstString(rec, ["name", "title", "categoryName"]);
|
|
15734
16447
|
if (!categoryId && !categoryName) return null;
|
|
15735
16448
|
return { id: categoryId || categoryName, name: categoryName || categoryId };
|
|
15736
16449
|
}).filter((item) => Boolean(item));
|
|
@@ -15742,21 +16455,21 @@ function isOpenTask(task) {
|
|
|
15742
16455
|
}
|
|
15743
16456
|
async function getAbleTimeOrientation(opts) {
|
|
15744
16457
|
const raw = await callAbleTimeTool("orientation", {}, opts);
|
|
15745
|
-
const record =
|
|
16458
|
+
const record = asRecord6(raw);
|
|
15746
16459
|
const viewerRec = firstRecord(record, ["viewer", "user", "actor", "me", "person"]) ?? record;
|
|
15747
16460
|
const viewer = {
|
|
15748
16461
|
id: firstString(viewerRec, ["id", "user_id"]) || void 0,
|
|
15749
16462
|
name: firstString(viewerRec, ["name", "display_name", "full_name"]) || firstString(record, ["name"]) || void 0
|
|
15750
16463
|
};
|
|
15751
|
-
const projects =
|
|
16464
|
+
const projects = asList2(
|
|
15752
16465
|
record?.projects ?? record?.current_projects ?? record?.currentProjects
|
|
15753
16466
|
).map(mapProject).filter((item) => Boolean(item));
|
|
15754
|
-
const tasks =
|
|
16467
|
+
const tasks = asList2(record?.tasks ?? record?.active_tasks ?? record?.activeTasks).map((item) => mapAbleTimeTask(item, opts?.host)).filter((item) => Boolean(item));
|
|
15755
16468
|
return { viewer, projects, tasks, raw };
|
|
15756
16469
|
}
|
|
15757
16470
|
async function listAbleTimeProjects(opts) {
|
|
15758
16471
|
const raw = await callAbleTimeTool("list_projects", { include_categories: true }, opts);
|
|
15759
|
-
return
|
|
16472
|
+
return asList2(raw).map(mapProject).filter((item) => Boolean(item));
|
|
15760
16473
|
}
|
|
15761
16474
|
async function listAbleTimeTasks(opts) {
|
|
15762
16475
|
const args = {};
|
|
@@ -15766,14 +16479,14 @@ async function listAbleTimeTasks(opts) {
|
|
|
15766
16479
|
}
|
|
15767
16480
|
if (opts?.query) args.query = opts.query;
|
|
15768
16481
|
const raw = await callAbleTimeTool("list_tasks", args, opts);
|
|
15769
|
-
const tasks =
|
|
16482
|
+
const tasks = asList2(raw).map((item) => mapAbleTimeTask(item, opts?.host)).filter((item) => Boolean(item));
|
|
15770
16483
|
return opts?.includeClosed ? tasks : tasks.filter(isOpenTask);
|
|
15771
16484
|
}
|
|
15772
16485
|
async function searchAbleTimeTasks(query, opts) {
|
|
15773
16486
|
const q = query.trim();
|
|
15774
16487
|
if (!q) return [];
|
|
15775
16488
|
const raw = await callAbleTimeTool("search_tasks", { query: q, q }, opts);
|
|
15776
|
-
return
|
|
16489
|
+
return asList2(raw).map((item) => mapAbleTimeTask(item, opts?.host)).filter((item) => Boolean(item)).filter(isOpenTask);
|
|
15777
16490
|
}
|
|
15778
16491
|
async function getAbleTimeTask(id, opts) {
|
|
15779
16492
|
const raw = await callAbleTimeTool("get_task", { id, task_id: id }, opts);
|
|
@@ -15821,7 +16534,7 @@ async function commentAbleTimeTask(input, opts) {
|
|
|
15821
16534
|
{ id, task_id: id, task: id, body, comment: body, text: body },
|
|
15822
16535
|
opts
|
|
15823
16536
|
);
|
|
15824
|
-
const rec =
|
|
16537
|
+
const rec = asRecord6(raw);
|
|
15825
16538
|
return {
|
|
15826
16539
|
id: rec ? firstString(rec, ["id", "comment_id"]) || void 0 : void 0,
|
|
15827
16540
|
body: rec ? firstString(rec, ["body", "text", "comment"]) || body : body
|
|
@@ -16011,8 +16724,7 @@ async function listAbleTimeAssignedIssues(opts) {
|
|
|
16011
16724
|
};
|
|
16012
16725
|
}
|
|
16013
16726
|
async function verifyAbleTimeConnection(input) {
|
|
16014
|
-
const token = input.token
|
|
16015
|
-
if (!token) throw new Error("AbleTime personal access token is required");
|
|
16727
|
+
const token = assertAbleTimeCredential(input.token);
|
|
16016
16728
|
const host = input.host?.trim() || void 0;
|
|
16017
16729
|
const orientation = await getAbleTimeOrientation({ token, host });
|
|
16018
16730
|
saveAbleTimeConnection({
|
|
@@ -16028,6 +16740,7 @@ var init_abletime = __esm({
|
|
|
16028
16740
|
"use strict";
|
|
16029
16741
|
init_app_settings();
|
|
16030
16742
|
init_abletime_mcp();
|
|
16743
|
+
init_abletime_rest();
|
|
16031
16744
|
CLOSED_STATES = /* @__PURE__ */ new Set([
|
|
16032
16745
|
"done",
|
|
16033
16746
|
"completed",
|
|
@@ -16599,12 +17312,19 @@ function buildQuotaHandoffAttachment(from, limitText, fallbackAgent) {
|
|
|
16599
17312
|
...instructions
|
|
16600
17313
|
].join("\n");
|
|
16601
17314
|
return {
|
|
16602
|
-
id: (0,
|
|
17315
|
+
id: (0, import_node_crypto10.randomUUID)(),
|
|
16603
17316
|
name: orch ? "Orchestration quota handoff.md" : "Quota handoff.md",
|
|
16604
17317
|
kind: "transcript",
|
|
16605
17318
|
content: body
|
|
16606
17319
|
};
|
|
16607
17320
|
}
|
|
17321
|
+
async function isolateQuotaFailover(run2) {
|
|
17322
|
+
try {
|
|
17323
|
+
return await run2();
|
|
17324
|
+
} catch {
|
|
17325
|
+
return true;
|
|
17326
|
+
}
|
|
17327
|
+
}
|
|
16608
17328
|
function createQuotaFailoverChat(from, fallbackAgent, limitText) {
|
|
16609
17329
|
const handoff = buildQuotaHandoffAttachment(from, limitText, fallbackAgent);
|
|
16610
17330
|
const tab = createChatTab({
|
|
@@ -16623,11 +17343,11 @@ function createQuotaFailoverChat(from, fallbackAgent, limitText) {
|
|
|
16623
17343
|
}
|
|
16624
17344
|
return updateThread(tab.id, patch);
|
|
16625
17345
|
}
|
|
16626
|
-
var
|
|
17346
|
+
var import_node_crypto10, QUOTA_CONTINUE_PROMPT, QUOTA_RESUME_PROMPT;
|
|
16627
17347
|
var init_quota_failover = __esm({
|
|
16628
17348
|
"src/orchestrator/quota-failover.ts"() {
|
|
16629
17349
|
"use strict";
|
|
16630
|
-
|
|
17350
|
+
import_node_crypto10 = require("crypto");
|
|
16631
17351
|
init_session_quota();
|
|
16632
17352
|
init_app_settings();
|
|
16633
17353
|
init_global_workspace();
|
|
@@ -18938,7 +19658,7 @@ var init_optional_services = __esm({
|
|
|
18938
19658
|
});
|
|
18939
19659
|
|
|
18940
19660
|
// src/plan/plan-present.ts
|
|
18941
|
-
function
|
|
19661
|
+
function asRecord7(v) {
|
|
18942
19662
|
return v != null && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
18943
19663
|
}
|
|
18944
19664
|
function isPresentPlanToolName(name) {
|
|
@@ -18950,7 +19670,7 @@ function extractPresentedPlan(parts) {
|
|
|
18950
19670
|
for (let i = parts.length - 1; i >= 0; i--) {
|
|
18951
19671
|
const p = parts[i];
|
|
18952
19672
|
if (p.type !== "tool" || !isPresentPlanToolName(p.name)) continue;
|
|
18953
|
-
const input =
|
|
19673
|
+
const input = asRecord7(p.input) ?? {};
|
|
18954
19674
|
const content = typeof input.content === "string" ? input.content : typeof input.plan === "string" ? input.plan : typeof input.markdown === "string" ? input.markdown : "";
|
|
18955
19675
|
if (!content.trim()) continue;
|
|
18956
19676
|
const title = typeof input.title === "string" && input.title.trim() ? input.title.trim() : "Plan";
|
|
@@ -19385,7 +20105,7 @@ function buildTask(input, now) {
|
|
|
19385
20105
|
const ts = nowIso2(now);
|
|
19386
20106
|
const agent = input.agent ? coerceOrchestratorAgent(input.agent) : null;
|
|
19387
20107
|
return {
|
|
19388
|
-
id: (0,
|
|
20108
|
+
id: (0, import_node_crypto11.randomUUID)(),
|
|
19389
20109
|
name: input.name?.trim() || defaultScheduleName(prompt),
|
|
19390
20110
|
prompt,
|
|
19391
20111
|
enabled: input.enabled !== false,
|
|
@@ -19472,11 +20192,11 @@ function recordScheduleRun(id, result) {
|
|
|
19472
20192
|
writeAll2(rows);
|
|
19473
20193
|
return next;
|
|
19474
20194
|
}
|
|
19475
|
-
var
|
|
20195
|
+
var import_node_crypto11, import_node_fs52, import_node_path49, import_croner, DURATION_RE, UNIT_MS;
|
|
19476
20196
|
var init_schedules = __esm({
|
|
19477
20197
|
"src/store/schedules.ts"() {
|
|
19478
20198
|
"use strict";
|
|
19479
|
-
|
|
20199
|
+
import_node_crypto11 = require("crypto");
|
|
19480
20200
|
import_node_fs52 = require("fs");
|
|
19481
20201
|
import_node_path49 = require("path");
|
|
19482
20202
|
import_croner = require("croner");
|
|
@@ -20165,51 +20885,62 @@ var init_orchestrator = __esm({
|
|
|
20165
20885
|
if (plan.action === "wait_reset" && plan.resumeAt) {
|
|
20166
20886
|
this.haltDrain.add(threadId);
|
|
20167
20887
|
this.scheduleQuotaResume(threadId, plan.resumeAt);
|
|
20168
|
-
|
|
20169
|
-
|
|
20170
|
-
|
|
20171
|
-
|
|
20172
|
-
|
|
20173
|
-
|
|
20174
|
-
|
|
20175
|
-
|
|
20176
|
-
|
|
20177
|
-
|
|
20178
|
-
|
|
20179
|
-
|
|
20180
|
-
|
|
20181
|
-
|
|
20888
|
+
try {
|
|
20889
|
+
setStatus(threadId, "idle", null);
|
|
20890
|
+
appendMessage(threadId, {
|
|
20891
|
+
role: "agent",
|
|
20892
|
+
text: `Sideboard will auto-retry this chat around ${plan.resumeAt.toLocaleString()} when the session limit resets.`,
|
|
20893
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
20894
|
+
});
|
|
20895
|
+
this.emit({
|
|
20896
|
+
type: "quota_failover",
|
|
20897
|
+
threadId,
|
|
20898
|
+
action: "wait_reset",
|
|
20899
|
+
message: plan.reason,
|
|
20900
|
+
resumeAt: plan.resumeAt.toISOString()
|
|
20901
|
+
});
|
|
20902
|
+
this.emit({ type: "status_changed", threadId, status: "idle" });
|
|
20903
|
+
} catch {
|
|
20904
|
+
}
|
|
20182
20905
|
return true;
|
|
20183
20906
|
}
|
|
20184
20907
|
if (plan.action === "switch_agent" && plan.fallbackAgent) {
|
|
20185
20908
|
this.haltDrain.add(threadId);
|
|
20186
|
-
|
|
20187
|
-
|
|
20188
|
-
|
|
20189
|
-
|
|
20190
|
-
|
|
20909
|
+
let next;
|
|
20910
|
+
try {
|
|
20911
|
+
next = createQuotaFailoverChat(
|
|
20912
|
+
thread,
|
|
20913
|
+
plan.fallbackAgent,
|
|
20914
|
+
plan.limitText
|
|
20915
|
+
);
|
|
20916
|
+
} catch {
|
|
20917
|
+
return false;
|
|
20918
|
+
}
|
|
20191
20919
|
this.clearQuotaResumeTimer(threadId);
|
|
20192
20920
|
try {
|
|
20193
20921
|
updateThread(threadId, { quotaResumeAt: null });
|
|
20194
20922
|
} catch {
|
|
20195
20923
|
}
|
|
20196
|
-
|
|
20197
|
-
|
|
20198
|
-
|
|
20199
|
-
|
|
20200
|
-
|
|
20201
|
-
|
|
20202
|
-
|
|
20203
|
-
|
|
20204
|
-
|
|
20205
|
-
|
|
20206
|
-
|
|
20207
|
-
|
|
20208
|
-
|
|
20209
|
-
|
|
20210
|
-
|
|
20211
|
-
|
|
20212
|
-
|
|
20924
|
+
try {
|
|
20925
|
+
appendMessage(threadId, {
|
|
20926
|
+
role: "agent",
|
|
20927
|
+
text: `Session limit on ${thread.agent}. Sideboard continued on ${plan.fallbackAgent} (Auto) in [${next.title}](sideboard://thread/${next.id}).`,
|
|
20928
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
20929
|
+
});
|
|
20930
|
+
this.emit({
|
|
20931
|
+
type: "quota_failover",
|
|
20932
|
+
threadId,
|
|
20933
|
+
action: "switch_agent",
|
|
20934
|
+
toThreadId: next.id,
|
|
20935
|
+
message: plan.reason
|
|
20936
|
+
});
|
|
20937
|
+
this.emit({
|
|
20938
|
+
type: "status_changed",
|
|
20939
|
+
threadId: next.id,
|
|
20940
|
+
status: next.status
|
|
20941
|
+
});
|
|
20942
|
+
} catch {
|
|
20943
|
+
}
|
|
20213
20944
|
void this.send(
|
|
20214
20945
|
next.id,
|
|
20215
20946
|
QUOTA_CONTINUE_PROMPT(thread.agent, plan.fallbackAgent)
|
|
@@ -20940,9 +21671,8 @@ var init_orchestrator = __esm({
|
|
|
20940
21671
|
this.maybeEnqueueJobContinue(threadId, chatText, parts);
|
|
20941
21672
|
} else {
|
|
20942
21673
|
const blob = [chatText, detail].filter(Boolean).join("\n");
|
|
20943
|
-
const failedOver = await
|
|
20944
|
-
threadId,
|
|
20945
|
-
blob
|
|
21674
|
+
const failedOver = await isolateQuotaFailover(
|
|
21675
|
+
() => this.maybeHandleOrchestrationQuotaFailover(threadId, blob)
|
|
20946
21676
|
);
|
|
20947
21677
|
this.maybeEnqueueCrashContinue(threadId, {
|
|
20948
21678
|
detail: detail || failDetail,
|
|
@@ -20976,9 +21706,8 @@ var init_orchestrator = __esm({
|
|
|
20976
21706
|
this.emit({ type: "status_changed", threadId, status: "error" });
|
|
20977
21707
|
}
|
|
20978
21708
|
this.emit({ type: "turn_finished", threadId, exitCode: 1 });
|
|
20979
|
-
const failedOver = await
|
|
20980
|
-
threadId,
|
|
20981
|
-
message
|
|
21709
|
+
const failedOver = await isolateQuotaFailover(
|
|
21710
|
+
() => this.maybeHandleOrchestrationQuotaFailover(threadId, message)
|
|
20982
21711
|
);
|
|
20983
21712
|
this.maybeEnqueueCrashContinue(threadId, {
|
|
20984
21713
|
detail: message,
|
|
@@ -22068,7 +22797,7 @@ init_system_ca();
|
|
|
22068
22797
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
22069
22798
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
22070
22799
|
var import_zod9 = require("zod");
|
|
22071
|
-
var
|
|
22800
|
+
var import_node_crypto14 = require("crypto");
|
|
22072
22801
|
var import_node_path53 = require("path");
|
|
22073
22802
|
init_orchestrator();
|
|
22074
22803
|
init_worktree();
|
|
@@ -22495,8 +23224,8 @@ async function createGitHubIssue(input, opts) {
|
|
|
22495
23224
|
init_fetch();
|
|
22496
23225
|
|
|
22497
23226
|
// src/integrations/linear-oauth.ts
|
|
22498
|
-
var
|
|
22499
|
-
var
|
|
23227
|
+
var import_node_crypto12 = require("crypto");
|
|
23228
|
+
var import_node_http2 = require("http");
|
|
22500
23229
|
init_app_settings();
|
|
22501
23230
|
init_fetch();
|
|
22502
23231
|
|
|
@@ -22507,7 +23236,7 @@ var BAKED_LINEAR_CLIENT_ID = "39a15abc7ea5bea17c47f47f85ff5fd0";
|
|
|
22507
23236
|
var LINEAR_OAUTH_PORT = 19848;
|
|
22508
23237
|
var LINEAR_OAUTH_REDIRECT = `http://127.0.0.1:${LINEAR_OAUTH_PORT}/callback`;
|
|
22509
23238
|
var LINEAR_TOKEN = "https://api.linear.app/oauth/token";
|
|
22510
|
-
var
|
|
23239
|
+
var REFRESH_SKEW_MS3 = 5 * 6e4;
|
|
22511
23240
|
function linearOAuthCredentials() {
|
|
22512
23241
|
const settings = loadAppSettings();
|
|
22513
23242
|
const clientId = process.env.SIDEBOARD_LINEAR_CLIENT_ID?.trim() || settings.integrations.linearClientId?.trim() || BAKED_LINEAR_CLIENT_ID.trim();
|
|
@@ -22548,7 +23277,7 @@ async function persistLinearTokens(data, viewer) {
|
|
|
22548
23277
|
organizationName: viewer?.organizationName
|
|
22549
23278
|
});
|
|
22550
23279
|
}
|
|
22551
|
-
var
|
|
23280
|
+
var refreshInFlight2 = null;
|
|
22552
23281
|
async function refreshLinearAccessToken(refreshToken) {
|
|
22553
23282
|
const { clientId, clientSecret } = linearOAuthCredentials();
|
|
22554
23283
|
const body = new URLSearchParams({
|
|
@@ -22575,16 +23304,16 @@ async function getLinearAuthToken(settings = loadAppSettings()) {
|
|
|
22575
23304
|
const refresh = settings.integrations.linearRefreshToken?.trim() || "";
|
|
22576
23305
|
const expiresAt = settings.integrations.linearTokenExpiresAt ?? 0;
|
|
22577
23306
|
const apiKey = settings.integrations.linearApiKey?.trim() || "";
|
|
22578
|
-
if (access && (!refresh || Date.now() < expiresAt -
|
|
23307
|
+
if (access && (!refresh || Date.now() < expiresAt - REFRESH_SKEW_MS3)) {
|
|
22579
23308
|
return access;
|
|
22580
23309
|
}
|
|
22581
23310
|
if (refresh) {
|
|
22582
|
-
if (!
|
|
22583
|
-
|
|
22584
|
-
|
|
23311
|
+
if (!refreshInFlight2) {
|
|
23312
|
+
refreshInFlight2 = refreshLinearAccessToken(refresh).finally(() => {
|
|
23313
|
+
refreshInFlight2 = null;
|
|
22585
23314
|
});
|
|
22586
23315
|
}
|
|
22587
|
-
return
|
|
23316
|
+
return refreshInFlight2.catch((err) => {
|
|
22588
23317
|
if (apiKey) return apiKey;
|
|
22589
23318
|
throw err;
|
|
22590
23319
|
});
|
|
@@ -25478,7 +26207,7 @@ init_worktree();
|
|
|
25478
26207
|
init_paths();
|
|
25479
26208
|
|
|
25480
26209
|
// src/board/board-pins.ts
|
|
25481
|
-
var
|
|
26210
|
+
var import_node_crypto13 = require("crypto");
|
|
25482
26211
|
var import_node_fs55 = require("fs");
|
|
25483
26212
|
var import_node_path51 = require("path");
|
|
25484
26213
|
init_paths();
|
|
@@ -26327,7 +27056,7 @@ async function startMcpServer() {
|
|
|
26327
27056
|
const ident = issue?.identifier ?? ref.trim();
|
|
26328
27057
|
const attachments = issue ? [
|
|
26329
27058
|
{
|
|
26330
|
-
id: (0,
|
|
27059
|
+
id: (0, import_node_crypto14.randomUUID)(),
|
|
26331
27060
|
name: issue.identifier,
|
|
26332
27061
|
kind: "issue",
|
|
26333
27062
|
content: [
|