@sideboard-ai/core 0.1.274 → 0.1.276
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-4ZSDI4YP.js → abletime-VCVPSLSU.js} +4 -4
- package/dist/{abletime-JNQAHLHH.js → abletime-VRATUMDO.js} +4 -4
- package/dist/{abletime-oauth-4RXZH2KU.js → abletime-oauth-4C4G2J3R.js} +3 -3
- package/dist/{abletime-oauth-DCEIINYT.js → abletime-oauth-4K7MICAD.js} +3 -3
- package/dist/{abletime-rest-KVBJFVFD.js → abletime-rest-6N63ZVQ3.js} +3 -3
- package/dist/{abletime-rest-VN6PZ6P2.js → abletime-rest-PIBCNHF3.js} +3 -3
- package/dist/{agents-LAFFXBBZ.js → agents-2FOWVJCN.js} +4 -4
- package/dist/{agents-ZCO37SKG.js → agents-JBVJRDH3.js} +4 -4
- package/dist/{app-settings-RAC4PSSP.js → app-settings-EQQZQNYD.js} +1 -1
- package/dist/{app-settings-L5HGYXWJ.js → app-settings-NI4X5HW3.js} +1 -1
- package/dist/{chunk-UXI52MLJ.js → chunk-2J34G3WJ.js} +3 -3
- package/dist/{chunk-PTNGMA5C.js → chunk-A2G3ZBJS.js} +2 -2
- package/dist/{chunk-BJ5T22L5.js → chunk-FELFTNH2.js} +22 -21
- package/dist/{chunk-GCSG3OGV.js → chunk-HOMZKYHF.js} +29 -27
- package/dist/{chunk-GONPMH36.js → chunk-IJDQKC5A.js} +5 -5
- package/dist/{chunk-USBZ7PH4.js → chunk-IPPY3FG5.js} +1 -1
- package/dist/{chunk-NWE3NOWQ.js → chunk-K7MWJLR7.js} +1 -1
- package/dist/{chunk-DVTEC4BF.js → chunk-MGS5T2A3.js} +29 -27
- package/dist/{chunk-ADWNK2JH.js → chunk-NIB2IZ6J.js} +5 -5
- package/dist/{chunk-AGXZAMOF.js → chunk-NQ7JMVSU.js} +2 -2
- package/dist/{chunk-JLDJGGCL.js → chunk-NT7RIWRR.js} +5 -5
- package/dist/{chunk-L6CYOON4.js → chunk-PZURG3XN.js} +5 -5
- package/dist/{chunk-LAIBYKZS.js → chunk-QINJKNLR.js} +3 -3
- package/dist/{chunk-2QUILREI.js → chunk-QTZO4LPU.js} +2 -2
- package/dist/{chunk-KZ2CDZBZ.js → chunk-R7HLXD4I.js} +3 -3
- package/dist/{chunk-XIVM7K77.js → chunk-SQOAFR2U.js} +23 -22
- package/dist/{chunk-OIENQEGG.js → chunk-WRIMAEF2.js} +67 -122
- package/dist/{chunk-DBFAFI5A.js → chunk-XKZAVGTT.js} +2 -2
- package/dist/{chunk-PAH7KDL2.js → chunk-YNTXWFXT.js} +67 -122
- package/dist/{conductor-B2BXSA7N.js → conductor-GBCE37K4.js} +4 -4
- package/dist/{conductor-DXBLL23V.js → conductor-YYQKZAND.js} +4 -4
- package/dist/{coordinator-prompt-4KVA6XUX.js → coordinator-prompt-B3GLVXPP.js} +3 -3
- package/dist/{coordinator-prompt-MVUHFNSC.js → coordinator-prompt-SFZCZ3HR.js} +3 -3
- package/dist/{global-workspace-FR7QTLPK.js → global-workspace-7XEXTIHL.js} +3 -3
- package/dist/{global-workspace-34Z2IWYQ.js → global-workspace-N2YYJKA5.js} +3 -3
- package/dist/index.cjs +1989 -182
- package/dist/index.d.cts +121 -22
- package/dist/index.d.ts +121 -22
- package/dist/index.js +1872 -45
- package/dist/mcp/run-stdio.cjs +90 -136
- package/dist/mcp/run-stdio.js +11 -11
- package/dist/{orchestrator-6T35CJHB.js → orchestrator-6QJLM6Y4.js} +6 -6
- package/dist/{orchestrator-QRSEOHXI.js → orchestrator-QMKVXVUR.js} +6 -6
- package/dist/{workspaces-5GERYHDJ.js → workspaces-BJCHMGVG.js} +3 -3
- package/dist/{workspaces-2JBDUMM6.js → workspaces-PBB6HY2D.js} +3 -3
- package/dist/{worktree-ACW23KJ5.js → worktree-L57T3KH7.js} +2 -2
- package/dist/{worktree-C7RPVHZ7.js → worktree-Q6UJHG25.js} +2 -2
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1081,6 +1081,9 @@ function normalizeVault(raw) {
|
|
|
1081
1081
|
if (typeof raw.remoteHostSecret === "string" && raw.remoteHostSecret.trim()) {
|
|
1082
1082
|
out.remoteHostSecret = raw.remoteHostSecret.trim();
|
|
1083
1083
|
}
|
|
1084
|
+
if (typeof raw.remoteAccountToken === "string" && raw.remoteAccountToken.trim()) {
|
|
1085
|
+
out.remoteAccountToken = raw.remoteAccountToken.trim();
|
|
1086
|
+
}
|
|
1084
1087
|
if (typeof raw.githubPat === "string" && raw.githubPat.trim()) {
|
|
1085
1088
|
out.githubPat = raw.githubPat.trim();
|
|
1086
1089
|
}
|
|
@@ -1777,7 +1780,8 @@ function emptyPublicIntegrations() {
|
|
|
1777
1780
|
hasVercelToken: false,
|
|
1778
1781
|
hasSupabaseToken: false,
|
|
1779
1782
|
hasPosthogToken: false,
|
|
1780
|
-
hasSentryToken: false
|
|
1783
|
+
hasSentryToken: false,
|
|
1784
|
+
hasRemoteAccount: false
|
|
1781
1785
|
};
|
|
1782
1786
|
}
|
|
1783
1787
|
function hasLinearOAuth(integrations) {
|
|
@@ -1794,29 +1798,7 @@ function toPublicAppSettings(settings) {
|
|
|
1794
1798
|
if (key.trim()) environment[key] = "";
|
|
1795
1799
|
}
|
|
1796
1800
|
const integrations = { ...settings.integrations };
|
|
1797
|
-
const
|
|
1798
|
-
const slackAppToken = integrations.slackAppToken;
|
|
1799
|
-
const githubPat = integrations.githubPat;
|
|
1800
|
-
const abletimeAccessToken = integrations.abletimeAccessToken;
|
|
1801
|
-
const abletimeRefreshToken = integrations.abletimeRefreshToken;
|
|
1802
|
-
const vercelToken = integrations.vercelToken;
|
|
1803
|
-
const supabaseAccessToken = integrations.supabaseAccessToken;
|
|
1804
|
-
const posthogPersonalApiKey = integrations.posthogPersonalApiKey;
|
|
1805
|
-
const sentryAuthToken = integrations.sentryAuthToken;
|
|
1806
|
-
delete integrations.linearApiKey;
|
|
1807
|
-
delete integrations.linearAccessToken;
|
|
1808
|
-
delete integrations.linearRefreshToken;
|
|
1809
|
-
delete integrations.linearClientSecret;
|
|
1810
|
-
delete integrations.slackClientSecret;
|
|
1811
|
-
delete integrations.slackAppToken;
|
|
1812
|
-
delete integrations.remoteHostSecret;
|
|
1813
|
-
delete integrations.githubPat;
|
|
1814
|
-
delete integrations.abletimeAccessToken;
|
|
1815
|
-
delete integrations.abletimeRefreshToken;
|
|
1816
|
-
delete integrations.vercelToken;
|
|
1817
|
-
delete integrations.supabaseAccessToken;
|
|
1818
|
-
delete integrations.posthogPersonalApiKey;
|
|
1819
|
-
delete integrations.sentryAuthToken;
|
|
1801
|
+
for (const key of INTEGRATION_VAULT_KEYS) delete integrations[key];
|
|
1820
1802
|
return {
|
|
1821
1803
|
...settings,
|
|
1822
1804
|
environment,
|
|
@@ -1824,15 +1806,18 @@ function toPublicAppSettings(settings) {
|
|
|
1824
1806
|
...integrations,
|
|
1825
1807
|
hasLinearApiKey: hasLinearCredentials(settings.integrations),
|
|
1826
1808
|
hasLinearOAuth: hasLinearOAuth(settings.integrations),
|
|
1827
|
-
hasSlackClientSecret: Boolean(slackClientSecret?.trim()),
|
|
1828
|
-
hasSlackAppToken: Boolean(slackAppToken?.trim()),
|
|
1829
|
-
hasGithubPat: Boolean(githubPat?.trim()),
|
|
1830
|
-
hasAbleTimeToken: Boolean(
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1809
|
+
hasSlackClientSecret: Boolean(settings.integrations.slackClientSecret?.trim()),
|
|
1810
|
+
hasSlackAppToken: Boolean(settings.integrations.slackAppToken?.trim()),
|
|
1811
|
+
hasGithubPat: Boolean(settings.integrations.githubPat?.trim()),
|
|
1812
|
+
hasAbleTimeToken: Boolean(
|
|
1813
|
+
settings.integrations.abletimeAccessToken?.trim() || settings.integrations.abletimeRefreshToken?.trim()
|
|
1814
|
+
),
|
|
1815
|
+
hasAbleTimeOAuth: Boolean(settings.integrations.abletimeRefreshToken?.trim()),
|
|
1816
|
+
hasVercelToken: Boolean(settings.integrations.vercelToken?.trim()),
|
|
1817
|
+
hasSupabaseToken: Boolean(settings.integrations.supabaseAccessToken?.trim()),
|
|
1818
|
+
hasPosthogToken: Boolean(settings.integrations.posthogPersonalApiKey?.trim()),
|
|
1819
|
+
hasSentryToken: Boolean(settings.integrations.sentryAuthToken?.trim()),
|
|
1820
|
+
hasRemoteAccount: Boolean(settings.integrations.remoteAccountToken?.trim())
|
|
1836
1821
|
}
|
|
1837
1822
|
};
|
|
1838
1823
|
}
|
|
@@ -1959,6 +1944,14 @@ function normalizeIntegrations(raw) {
|
|
|
1959
1944
|
const secret = source.remoteHostSecret.trim();
|
|
1960
1945
|
if (secret) out.remoteHostSecret = secret;
|
|
1961
1946
|
}
|
|
1947
|
+
if (typeof source.remoteAccountToken === "string") {
|
|
1948
|
+
const token = source.remoteAccountToken.trim();
|
|
1949
|
+
if (token) out.remoteAccountToken = token;
|
|
1950
|
+
}
|
|
1951
|
+
if (typeof source.remoteAccountEmail === "string") {
|
|
1952
|
+
const email = source.remoteAccountEmail.trim().toLowerCase();
|
|
1953
|
+
if (email) out.remoteAccountEmail = email;
|
|
1954
|
+
}
|
|
1962
1955
|
if (typeof source.githubGitAuthMode === "string" && GIT_AUTH_MODES.has(source.githubGitAuthMode)) {
|
|
1963
1956
|
out.githubGitAuthMode = source.githubGitAuthMode;
|
|
1964
1957
|
}
|
|
@@ -2302,110 +2295,31 @@ function readSettingsFile() {
|
|
|
2302
2295
|
}
|
|
2303
2296
|
}
|
|
2304
2297
|
function diskHoldsSecrets(disk) {
|
|
2305
|
-
return Boolean(
|
|
2306
|
-
disk.integrations.linearApiKey || disk.integrations.linearAccessToken || disk.integrations.linearRefreshToken || disk.integrations.linearClientSecret || disk.integrations.slackClientSecret || disk.integrations.slackAppToken || disk.integrations.remoteHostSecret || 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
|
|
2307
|
-
);
|
|
2298
|
+
return INTEGRATION_VAULT_KEYS.some((key) => Boolean(disk.integrations[key])) || Object.keys(disk.environment).length > 0;
|
|
2308
2299
|
}
|
|
2309
2300
|
function mergeVault(disk) {
|
|
2310
2301
|
const vault = loadSecretVault();
|
|
2311
2302
|
const environment = { ...vault.environment ?? {}, ...disk.environment };
|
|
2312
2303
|
const integrations = { ...disk.integrations };
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
if (vault.linearAccessToken && !integrations.linearAccessToken) {
|
|
2317
|
-
integrations.linearAccessToken = vault.linearAccessToken;
|
|
2318
|
-
}
|
|
2319
|
-
if (vault.linearRefreshToken && !integrations.linearRefreshToken) {
|
|
2320
|
-
integrations.linearRefreshToken = vault.linearRefreshToken;
|
|
2321
|
-
}
|
|
2322
|
-
if (vault.linearClientSecret && !integrations.linearClientSecret) {
|
|
2323
|
-
integrations.linearClientSecret = vault.linearClientSecret;
|
|
2324
|
-
}
|
|
2325
|
-
if (vault.slackClientSecret && !integrations.slackClientSecret) {
|
|
2326
|
-
integrations.slackClientSecret = vault.slackClientSecret;
|
|
2327
|
-
}
|
|
2328
|
-
if (vault.slackAppToken && !integrations.slackAppToken) {
|
|
2329
|
-
integrations.slackAppToken = vault.slackAppToken;
|
|
2330
|
-
}
|
|
2331
|
-
if (vault.remoteHostSecret && !integrations.remoteHostSecret) {
|
|
2332
|
-
integrations.remoteHostSecret = vault.remoteHostSecret;
|
|
2333
|
-
}
|
|
2334
|
-
if (vault.githubPat && !integrations.githubPat) {
|
|
2335
|
-
integrations.githubPat = vault.githubPat;
|
|
2336
|
-
}
|
|
2337
|
-
if (vault.abletimeAccessToken && !integrations.abletimeAccessToken) {
|
|
2338
|
-
integrations.abletimeAccessToken = vault.abletimeAccessToken;
|
|
2339
|
-
}
|
|
2340
|
-
if (vault.abletimeRefreshToken && !integrations.abletimeRefreshToken) {
|
|
2341
|
-
integrations.abletimeRefreshToken = vault.abletimeRefreshToken;
|
|
2342
|
-
}
|
|
2343
|
-
if (vault.vercelToken && !integrations.vercelToken) {
|
|
2344
|
-
integrations.vercelToken = vault.vercelToken;
|
|
2345
|
-
}
|
|
2346
|
-
if (vault.supabaseAccessToken && !integrations.supabaseAccessToken) {
|
|
2347
|
-
integrations.supabaseAccessToken = vault.supabaseAccessToken;
|
|
2348
|
-
}
|
|
2349
|
-
if (vault.posthogPersonalApiKey && !integrations.posthogPersonalApiKey) {
|
|
2350
|
-
integrations.posthogPersonalApiKey = vault.posthogPersonalApiKey;
|
|
2351
|
-
}
|
|
2352
|
-
if (vault.sentryAuthToken && !integrations.sentryAuthToken) {
|
|
2353
|
-
integrations.sentryAuthToken = vault.sentryAuthToken;
|
|
2304
|
+
for (const key of INTEGRATION_VAULT_KEYS) {
|
|
2305
|
+
const stored = vault[key];
|
|
2306
|
+
if (stored && !integrations[key]) integrations[key] = stored;
|
|
2354
2307
|
}
|
|
2355
2308
|
return { ...disk, environment, integrations };
|
|
2356
2309
|
}
|
|
2357
2310
|
function persistSplit(settings) {
|
|
2358
2311
|
const integrations = { ...settings.integrations };
|
|
2359
|
-
const
|
|
2360
|
-
const
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
const slackAppToken = integrations.slackAppToken;
|
|
2365
|
-
const remoteHostSecret = integrations.remoteHostSecret;
|
|
2366
|
-
const githubPat = integrations.githubPat;
|
|
2367
|
-
const abletimeAccessToken = integrations.abletimeAccessToken;
|
|
2368
|
-
const abletimeRefreshToken = integrations.abletimeRefreshToken;
|
|
2369
|
-
const vercelToken = integrations.vercelToken;
|
|
2370
|
-
const supabaseAccessToken = integrations.supabaseAccessToken;
|
|
2371
|
-
const posthogPersonalApiKey = integrations.posthogPersonalApiKey;
|
|
2372
|
-
const sentryAuthToken = integrations.sentryAuthToken;
|
|
2373
|
-
delete integrations.linearApiKey;
|
|
2374
|
-
delete integrations.linearAccessToken;
|
|
2375
|
-
delete integrations.linearRefreshToken;
|
|
2376
|
-
delete integrations.linearClientSecret;
|
|
2377
|
-
delete integrations.slackClientSecret;
|
|
2378
|
-
delete integrations.slackAppToken;
|
|
2379
|
-
delete integrations.remoteHostSecret;
|
|
2380
|
-
delete integrations.githubPat;
|
|
2381
|
-
delete integrations.abletimeAccessToken;
|
|
2382
|
-
delete integrations.abletimeRefreshToken;
|
|
2383
|
-
delete integrations.vercelToken;
|
|
2384
|
-
delete integrations.supabaseAccessToken;
|
|
2385
|
-
delete integrations.posthogPersonalApiKey;
|
|
2386
|
-
delete integrations.sentryAuthToken;
|
|
2312
|
+
const vault = { environment: settings.environment };
|
|
2313
|
+
for (const key of INTEGRATION_VAULT_KEYS) {
|
|
2314
|
+
vault[key] = integrations[key];
|
|
2315
|
+
delete integrations[key];
|
|
2316
|
+
}
|
|
2387
2317
|
writePrivateFile(
|
|
2388
2318
|
appSettingsPath(),
|
|
2389
2319
|
`${JSON.stringify({ ...settings, environment: {}, integrations }, null, 2)}
|
|
2390
2320
|
`
|
|
2391
2321
|
);
|
|
2392
|
-
saveSecretVault(
|
|
2393
|
-
linearApiKey,
|
|
2394
|
-
linearAccessToken,
|
|
2395
|
-
linearRefreshToken,
|
|
2396
|
-
linearClientSecret,
|
|
2397
|
-
slackClientSecret,
|
|
2398
|
-
slackAppToken,
|
|
2399
|
-
remoteHostSecret,
|
|
2400
|
-
githubPat,
|
|
2401
|
-
abletimeAccessToken,
|
|
2402
|
-
abletimeRefreshToken,
|
|
2403
|
-
vercelToken,
|
|
2404
|
-
supabaseAccessToken,
|
|
2405
|
-
posthogPersonalApiKey,
|
|
2406
|
-
sentryAuthToken,
|
|
2407
|
-
environment: settings.environment
|
|
2408
|
-
});
|
|
2322
|
+
saveSecretVault(vault);
|
|
2409
2323
|
}
|
|
2410
2324
|
function loadAppSettings() {
|
|
2411
2325
|
const disk = readSettingsFile();
|
|
@@ -2616,6 +2530,20 @@ function updateIntegrationsSettings(patch) {
|
|
|
2616
2530
|
integrations.remoteHostSecret = patch.remoteHostSecret.trim();
|
|
2617
2531
|
}
|
|
2618
2532
|
}
|
|
2533
|
+
if ("remoteAccountToken" in patch) {
|
|
2534
|
+
if (patch.remoteAccountToken == null || patch.remoteAccountToken.trim() === "") {
|
|
2535
|
+
delete integrations.remoteAccountToken;
|
|
2536
|
+
} else {
|
|
2537
|
+
integrations.remoteAccountToken = patch.remoteAccountToken.trim();
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
if ("remoteAccountEmail" in patch) {
|
|
2541
|
+
if (patch.remoteAccountEmail == null || patch.remoteAccountEmail.trim() === "") {
|
|
2542
|
+
delete integrations.remoteAccountEmail;
|
|
2543
|
+
} else {
|
|
2544
|
+
integrations.remoteAccountEmail = patch.remoteAccountEmail.trim().toLowerCase();
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2619
2547
|
if ("githubGitAuthMode" in patch) {
|
|
2620
2548
|
if (patch.githubGitAuthMode == null) {
|
|
2621
2549
|
delete integrations.githubGitAuthMode;
|
|
@@ -3354,7 +3282,7 @@ function childEnvWithAppSettings(extra) {
|
|
|
3354
3282
|
function harnessEnvKey(harness) {
|
|
3355
3283
|
return HARNESS_ENV_KEYS[harness];
|
|
3356
3284
|
}
|
|
3357
|
-
var import_node_crypto2, import_node_fs9, import_node_os4, import_node_path10, HARNESS_ENV_KEYS, DEFAULT_AGENTS, GITHUB_GIT_AUTH_MODES, FOLLOW_UP_BEHAVIORS, AGENT_DONE_SOUNDS, CHAT_TEXT_SCALES, CHAT_TEXT_SCALE_DEFAULT, CLOUD_CONNECT_AGENTS, ISSUE_SOURCES, GIT_AUTH_MODES, REVIEW_LABEL_MAX, EMPTY_SETTINGS, CONFIRM_ACCOUNT, CONFIRM_PROJECT, DEFAULT_CLI_BIN;
|
|
3285
|
+
var import_node_crypto2, import_node_fs9, import_node_os4, import_node_path10, HARNESS_ENV_KEYS, DEFAULT_AGENTS, GITHUB_GIT_AUTH_MODES, FOLLOW_UP_BEHAVIORS, AGENT_DONE_SOUNDS, CHAT_TEXT_SCALES, CHAT_TEXT_SCALE_DEFAULT, INTEGRATION_VAULT_KEYS, CLOUD_CONNECT_AGENTS, ISSUE_SOURCES, GIT_AUTH_MODES, REVIEW_LABEL_MAX, EMPTY_SETTINGS, CONFIRM_ACCOUNT, CONFIRM_PROJECT, DEFAULT_CLI_BIN;
|
|
3358
3286
|
var init_app_settings = __esm({
|
|
3359
3287
|
"src/store/app-settings.ts"() {
|
|
3360
3288
|
"use strict";
|
|
@@ -3407,6 +3335,23 @@ var init_app_settings = __esm({
|
|
|
3407
3335
|
];
|
|
3408
3336
|
CHAT_TEXT_SCALES = [1.2, 1.4];
|
|
3409
3337
|
CHAT_TEXT_SCALE_DEFAULT = 1.2;
|
|
3338
|
+
INTEGRATION_VAULT_KEYS = [
|
|
3339
|
+
"linearApiKey",
|
|
3340
|
+
"linearAccessToken",
|
|
3341
|
+
"linearRefreshToken",
|
|
3342
|
+
"linearClientSecret",
|
|
3343
|
+
"slackClientSecret",
|
|
3344
|
+
"slackAppToken",
|
|
3345
|
+
"remoteHostSecret",
|
|
3346
|
+
"remoteAccountToken",
|
|
3347
|
+
"githubPat",
|
|
3348
|
+
"abletimeAccessToken",
|
|
3349
|
+
"abletimeRefreshToken",
|
|
3350
|
+
"vercelToken",
|
|
3351
|
+
"supabaseAccessToken",
|
|
3352
|
+
"posthogPersonalApiKey",
|
|
3353
|
+
"sentryAuthToken"
|
|
3354
|
+
];
|
|
3410
3355
|
CLOUD_CONNECT_AGENTS = /* @__PURE__ */ new Set([
|
|
3411
3356
|
"claude",
|
|
3412
3357
|
"codex",
|
|
@@ -18515,7 +18460,7 @@ function lastRequestOccupancy(thread) {
|
|
|
18515
18460
|
function shouldResetSessionForOccupancy(thread, occupancyTokens = SESSION_RESET_OCCUPANCY_TOKENS) {
|
|
18516
18461
|
return lastRequestOccupancy(thread) >= occupancyTokens;
|
|
18517
18462
|
}
|
|
18518
|
-
async function maybeCompactContext(thread, thresholds = {},
|
|
18463
|
+
async function maybeCompactContext(thread, thresholds = {}, summarize2 = summarizeConversation) {
|
|
18519
18464
|
if (!shouldCompactContext(thread.messages, thresholds)) {
|
|
18520
18465
|
return { didCompact: false, thread };
|
|
18521
18466
|
}
|
|
@@ -18524,7 +18469,7 @@ async function maybeCompactContext(thread, thresholds = {}, summarize = summariz
|
|
|
18524
18469
|
return { didCompact: false, thread };
|
|
18525
18470
|
}
|
|
18526
18471
|
const transcript = formatMessagesAsTranscript(older, { tools: "summary" });
|
|
18527
|
-
const { summary, method } = await
|
|
18472
|
+
const { summary, method } = await summarize2(transcript, {
|
|
18528
18473
|
cwd: thread.worktreePath
|
|
18529
18474
|
});
|
|
18530
18475
|
let messages = applyCompaction(thread.messages, summary, thresholds);
|
|
@@ -19213,17 +19158,7 @@ var init_worktree_run_ports = __esm({
|
|
|
19213
19158
|
}
|
|
19214
19159
|
});
|
|
19215
19160
|
|
|
19216
|
-
// src/mcp/
|
|
19217
|
-
function mcpWaitForJobTimeoutMs(requested) {
|
|
19218
|
-
return mcpWaitForTurnTimeoutMs(requested);
|
|
19219
|
-
}
|
|
19220
|
-
function sanitizeDetachedJobId(id) {
|
|
19221
|
-
const s = id.trim();
|
|
19222
|
-
if (!JOB_ID_RE.test(s)) {
|
|
19223
|
-
throw new Error(`detached-job id must be 1\u201364 chars [A-Za-z0-9._-], got ${JSON.stringify(id)}`);
|
|
19224
|
-
}
|
|
19225
|
-
return s;
|
|
19226
|
-
}
|
|
19161
|
+
// src/mcp/job-process.ts
|
|
19227
19162
|
function jobAlive(pid) {
|
|
19228
19163
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
19229
19164
|
try {
|
|
@@ -19247,6 +19182,23 @@ function jobTreeAlive(pid) {
|
|
|
19247
19182
|
if (pid == null) return false;
|
|
19248
19183
|
return jobAlive(pid) || processGroupAlive(pid);
|
|
19249
19184
|
}
|
|
19185
|
+
var init_job_process = __esm({
|
|
19186
|
+
"src/mcp/job-process.ts"() {
|
|
19187
|
+
"use strict";
|
|
19188
|
+
}
|
|
19189
|
+
});
|
|
19190
|
+
|
|
19191
|
+
// src/mcp/wait-for-job.ts
|
|
19192
|
+
function mcpWaitForJobTimeoutMs(requested) {
|
|
19193
|
+
return mcpWaitForTurnTimeoutMs(requested);
|
|
19194
|
+
}
|
|
19195
|
+
function sanitizeDetachedJobId(id) {
|
|
19196
|
+
const s = id.trim();
|
|
19197
|
+
if (!JOB_ID_RE.test(s)) {
|
|
19198
|
+
throw new Error(`detached-job id must be 1\u201364 chars [A-Za-z0-9._-], got ${JSON.stringify(id)}`);
|
|
19199
|
+
}
|
|
19200
|
+
return s;
|
|
19201
|
+
}
|
|
19250
19202
|
function readIntFile(file) {
|
|
19251
19203
|
if (!(0, import_node_fs50.existsSync)(file)) return null;
|
|
19252
19204
|
const n = Number.parseInt((0, import_node_fs50.readFileSync)(file, "utf8").trim(), 10);
|
|
@@ -19430,7 +19382,7 @@ async function waitForDetachedJob(cwd, id, opts) {
|
|
|
19430
19382
|
const root = cwd.trim() || process.cwd();
|
|
19431
19383
|
const dir = resolveJobDir(root, jobId);
|
|
19432
19384
|
const timeoutMs = mcpWaitForJobTimeoutMs(opts?.timeoutMs);
|
|
19433
|
-
const
|
|
19385
|
+
const sleep3 = opts?.sleep ?? sleepMs;
|
|
19434
19386
|
if (!(0, import_node_fs50.existsSync)(dir)) {
|
|
19435
19387
|
return {
|
|
19436
19388
|
stillRunning: false,
|
|
@@ -19456,7 +19408,7 @@ async function waitForDetachedJob(cwd, id, opts) {
|
|
|
19456
19408
|
snap = snapshotJob(dir);
|
|
19457
19409
|
if (snap.exitCode === 0 && !snap.running) return toResult(jobId, snap);
|
|
19458
19410
|
if (!snap.running) return toResult(jobId, snap, { failed: true });
|
|
19459
|
-
await
|
|
19411
|
+
await sleep3(Math.min(2e3, Math.max(50, deadline - Date.now())));
|
|
19460
19412
|
}
|
|
19461
19413
|
snap = snapshotJob(dir);
|
|
19462
19414
|
return toResult(jobId, snap);
|
|
@@ -19488,7 +19440,7 @@ async function stopDetachedJob(cwd, id, opts) {
|
|
|
19488
19440
|
const jobId = sanitizeDetachedJobId(id);
|
|
19489
19441
|
const root = cwd.trim() || process.cwd();
|
|
19490
19442
|
const dir = resolveJobDir(root, jobId);
|
|
19491
|
-
const
|
|
19443
|
+
const sleep3 = opts?.sleep ?? sleepMs;
|
|
19492
19444
|
const why = opts?.reason?.trim() ?? "";
|
|
19493
19445
|
if (!(0, import_node_fs50.existsSync)(dir)) {
|
|
19494
19446
|
return {
|
|
@@ -19520,11 +19472,11 @@ async function stopDetachedJob(cwd, id, opts) {
|
|
|
19520
19472
|
const graceMs = Number.isFinite(opts?.graceMs) ? Math.max(50, opts.graceMs) : 2e3;
|
|
19521
19473
|
const deadline = Date.now() + graceMs;
|
|
19522
19474
|
while (Date.now() < deadline && jobTreeAlive(targetPid)) {
|
|
19523
|
-
await
|
|
19475
|
+
await sleep3(Math.min(100, Math.max(20, deadline - Date.now())));
|
|
19524
19476
|
}
|
|
19525
19477
|
if (targetPid != null) {
|
|
19526
19478
|
killProcessTree(targetPid, "SIGKILL");
|
|
19527
|
-
await
|
|
19479
|
+
await sleep3(100);
|
|
19528
19480
|
}
|
|
19529
19481
|
const exitFile = (0, import_node_path50.join)(dir, "exit");
|
|
19530
19482
|
if (!(0, import_node_fs50.existsSync)(exitFile)) {
|
|
@@ -19556,6 +19508,7 @@ var init_wait_for_job = __esm({
|
|
|
19556
19508
|
import_node_fs50 = require("fs");
|
|
19557
19509
|
import_node_path50 = require("path");
|
|
19558
19510
|
init_workspace_scratch();
|
|
19511
|
+
init_job_process();
|
|
19559
19512
|
init_wait_for_turn();
|
|
19560
19513
|
MAX_JOB_CONTINUES = 8;
|
|
19561
19514
|
MAX_JOB_DELTA_LINES = 80;
|
|
@@ -26890,7 +26843,8 @@ var init_orchestrator = __esm({
|
|
|
26890
26843
|
throw new Error(`Thread is archived: ${thread.id}`);
|
|
26891
26844
|
}
|
|
26892
26845
|
const followUp = resolveSendFollowUp(current, opts?.followUp);
|
|
26893
|
-
const
|
|
26846
|
+
const explicit = opts?.attachments;
|
|
26847
|
+
const consumed = explicit !== void 0 ? { attachments: current.attachments, consumed: explicit } : consumeComposerAttachments(current);
|
|
26894
26848
|
const queued = appendQueuedItem(
|
|
26895
26849
|
current.queue,
|
|
26896
26850
|
current.queueAttachments,
|
|
@@ -26904,7 +26858,7 @@ var init_orchestrator = __esm({
|
|
|
26904
26858
|
const inFlight = this.activeTurns.has(thread.id) || this.startingTurns.has(thread.id) || current.status === "running";
|
|
26905
26859
|
shouldSteer = followUp === "steer" && (inFlight || current.queue.length > 0);
|
|
26906
26860
|
const patch = queued;
|
|
26907
|
-
if (consumed.consumed.length > 0) {
|
|
26861
|
+
if (explicit === void 0 && consumed.consumed.length > 0) {
|
|
26908
26862
|
patch.attachments = consumed.attachments;
|
|
26909
26863
|
}
|
|
26910
26864
|
if (!inFlight) patch.status = "queued";
|
|
@@ -29393,6 +29347,7 @@ __export(index_exports, {
|
|
|
29393
29347
|
disconnectLinear: () => disconnectLinear,
|
|
29394
29348
|
disconnectLinearConnection: () => disconnectLinearConnection,
|
|
29395
29349
|
disconnectOptionalService: () => disconnectOptionalService,
|
|
29350
|
+
disconnectRemoteAccount: () => disconnectRemoteAccount,
|
|
29396
29351
|
disconnectSlackWorkspace: () => disconnectSlackWorkspace,
|
|
29397
29352
|
discoverSkills: () => discoverSkills,
|
|
29398
29353
|
downloadAbleTimeAttachment: () => downloadAbleTimeAttachment,
|
|
@@ -29649,6 +29604,7 @@ __export(index_exports, {
|
|
|
29649
29604
|
isPrNotMergeableError: () => isPrNotMergeableError,
|
|
29650
29605
|
isPresentPlanToolName: () => isPresentPlanToolName,
|
|
29651
29606
|
isPrimaryCheckoutThread: () => isPrimaryCheckoutThread,
|
|
29607
|
+
isRemoteAccountLoginCancelled: () => isRemoteAccountLoginCancelled,
|
|
29652
29608
|
isRemoteStopCommand: () => isRemoteStopCommand,
|
|
29653
29609
|
isSelfWrittenRecord: () => isSelfWrittenRecord,
|
|
29654
29610
|
isSessionQuotaLimit: () => isSessionQuotaLimit,
|
|
@@ -30010,6 +29966,7 @@ __export(index_exports, {
|
|
|
30010
29966
|
startMcpServer: () => startMcpServer,
|
|
30011
29967
|
startOrchestration: () => startOrchestration,
|
|
30012
29968
|
startRelayServer: () => startRelayServer,
|
|
29969
|
+
startRemoteAccountLogin: () => startRemoteAccountLogin,
|
|
30013
29970
|
startSlackOAuth: () => startSlackOAuth,
|
|
30014
29971
|
statWorktreePath: () => statWorktreePath,
|
|
30015
29972
|
stopAgentRpcServer: () => stopAgentRpcServer,
|
|
@@ -30543,12 +30500,12 @@ async function persistLinearTokens(data, viewer) {
|
|
|
30543
30500
|
organizationName: viewer?.organizationName
|
|
30544
30501
|
});
|
|
30545
30502
|
}
|
|
30546
|
-
async function fetchLinearViewer(
|
|
30503
|
+
async function fetchLinearViewer(accessToken2) {
|
|
30547
30504
|
const res = await httpFetch(LINEAR_GRAPHQL, {
|
|
30548
30505
|
method: "POST",
|
|
30549
30506
|
headers: {
|
|
30550
30507
|
"Content-Type": "application/json",
|
|
30551
|
-
Authorization: linearAuthorizationHeader(
|
|
30508
|
+
Authorization: linearAuthorizationHeader(accessToken2)
|
|
30552
30509
|
},
|
|
30553
30510
|
body: JSON.stringify({
|
|
30554
30511
|
query: "{ viewer { name organization { name } } }"
|
|
@@ -38389,7 +38346,14 @@ function parseRemoteClientMessage(raw) {
|
|
|
38389
38346
|
const hostSecret = str5(parsed.hostSecret);
|
|
38390
38347
|
const deviceLabel = str5(parsed.deviceLabel);
|
|
38391
38348
|
if (!deviceId || !hostSecret || !deviceLabel) return null;
|
|
38392
|
-
|
|
38349
|
+
const accountToken = str5(parsed.accountToken);
|
|
38350
|
+
return {
|
|
38351
|
+
type: "host_register",
|
|
38352
|
+
deviceId,
|
|
38353
|
+
hostSecret,
|
|
38354
|
+
deviceLabel,
|
|
38355
|
+
...accountToken ? { accountToken } : {}
|
|
38356
|
+
};
|
|
38393
38357
|
}
|
|
38394
38358
|
if (msg.type === "pair") {
|
|
38395
38359
|
const code = str5(parsed.code).toUpperCase();
|
|
@@ -38518,6 +38482,10 @@ function newCode() {
|
|
|
38518
38482
|
return code;
|
|
38519
38483
|
}
|
|
38520
38484
|
var RemoteHub = class {
|
|
38485
|
+
constructor(opts = {}) {
|
|
38486
|
+
this.opts = opts;
|
|
38487
|
+
}
|
|
38488
|
+
opts;
|
|
38521
38489
|
hosts = /* @__PURE__ */ new Map();
|
|
38522
38490
|
remembered = /* @__PURE__ */ new Map();
|
|
38523
38491
|
bySocket = /* @__PURE__ */ new Map();
|
|
@@ -38556,7 +38524,7 @@ var RemoteHub = class {
|
|
|
38556
38524
|
}
|
|
38557
38525
|
dispatch(socket, msg) {
|
|
38558
38526
|
if (msg.type === "host_register") {
|
|
38559
|
-
this.registerHost(socket, msg.deviceId, msg.deviceLabel, msg.hostSecret);
|
|
38527
|
+
this.registerHost(socket, msg.deviceId, msg.deviceLabel, msg.hostSecret, msg.accountToken ?? "");
|
|
38560
38528
|
return;
|
|
38561
38529
|
}
|
|
38562
38530
|
if (msg.type === "host_pair") {
|
|
@@ -38583,7 +38551,14 @@ var RemoteHub = class {
|
|
|
38583
38551
|
this.fromHost(socket, msg);
|
|
38584
38552
|
}
|
|
38585
38553
|
}
|
|
38586
|
-
registerHost(socket, deviceId, deviceLabel, hostSecret) {
|
|
38554
|
+
registerHost(socket, deviceId, deviceLabel, hostSecret, accountToken) {
|
|
38555
|
+
if (this.opts.authorizeHost) {
|
|
38556
|
+
const decision = this.opts.authorizeHost({ accountToken, deviceId, deviceLabel, hostSecret });
|
|
38557
|
+
if (!decision.ok) {
|
|
38558
|
+
send2(socket, { type: "error", message: decision.message });
|
|
38559
|
+
return;
|
|
38560
|
+
}
|
|
38561
|
+
}
|
|
38587
38562
|
const prior = this.remembered.get(deviceId);
|
|
38588
38563
|
const existing = this.hosts.get(deviceId);
|
|
38589
38564
|
if (prior && prior.hostSecret !== hostSecret || existing && existing.hostSecret !== hostSecret) {
|
|
@@ -38615,6 +38590,7 @@ var RemoteHub = class {
|
|
|
38615
38590
|
this.bySocket.set(socket, { role: "host", deviceId });
|
|
38616
38591
|
send2(socket, { type: "registered", deviceId, deviceLabel });
|
|
38617
38592
|
if (host.phone) send2(socket, { type: "phone_joined" });
|
|
38593
|
+
this.opts.onAuthorized?.(socket);
|
|
38618
38594
|
}
|
|
38619
38595
|
issueCode(socket) {
|
|
38620
38596
|
const bound = this.bySocket.get(socket);
|
|
@@ -38676,6 +38652,7 @@ var RemoteHub = class {
|
|
|
38676
38652
|
online: Boolean(host)
|
|
38677
38653
|
};
|
|
38678
38654
|
});
|
|
38655
|
+
if (hosts.some((host) => host.deviceId)) this.opts.onAuthorized?.(socket);
|
|
38679
38656
|
send2(socket, { type: "hosts", hosts });
|
|
38680
38657
|
}
|
|
38681
38658
|
releasePhoneSocket(socket) {
|
|
@@ -38715,6 +38692,7 @@ var RemoteHub = class {
|
|
|
38715
38692
|
deviceLabel: host.deviceLabel
|
|
38716
38693
|
});
|
|
38717
38694
|
this.bySocket.set(socket, { role: "phone", deviceId: host.deviceId });
|
|
38695
|
+
this.opts.onAuthorized?.(socket);
|
|
38718
38696
|
send2(socket, {
|
|
38719
38697
|
type: "paired",
|
|
38720
38698
|
deviceId: host.deviceId,
|
|
@@ -38753,11 +38731,802 @@ var import_ws5 = require("ws");
|
|
|
38753
38731
|
|
|
38754
38732
|
// src/remote/listen.ts
|
|
38755
38733
|
init_orchestrator_capable();
|
|
38734
|
+
init_stage_files();
|
|
38756
38735
|
init_orchestrator();
|
|
38757
38736
|
init_app_settings();
|
|
38758
38737
|
init_global_workspace();
|
|
38759
38738
|
init_thread_store();
|
|
38739
|
+
init_implied_first_prompt();
|
|
38740
|
+
|
|
38741
|
+
// src/remote/phone-media.ts
|
|
38742
|
+
init_diff();
|
|
38743
|
+
var PHONE_IMAGE_MAX_BYTES = 15e5;
|
|
38744
|
+
var IMAGE_MIME = {
|
|
38745
|
+
png: "image/png",
|
|
38746
|
+
jpg: "image/jpeg",
|
|
38747
|
+
jpeg: "image/jpeg",
|
|
38748
|
+
gif: "image/gif",
|
|
38749
|
+
webp: "image/webp",
|
|
38750
|
+
bmp: "image/bmp"
|
|
38751
|
+
};
|
|
38752
|
+
function phoneWorktreeRelative(worktreePath, raw) {
|
|
38753
|
+
let text6 = raw.trim();
|
|
38754
|
+
if (!text6) return null;
|
|
38755
|
+
if (/^file:\/\//i.test(text6)) {
|
|
38756
|
+
try {
|
|
38757
|
+
text6 = decodeURIComponent(new URL(text6).pathname);
|
|
38758
|
+
} catch {
|
|
38759
|
+
return null;
|
|
38760
|
+
}
|
|
38761
|
+
}
|
|
38762
|
+
text6 = (text6.split("#")[0] ?? text6).replace(/\\/g, "/");
|
|
38763
|
+
if (!text6 || text6.includes("\0") || text6.split("/").includes("..")) return null;
|
|
38764
|
+
const root = worktreePath.replace(/\/+$/, "");
|
|
38765
|
+
if (!root || root === "/") return null;
|
|
38766
|
+
if (text6.startsWith("/")) {
|
|
38767
|
+
if (text6 !== root && !text6.startsWith(`${root}/`)) return null;
|
|
38768
|
+
text6 = text6 === root ? "" : text6.slice(root.length + 1);
|
|
38769
|
+
}
|
|
38770
|
+
text6 = text6.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
38771
|
+
if (!text6 || text6 === "." || text6.startsWith("/")) return null;
|
|
38772
|
+
return text6;
|
|
38773
|
+
}
|
|
38774
|
+
function phoneImageMime(relativePath) {
|
|
38775
|
+
const ext = relativePath.split(".").pop()?.toLowerCase() ?? "";
|
|
38776
|
+
return IMAGE_MIME[ext] ?? null;
|
|
38777
|
+
}
|
|
38778
|
+
function phoneImageDataUrl(worktreePath, raw) {
|
|
38779
|
+
const relative = phoneWorktreeRelative(worktreePath, raw);
|
|
38780
|
+
if (!relative) return null;
|
|
38781
|
+
const mime = phoneImageMime(relative);
|
|
38782
|
+
if (!mime) return null;
|
|
38783
|
+
try {
|
|
38784
|
+
const file = readWorktreeFileForUpload(worktreePath, relative, { maxBytes: PHONE_IMAGE_MAX_BYTES });
|
|
38785
|
+
return `data:${mime};base64,${file.contentBase64}`;
|
|
38786
|
+
} catch {
|
|
38787
|
+
return null;
|
|
38788
|
+
}
|
|
38789
|
+
}
|
|
38790
|
+
|
|
38791
|
+
// src/remote/phone-chats.ts
|
|
38792
|
+
var import_node_crypto25 = require("crypto");
|
|
38793
|
+
init_list_models();
|
|
38794
|
+
init_home_board();
|
|
38795
|
+
init_stage_files();
|
|
38796
|
+
init_worktree_labels();
|
|
38797
|
+
init_thinking_effort();
|
|
38798
|
+
init_app_settings();
|
|
38799
|
+
init_global_workspace();
|
|
38800
|
+
init_thread_store();
|
|
38801
|
+
init_workspaces();
|
|
38802
|
+
init_chat_tabs();
|
|
38803
|
+
init_create();
|
|
38804
|
+
init_orchestrator();
|
|
38805
|
+
var PHONE_CONTROL_PREFIX = "\0sb.phone\n";
|
|
38806
|
+
var PHONE_USER_PREFIX = "Phone\n\n";
|
|
38807
|
+
var TRANSCRIPT_LIMIT = 40;
|
|
38808
|
+
var MESSAGE_LIMIT = 4e3;
|
|
38809
|
+
var PREVIEW_LIMIT = 90;
|
|
38810
|
+
var HISTORY_PAGE_SIZE = 40;
|
|
38811
|
+
var AGENT_KINDS = /* @__PURE__ */ new Set(["claude", "codex", "opencode", "brightsy", "cursor"]);
|
|
38812
|
+
var MAX_PHONE_FILES = 8;
|
|
38813
|
+
var MAX_PHONE_FILE_CHARS = Math.ceil(8 * 1024 * 1024 * 4 / 3);
|
|
38814
|
+
var MAX_PHONE_FILES_CHARS = Math.ceil(20 * 1024 * 1024 * 4 / 3);
|
|
38815
|
+
var MAX_DICTATION_CHARS = Math.ceil(12 * 1024 * 1024 * 4 / 3);
|
|
38816
|
+
function encodePhoneControl(payload) {
|
|
38817
|
+
return PHONE_CONTROL_PREFIX + JSON.stringify(payload);
|
|
38818
|
+
}
|
|
38819
|
+
function takePhoneControl(text6) {
|
|
38820
|
+
if (!text6.startsWith(PHONE_CONTROL_PREFIX)) return null;
|
|
38821
|
+
let parsed;
|
|
38822
|
+
try {
|
|
38823
|
+
parsed = JSON.parse(text6.slice(PHONE_CONTROL_PREFIX.length));
|
|
38824
|
+
} catch {
|
|
38825
|
+
return "invalid";
|
|
38826
|
+
}
|
|
38827
|
+
return parsePhoneControl(parsed);
|
|
38828
|
+
}
|
|
38829
|
+
function parsePhoneControl(value) {
|
|
38830
|
+
if (!value || typeof value !== "object") return "invalid";
|
|
38831
|
+
const op = value.op;
|
|
38832
|
+
if (op === "list") return { op: "list" };
|
|
38833
|
+
if (op === "history") {
|
|
38834
|
+
const query = textField(value, "query");
|
|
38835
|
+
const after = textField(value, "after");
|
|
38836
|
+
return { op: "history", ...query ? { query } : {}, ...after ? { after } : {} };
|
|
38837
|
+
}
|
|
38838
|
+
if (op === "sources") {
|
|
38839
|
+
const repoPath = textField(value, "repoPath");
|
|
38840
|
+
if (!repoPath || !isProjectPath(repoPath)) return "invalid";
|
|
38841
|
+
const query = textField(value, "query");
|
|
38842
|
+
return { op: "sources", repoPath, ...query ? { query } : {} };
|
|
38843
|
+
}
|
|
38844
|
+
if (op === "models") {
|
|
38845
|
+
const agent = agentField(value);
|
|
38846
|
+
if (!agent) return "invalid";
|
|
38847
|
+
return { op: "models", agent };
|
|
38848
|
+
}
|
|
38849
|
+
if (op === "dictate") {
|
|
38850
|
+
const id = textField(value, "id");
|
|
38851
|
+
const audioBase64 = base64Field(value, "audioBase64");
|
|
38852
|
+
if (!id || !audioBase64 || audioBase64.length > MAX_DICTATION_CHARS) return "invalid";
|
|
38853
|
+
return { op: "dictate", id, audioBase64 };
|
|
38854
|
+
}
|
|
38855
|
+
if (op === "create") return parsePhoneCreate(value);
|
|
38856
|
+
const chatId = typeof value.chatId === "string" ? value.chatId.trim() : "";
|
|
38857
|
+
if (!chatId) return "invalid";
|
|
38858
|
+
if (op === "open" || op === "archive" || op === "restore" || op === "stop") return { op, chatId };
|
|
38859
|
+
if (op === "open-file" || op === "media") {
|
|
38860
|
+
const path2 = textField(value, "path");
|
|
38861
|
+
if (!path2 || path2.length > 2e3) return "invalid";
|
|
38862
|
+
if (op === "media") return { op, chatId, path: path2 };
|
|
38863
|
+
const startLine = lineField(value, "startLine");
|
|
38864
|
+
const endLine = lineField(value, "endLine");
|
|
38865
|
+
return {
|
|
38866
|
+
op,
|
|
38867
|
+
chatId,
|
|
38868
|
+
path: path2,
|
|
38869
|
+
...startLine != null ? { startLine } : {},
|
|
38870
|
+
...endLine != null ? { endLine } : {}
|
|
38871
|
+
};
|
|
38872
|
+
}
|
|
38873
|
+
const draft = parsePhoneDraft(value);
|
|
38874
|
+
if (draft === "invalid") return "invalid";
|
|
38875
|
+
if (op === "options") return { op: "options", chatId, ...draft };
|
|
38876
|
+
if (op === "prompt") {
|
|
38877
|
+
const text6 = typeof value.text === "string" ? value.text.trim() : "";
|
|
38878
|
+
const hasPayload = Boolean(text6 || draft.files?.length || draft.links?.length);
|
|
38879
|
+
if (!hasPayload) return "invalid";
|
|
38880
|
+
return { op: "prompt", chatId, text: text6, ...draft };
|
|
38881
|
+
}
|
|
38882
|
+
return "invalid";
|
|
38883
|
+
}
|
|
38884
|
+
function textField(value, key) {
|
|
38885
|
+
const raw = value[key];
|
|
38886
|
+
return typeof raw === "string" ? raw.trim() : "";
|
|
38887
|
+
}
|
|
38888
|
+
function lineField(value, key) {
|
|
38889
|
+
const raw = value[key];
|
|
38890
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1 || raw > 1e6) return void 0;
|
|
38891
|
+
return raw;
|
|
38892
|
+
}
|
|
38893
|
+
function agentField(value) {
|
|
38894
|
+
const agent = textField(value, "agent");
|
|
38895
|
+
if (!agent) return void 0;
|
|
38896
|
+
return AGENT_KINDS.has(agent) ? agent : void 0;
|
|
38897
|
+
}
|
|
38898
|
+
function base64Field(value, key) {
|
|
38899
|
+
const raw = value[key];
|
|
38900
|
+
if (typeof raw !== "string") return "";
|
|
38901
|
+
return raw.replace(/\s/g, "");
|
|
38902
|
+
}
|
|
38903
|
+
function parsePhoneDraft(value) {
|
|
38904
|
+
const draft = {};
|
|
38905
|
+
if (textField(value, "agent")) {
|
|
38906
|
+
const agent = agentField(value);
|
|
38907
|
+
if (!agent) return "invalid";
|
|
38908
|
+
draft.agent = agent;
|
|
38909
|
+
}
|
|
38910
|
+
if ("model" in value) {
|
|
38911
|
+
const model = value.model;
|
|
38912
|
+
if (model === null || model === "") draft.model = null;
|
|
38913
|
+
else if (typeof model === "string") draft.model = model.trim().slice(0, 200);
|
|
38914
|
+
else return "invalid";
|
|
38915
|
+
}
|
|
38916
|
+
if ("effort" in value && value.effort != null) {
|
|
38917
|
+
const effort = value.effort;
|
|
38918
|
+
if (!isThinkingEffort(effort)) return "invalid";
|
|
38919
|
+
draft.effort = effort;
|
|
38920
|
+
}
|
|
38921
|
+
if ("fast" in value) {
|
|
38922
|
+
const fast = value.fast;
|
|
38923
|
+
if (typeof fast !== "boolean") return "invalid";
|
|
38924
|
+
draft.fast = fast;
|
|
38925
|
+
}
|
|
38926
|
+
if ("planMode" in value) {
|
|
38927
|
+
const planMode = value.planMode;
|
|
38928
|
+
if (typeof planMode !== "boolean") return "invalid";
|
|
38929
|
+
draft.planMode = planMode;
|
|
38930
|
+
}
|
|
38931
|
+
if ("autonomy" in value && value.autonomy != null) {
|
|
38932
|
+
const autonomy = value.autonomy;
|
|
38933
|
+
if (autonomy !== "default" && autonomy !== "full") return "invalid";
|
|
38934
|
+
draft.autonomy = autonomy;
|
|
38935
|
+
}
|
|
38936
|
+
const files = parsePhoneFiles(value.files);
|
|
38937
|
+
if (files === "invalid") return "invalid";
|
|
38938
|
+
if (files.length) draft.files = files;
|
|
38939
|
+
const links = parsePhoneLinks(value.links);
|
|
38940
|
+
if (links === "invalid") return "invalid";
|
|
38941
|
+
if (links.length) draft.links = links;
|
|
38942
|
+
const prompt = textField(value, "prompt");
|
|
38943
|
+
if (prompt) draft.prompt = prompt.slice(0, 2e4);
|
|
38944
|
+
return draft;
|
|
38945
|
+
}
|
|
38946
|
+
function parsePhoneFiles(value) {
|
|
38947
|
+
if (value == null) return [];
|
|
38948
|
+
if (!Array.isArray(value) || value.length > MAX_PHONE_FILES) return "invalid";
|
|
38949
|
+
const files = [];
|
|
38950
|
+
let total = 0;
|
|
38951
|
+
for (const item of value) {
|
|
38952
|
+
if (!item || typeof item !== "object") return "invalid";
|
|
38953
|
+
const name = (textField(item, "name").replace(/[/\\]/g, "_") || "file").slice(0, 180);
|
|
38954
|
+
const dataBase64 = base64Field(item, "dataBase64");
|
|
38955
|
+
if (!dataBase64 || !/^[A-Za-z0-9+/=]+$/.test(dataBase64)) return "invalid";
|
|
38956
|
+
if (dataBase64.length > MAX_PHONE_FILE_CHARS) return "invalid";
|
|
38957
|
+
total += dataBase64.length;
|
|
38958
|
+
if (total > MAX_PHONE_FILES_CHARS) return "invalid";
|
|
38959
|
+
files.push({ name, dataBase64 });
|
|
38960
|
+
}
|
|
38961
|
+
return files;
|
|
38962
|
+
}
|
|
38963
|
+
function parsePhoneLinks(value) {
|
|
38964
|
+
if (value == null) return [];
|
|
38965
|
+
if (!Array.isArray(value) || value.length > 8) return "invalid";
|
|
38966
|
+
const links = [];
|
|
38967
|
+
for (const item of value) {
|
|
38968
|
+
if (!item || typeof item !== "object") return "invalid";
|
|
38969
|
+
const ref = textField(item, "ref").slice(0, 80);
|
|
38970
|
+
if (!ref) return "invalid";
|
|
38971
|
+
const title = (textField(item, "title") || ref).slice(0, 200);
|
|
38972
|
+
const url = textField(item, "url").slice(0, 500);
|
|
38973
|
+
links.push({ ref, title, ...url ? { url } : {} });
|
|
38974
|
+
}
|
|
38975
|
+
return links;
|
|
38976
|
+
}
|
|
38977
|
+
function parsePhoneCreate(value) {
|
|
38978
|
+
const draft = parsePhoneDraft(value);
|
|
38979
|
+
if (draft === "invalid") return "invalid";
|
|
38980
|
+
const goal = textField(value, "goal") || draft.prompt || "";
|
|
38981
|
+
const where = value.where;
|
|
38982
|
+
if (where == null || where === "orchestration") {
|
|
38983
|
+
return {
|
|
38984
|
+
op: "create",
|
|
38985
|
+
where: "orchestration",
|
|
38986
|
+
...draft,
|
|
38987
|
+
...goal ? { goal, prompt: goal } : {}
|
|
38988
|
+
};
|
|
38989
|
+
}
|
|
38990
|
+
if (where === "worktree") {
|
|
38991
|
+
const chatId = textField(value, "chatId");
|
|
38992
|
+
if (!chatId) return "invalid";
|
|
38993
|
+
return { op: "create", where: "worktree", chatId, ...draft };
|
|
38994
|
+
}
|
|
38995
|
+
if (where === "project") {
|
|
38996
|
+
const repoPath = textField(value, "repoPath");
|
|
38997
|
+
if (!repoPath || !isProjectPath(repoPath)) return "invalid";
|
|
38998
|
+
const sourceRaw = textField(value, "sourceType");
|
|
38999
|
+
let sourceType;
|
|
39000
|
+
if (sourceRaw === "branch" || sourceRaw === "pr" || sourceRaw === "ticket") sourceType = sourceRaw;
|
|
39001
|
+
else if (sourceRaw) return "invalid";
|
|
39002
|
+
const sourceRef = textField(value, "sourceRef");
|
|
39003
|
+
if ((sourceType === "pr" || sourceType === "ticket") && !sourceRef) return "invalid";
|
|
39004
|
+
const title = textField(value, "title").slice(0, 200);
|
|
39005
|
+
const cowboy = value.cowboy === true;
|
|
39006
|
+
return {
|
|
39007
|
+
op: "create",
|
|
39008
|
+
where: "project",
|
|
39009
|
+
repoPath,
|
|
39010
|
+
...draft,
|
|
39011
|
+
...sourceType ? { sourceType } : {},
|
|
39012
|
+
...sourceRef ? { sourceRef } : {},
|
|
39013
|
+
...title ? { title } : {},
|
|
39014
|
+
...cowboy ? { cowboy: true } : {}
|
|
39015
|
+
};
|
|
39016
|
+
}
|
|
39017
|
+
return "invalid";
|
|
39018
|
+
}
|
|
39019
|
+
function phoneVisibleText(role, text6) {
|
|
39020
|
+
const trimmed = text6.trim();
|
|
39021
|
+
if (role === "user" && trimmed.startsWith(PHONE_USER_PREFIX)) {
|
|
39022
|
+
return trimmed.slice(PHONE_USER_PREFIX.length).trim();
|
|
39023
|
+
}
|
|
39024
|
+
return trimmed;
|
|
39025
|
+
}
|
|
39026
|
+
function clip(text6, limit) {
|
|
39027
|
+
if (text6.length <= limit) return text6;
|
|
39028
|
+
return `${text6.slice(0, limit - 1)}\u2026`;
|
|
39029
|
+
}
|
|
39030
|
+
function summarize(thread) {
|
|
39031
|
+
return {
|
|
39032
|
+
id: thread.id,
|
|
39033
|
+
title: thread.title?.trim() || "Untitled",
|
|
39034
|
+
status: thread.status,
|
|
39035
|
+
preview: previewOf(thread),
|
|
39036
|
+
updatedAt: thread.updatedAt
|
|
39037
|
+
};
|
|
39038
|
+
}
|
|
39039
|
+
function previewOf(thread) {
|
|
39040
|
+
const messages = listPhoneMessages(thread);
|
|
39041
|
+
const last = messages[messages.length - 1];
|
|
39042
|
+
if (!last) return "";
|
|
39043
|
+
return clip(last.text.replace(/\s+/g, " ").trim(), PREVIEW_LIMIT);
|
|
39044
|
+
}
|
|
39045
|
+
function listPhoneMessages(thread) {
|
|
39046
|
+
const out = [];
|
|
39047
|
+
for (const message of thread.messages) {
|
|
39048
|
+
if (message.role !== "user" && message.role !== "agent") continue;
|
|
39049
|
+
const names = (message.attachments ?? []).map((item) => item.name.trim()).filter(Boolean);
|
|
39050
|
+
const text6 = clip(
|
|
39051
|
+
[phoneVisibleText(message.role, message.text), names.length ? names.join(", ") : ""].filter(Boolean).join("\n"),
|
|
39052
|
+
MESSAGE_LIMIT
|
|
39053
|
+
);
|
|
39054
|
+
if (!text6) continue;
|
|
39055
|
+
out.push({ role: message.role, text: text6 });
|
|
39056
|
+
}
|
|
39057
|
+
for (const prompt of thread.queue) {
|
|
39058
|
+
const text6 = clip(phoneVisibleText("user", prompt), MESSAGE_LIMIT);
|
|
39059
|
+
if (text6) out.push({ role: "user", text: text6 });
|
|
39060
|
+
}
|
|
39061
|
+
return out.slice(-TRANSCRIPT_LIMIT);
|
|
39062
|
+
}
|
|
39063
|
+
function repoName(repoPath) {
|
|
39064
|
+
const parts = repoPath.replace(/\/$/, "").split("/");
|
|
39065
|
+
return parts[parts.length - 1] || repoPath;
|
|
39066
|
+
}
|
|
39067
|
+
function isProjectPath(path2) {
|
|
39068
|
+
if (!path2 || path2 === "/" || path2 === ".") return false;
|
|
39069
|
+
return !isGlobalThread({ repoPath: path2 });
|
|
39070
|
+
}
|
|
39071
|
+
function listPhoneChats() {
|
|
39072
|
+
return listGlobalThreads().slice().sort((a, b) => a.updatedAt < b.updatedAt ? 1 : a.updatedAt > b.updatedAt ? -1 : 0).map(summarize);
|
|
39073
|
+
}
|
|
39074
|
+
function listPhoneSidebar() {
|
|
39075
|
+
const threads = listThreads();
|
|
39076
|
+
const byRepo = /* @__PURE__ */ new Map();
|
|
39077
|
+
for (const thread of threads) {
|
|
39078
|
+
if (!isProjectPath(thread.repoPath)) continue;
|
|
39079
|
+
const list = byRepo.get(thread.repoPath) ?? [];
|
|
39080
|
+
list.push(thread);
|
|
39081
|
+
byRepo.set(thread.repoPath, list);
|
|
39082
|
+
}
|
|
39083
|
+
for (const workspace of listWorkspaces()) {
|
|
39084
|
+
if (!isProjectPath(workspace.path) || byRepo.has(workspace.path)) continue;
|
|
39085
|
+
byRepo.set(workspace.path, []);
|
|
39086
|
+
}
|
|
39087
|
+
const projects = [...byRepo.entries()].sort(([a], [b]) => repoName(a).localeCompare(repoName(b))).map(([path2, repoThreads]) => ({
|
|
39088
|
+
name: repoName(path2),
|
|
39089
|
+
path: path2,
|
|
39090
|
+
worktrees: groupHomeBoardWorktrees(repoThreads).map((group) => ({
|
|
39091
|
+
label: worktreeDisplayLabelForGroup(group),
|
|
39092
|
+
chats: group.map(summarize)
|
|
39093
|
+
}))
|
|
39094
|
+
}));
|
|
39095
|
+
return { orchestration: listPhoneChats(), projects };
|
|
39096
|
+
}
|
|
39097
|
+
function historyStamp(thread) {
|
|
39098
|
+
return thread.archivedAt || thread.updatedAt;
|
|
39099
|
+
}
|
|
39100
|
+
function historyCursor(chat) {
|
|
39101
|
+
return `${chat.updatedAt} ${chat.id}`;
|
|
39102
|
+
}
|
|
39103
|
+
function historyQueryMatch(chat, query) {
|
|
39104
|
+
if (!query) return true;
|
|
39105
|
+
return `${chat.title} ${chat.where} ${chat.agent} ${chat.preview}`.toLowerCase().includes(query);
|
|
39106
|
+
}
|
|
39107
|
+
function listPhoneHistory(opts) {
|
|
39108
|
+
const query = opts?.query?.trim().toLowerCase() ?? "";
|
|
39109
|
+
const after = opts?.after?.trim() ?? "";
|
|
39110
|
+
const all = listThreads({ includeArchived: true }).filter((thread) => thread.status === "archived").filter((thread) => isGlobalThread(thread) || isProjectPath(thread.repoPath)).sort((a, b) => historyStamp(a) < historyStamp(b) ? 1 : historyStamp(a) > historyStamp(b) ? -1 : 0).map((thread) => ({
|
|
39111
|
+
id: thread.id,
|
|
39112
|
+
title: thread.title?.trim() || "Untitled",
|
|
39113
|
+
preview: previewOf(thread),
|
|
39114
|
+
updatedAt: historyStamp(thread),
|
|
39115
|
+
where: isGlobalThread(thread) ? "Orchestration" : repoName(thread.repoPath),
|
|
39116
|
+
agent: thread.agent
|
|
39117
|
+
})).filter((chat) => historyQueryMatch(chat, query));
|
|
39118
|
+
let start = 0;
|
|
39119
|
+
if (after) {
|
|
39120
|
+
const index = all.findIndex((chat) => historyCursor(chat) === after);
|
|
39121
|
+
start = index >= 0 ? index + 1 : all.length;
|
|
39122
|
+
}
|
|
39123
|
+
const chats = all.slice(start, start + HISTORY_PAGE_SIZE);
|
|
39124
|
+
const last = chats.at(-1);
|
|
39125
|
+
const more = start + chats.length < all.length;
|
|
39126
|
+
return {
|
|
39127
|
+
chats,
|
|
39128
|
+
total: all.length,
|
|
39129
|
+
...more && last ? { next: historyCursor(last) } : {}
|
|
39130
|
+
};
|
|
39131
|
+
}
|
|
39132
|
+
function matchPhoneThread(chatId) {
|
|
39133
|
+
const id = chatId.trim();
|
|
39134
|
+
if (!id) return null;
|
|
39135
|
+
const exact = readThread(id);
|
|
39136
|
+
if (exact) return exact;
|
|
39137
|
+
const match = findThreadByRef(id);
|
|
39138
|
+
if (!match) return null;
|
|
39139
|
+
if (match.id !== id && !match.id.startsWith(id)) return null;
|
|
39140
|
+
return match;
|
|
39141
|
+
}
|
|
39142
|
+
function visiblePhoneThread(thread) {
|
|
39143
|
+
if (isGlobalThread(thread)) return true;
|
|
39144
|
+
return isProjectPath(thread.repoPath);
|
|
39145
|
+
}
|
|
39146
|
+
function findPhoneChat(chatId) {
|
|
39147
|
+
const thread = matchPhoneThread(chatId);
|
|
39148
|
+
if (!thread || !visiblePhoneThread(thread)) return null;
|
|
39149
|
+
return thread;
|
|
39150
|
+
}
|
|
39151
|
+
function readPhoneThread(chatId) {
|
|
39152
|
+
const thread = findPhoneChat(chatId);
|
|
39153
|
+
if (!thread || thread.status === "archived") return null;
|
|
39154
|
+
return thread;
|
|
39155
|
+
}
|
|
39156
|
+
function phoneComposerOptions(thread) {
|
|
39157
|
+
return {
|
|
39158
|
+
agent: thread.agent,
|
|
39159
|
+
model: thread.model ?? null,
|
|
39160
|
+
effort: thread.effort,
|
|
39161
|
+
fast: Boolean(thread.fast),
|
|
39162
|
+
planMode: Boolean(thread.planMode),
|
|
39163
|
+
autonomy: thread.autonomy ?? "default"
|
|
39164
|
+
};
|
|
39165
|
+
}
|
|
39166
|
+
function phoneTurnAttachments(draft) {
|
|
39167
|
+
const links = (draft.links ?? []).map((link) => ({
|
|
39168
|
+
id: (0, import_node_crypto25.randomUUID)(),
|
|
39169
|
+
name: link.ref,
|
|
39170
|
+
kind: "issue",
|
|
39171
|
+
content: [`Linked issue: ${link.ref} \u2014 ${link.title}`, link.url ? `URL: ${link.url}` : null].filter(Boolean).join("\n")
|
|
39172
|
+
}));
|
|
39173
|
+
const files = (draft.files ?? []).map((file) => ({
|
|
39174
|
+
id: (0, import_node_crypto25.randomUUID)(),
|
|
39175
|
+
name: file.name,
|
|
39176
|
+
kind: "file",
|
|
39177
|
+
previewDataUrl: `data:application/octet-stream;base64,${file.dataBase64}`,
|
|
39178
|
+
content: `File attached: ${file.name}`
|
|
39179
|
+
}));
|
|
39180
|
+
return [...links, ...files];
|
|
39181
|
+
}
|
|
39182
|
+
function openedFrom(thread, place) {
|
|
39183
|
+
const fresh = readThread(thread.id) ?? thread;
|
|
39184
|
+
return {
|
|
39185
|
+
chat: summarize(fresh),
|
|
39186
|
+
messages: listPhoneMessages(fresh),
|
|
39187
|
+
place,
|
|
39188
|
+
options: phoneComposerOptions(fresh)
|
|
39189
|
+
};
|
|
39190
|
+
}
|
|
39191
|
+
function openPhoneChat(chatId) {
|
|
39192
|
+
const thread = readPhoneThread(chatId);
|
|
39193
|
+
if (!thread) return null;
|
|
39194
|
+
const place = isGlobalThread(thread) ? { kind: "orchestration" } : projectPlace(thread);
|
|
39195
|
+
return openedFrom(thread, place);
|
|
39196
|
+
}
|
|
39197
|
+
function sameRepoPath3(a, b) {
|
|
39198
|
+
return a.replace(/\/+$/, "") === b.replace(/\/+$/, "");
|
|
39199
|
+
}
|
|
39200
|
+
function projectPlace(thread) {
|
|
39201
|
+
const group = threadsSharingWorktree(thread.worktreePath);
|
|
39202
|
+
return {
|
|
39203
|
+
kind: "project",
|
|
39204
|
+
repoPath: thread.repoPath,
|
|
39205
|
+
worktree: worktreeDisplayLabelForGroup(group.length > 0 ? group : [thread])
|
|
39206
|
+
};
|
|
39207
|
+
}
|
|
39208
|
+
function assertKnownProject(repoPath) {
|
|
39209
|
+
const path2 = repoPath.trim();
|
|
39210
|
+
if (!isProjectPath(path2)) throw new Error("Pick a project on this Mac.");
|
|
39211
|
+
const known = listWorkspaces().some((workspace) => sameRepoPath3(workspace.path, path2)) || listThreads().some(
|
|
39212
|
+
(thread) => thread.status !== "archived" && sameRepoPath3(thread.repoPath, path2)
|
|
39213
|
+
);
|
|
39214
|
+
if (!known) throw new Error("That project is not on this Mac.");
|
|
39215
|
+
return path2;
|
|
39216
|
+
}
|
|
39217
|
+
function draftAgentOptions(draft, defaults) {
|
|
39218
|
+
return {
|
|
39219
|
+
agent: draft?.agent ?? defaults.agent,
|
|
39220
|
+
model: draft && "model" in draft ? draft.model ?? null : defaults.model,
|
|
39221
|
+
effort: draft?.effort ?? defaults.effort,
|
|
39222
|
+
fast: draft?.fast ?? defaults.fast,
|
|
39223
|
+
planMode: Boolean(draft?.planMode),
|
|
39224
|
+
autonomy: draft?.autonomy
|
|
39225
|
+
};
|
|
39226
|
+
}
|
|
39227
|
+
function createPhoneChat(agent) {
|
|
39228
|
+
const defaults = resolveOrchestratorDefaults();
|
|
39229
|
+
const thread = createGlobalChat({
|
|
39230
|
+
agent: agent ?? defaults.agent,
|
|
39231
|
+
model: defaults.model,
|
|
39232
|
+
effort: defaults.effort,
|
|
39233
|
+
fast: defaults.fast
|
|
39234
|
+
});
|
|
39235
|
+
return openedFrom(thread, { kind: "orchestration" });
|
|
39236
|
+
}
|
|
39237
|
+
async function createPhoneOrchestration(draft) {
|
|
39238
|
+
const goal = (draft?.goal ?? draft?.prompt ?? "").trim();
|
|
39239
|
+
const attachments = phoneTurnAttachments(draft ?? {});
|
|
39240
|
+
if (!goal && attachments.length === 0) return createPhoneChat(draft?.agent);
|
|
39241
|
+
const defaults = resolveOrchestratorDefaults();
|
|
39242
|
+
const options = draftAgentOptions(draft, defaults);
|
|
39243
|
+
const thread = await startOrchestration({
|
|
39244
|
+
goal: goal || "See the attached files.",
|
|
39245
|
+
agent: options.agent,
|
|
39246
|
+
model: options.model,
|
|
39247
|
+
effort: options.effort,
|
|
39248
|
+
fast: options.fast,
|
|
39249
|
+
planMode: options.planMode,
|
|
39250
|
+
autonomy: options.autonomy,
|
|
39251
|
+
attachments
|
|
39252
|
+
});
|
|
39253
|
+
return openedFrom(thread, { kind: "orchestration" });
|
|
39254
|
+
}
|
|
39255
|
+
function createPhoneWorktreeAgent(fromChatId, draft) {
|
|
39256
|
+
const source = readPhoneThread(fromChatId);
|
|
39257
|
+
if (!source || isGlobalThread(source)) throw new Error("That worktree is not on this Mac.");
|
|
39258
|
+
const defaults = resolveNewThreadOptions();
|
|
39259
|
+
const options = draftAgentOptions(draft, defaults);
|
|
39260
|
+
const attachments = persistPendingFileAttachments(
|
|
39261
|
+
source.worktreePath,
|
|
39262
|
+
phoneTurnAttachments(draft ?? {})
|
|
39263
|
+
);
|
|
39264
|
+
const thread = createChatTab({
|
|
39265
|
+
fromThreadId: source.id,
|
|
39266
|
+
agent: options.agent,
|
|
39267
|
+
model: options.model,
|
|
39268
|
+
effort: options.effort,
|
|
39269
|
+
fast: options.fast,
|
|
39270
|
+
autonomy: options.autonomy,
|
|
39271
|
+
attachments
|
|
39272
|
+
});
|
|
39273
|
+
if (draft?.planMode !== void 0) updateThread(thread.id, { planMode: draft.planMode });
|
|
39274
|
+
return openedFrom(thread, projectPlace(readThread(thread.id) ?? thread));
|
|
39275
|
+
}
|
|
39276
|
+
async function createPhoneProjectWorktree(repoPathOrInput) {
|
|
39277
|
+
const input = typeof repoPathOrInput === "string" ? { repoPath: repoPathOrInput } : repoPathOrInput;
|
|
39278
|
+
const path2 = assertKnownProject(input.repoPath);
|
|
39279
|
+
const defaults = resolveNewThreadOptions();
|
|
39280
|
+
const options = draftAgentOptions(input, defaults);
|
|
39281
|
+
const thread = await createThread({
|
|
39282
|
+
sourceType: input.sourceType ?? "branch",
|
|
39283
|
+
sourceRef: input.sourceRef || "default",
|
|
39284
|
+
repoPath: path2,
|
|
39285
|
+
title: input.title,
|
|
39286
|
+
cowboy: input.cowboy,
|
|
39287
|
+
agent: options.agent,
|
|
39288
|
+
model: options.model,
|
|
39289
|
+
effort: options.effort,
|
|
39290
|
+
fast: options.fast,
|
|
39291
|
+
planMode: options.planMode,
|
|
39292
|
+
autonomy: options.autonomy,
|
|
39293
|
+
attachments: phoneTurnAttachments(input),
|
|
39294
|
+
prompt: input.prompt,
|
|
39295
|
+
reuseExisting: false
|
|
39296
|
+
});
|
|
39297
|
+
return openedFrom(thread, projectPlace(thread));
|
|
39298
|
+
}
|
|
39299
|
+
async function listPhoneModels(agent) {
|
|
39300
|
+
const catalogs = await listModelsForAgent(agent);
|
|
39301
|
+
return (catalogs[0]?.models ?? []).slice(0, 40).map((model) => ({
|
|
39302
|
+
id: model.id,
|
|
39303
|
+
label: model.displayName || model.id
|
|
39304
|
+
}));
|
|
39305
|
+
}
|
|
39306
|
+
function archivePhoneChat(chatId) {
|
|
39307
|
+
const thread = readPhoneThread(chatId);
|
|
39308
|
+
if (!thread) return false;
|
|
39309
|
+
updateThread(thread.id, { status: "archived" });
|
|
39310
|
+
return true;
|
|
39311
|
+
}
|
|
39312
|
+
|
|
39313
|
+
// src/remote/phone-sources.ts
|
|
39314
|
+
init_worktree();
|
|
39315
|
+
function sourceWarning(label, err) {
|
|
39316
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
39317
|
+
return message ? `${label}: ${message}` : `${label} failed.`;
|
|
39318
|
+
}
|
|
39319
|
+
async function listPhoneSources(repoPath, query) {
|
|
39320
|
+
const path2 = assertKnownProject(repoPath);
|
|
39321
|
+
const q = query?.trim() ?? "";
|
|
39322
|
+
const warnings = [];
|
|
39323
|
+
const [prs, branches, issues] = await Promise.all([
|
|
39324
|
+
listPrs(path2, { state: "open", limit: 40, ...q ? { query: q } : {} }).catch((err) => {
|
|
39325
|
+
warnings.push(sourceWarning("Pull requests", err));
|
|
39326
|
+
return [];
|
|
39327
|
+
}),
|
|
39328
|
+
listBranches(path2).catch((err) => {
|
|
39329
|
+
warnings.push(sourceWarning("Branches", err));
|
|
39330
|
+
return [];
|
|
39331
|
+
}),
|
|
39332
|
+
listIssues(path2, { limit: 40, ...q ? { query: q } : {} }).catch((err) => {
|
|
39333
|
+
warnings.push(sourceWarning("Issues", err));
|
|
39334
|
+
return { issues: [] };
|
|
39335
|
+
})
|
|
39336
|
+
]);
|
|
39337
|
+
const needle = q.toLowerCase();
|
|
39338
|
+
const branchRows = branches.filter((branch) => !needle || branch.name.toLowerCase().includes(needle)).slice(0, 50).map((branch) => ({
|
|
39339
|
+
ref: branch.name,
|
|
39340
|
+
title: branch.current ? `${branch.name} (current)` : branch.name
|
|
39341
|
+
}));
|
|
39342
|
+
return {
|
|
39343
|
+
prs: prs.slice(0, 40).map((pr) => ({
|
|
39344
|
+
ref: String(pr.number),
|
|
39345
|
+
title: clip(pr.title || `PR #${pr.number}`, 140),
|
|
39346
|
+
...pr.url ? { url: pr.url } : {}
|
|
39347
|
+
})),
|
|
39348
|
+
branches: branchRows,
|
|
39349
|
+
issues: (issues.issues ?? []).slice(0, 40).map((issue) => ({
|
|
39350
|
+
ref: issue.identifier,
|
|
39351
|
+
title: clip(issue.title || issue.identifier, 140),
|
|
39352
|
+
...issue.url ? { url: issue.url } : {}
|
|
39353
|
+
})),
|
|
39354
|
+
...warnings.length ? { warnings } : {}
|
|
39355
|
+
};
|
|
39356
|
+
}
|
|
39357
|
+
|
|
39358
|
+
// src/remote/phone-live.ts
|
|
39359
|
+
init_orchestrator();
|
|
39360
|
+
init_thread_store();
|
|
39361
|
+
|
|
39362
|
+
// src/remote/phone-stream.ts
|
|
39363
|
+
init_message_parts();
|
|
39364
|
+
var PHONE_STREAM_FLUSH_MS = 80;
|
|
39365
|
+
function phoneStreamFrame(parts) {
|
|
39366
|
+
return {
|
|
39367
|
+
text: visibleAssistantText(void 0, parts),
|
|
39368
|
+
activity: liveActivitySummary(parts)
|
|
39369
|
+
};
|
|
39370
|
+
}
|
|
39371
|
+
var SKIPPED = /* @__PURE__ */ new Set(["session_id", "usage", "stderr", "exit"]);
|
|
39372
|
+
function phoneStreamParts(parts, event) {
|
|
39373
|
+
if (SKIPPED.has(event.type)) return null;
|
|
39374
|
+
return applyAgentEvent(parts, event);
|
|
39375
|
+
}
|
|
39376
|
+
function phoneStreamFlushNow(event) {
|
|
39377
|
+
return event.type === "tool_use" || event.type === "tool_result" && !event.partial;
|
|
39378
|
+
}
|
|
39379
|
+
|
|
39380
|
+
// src/remote/phone-live.ts
|
|
38760
39381
|
var REMOTE_STOPPED_REPLY = "Sideboard stopped the in-progress turn.";
|
|
39382
|
+
var chatChains = /* @__PURE__ */ new Map();
|
|
39383
|
+
var chatGeneration = /* @__PURE__ */ new Map();
|
|
39384
|
+
var phoneTurnGen = /* @__PURE__ */ new Map();
|
|
39385
|
+
function bumpChat(chatId) {
|
|
39386
|
+
const next = (chatGeneration.get(chatId) ?? 0) + 1;
|
|
39387
|
+
chatGeneration.set(chatId, next);
|
|
39388
|
+
return next;
|
|
39389
|
+
}
|
|
39390
|
+
function finishPhoneTurn(chatId, generation) {
|
|
39391
|
+
if (phoneTurnGen.get(chatId) === generation) phoneTurnGen.delete(chatId);
|
|
39392
|
+
}
|
|
39393
|
+
function enqueueChat(chatId, fn) {
|
|
39394
|
+
const prev = chatChains.get(chatId) ?? Promise.resolve();
|
|
39395
|
+
const run2 = prev.then(fn, fn);
|
|
39396
|
+
chatChains.set(
|
|
39397
|
+
chatId,
|
|
39398
|
+
run2.then(
|
|
39399
|
+
() => void 0,
|
|
39400
|
+
() => void 0
|
|
39401
|
+
)
|
|
39402
|
+
);
|
|
39403
|
+
}
|
|
39404
|
+
function emitControl(opts, reply) {
|
|
39405
|
+
opts.onOutbound({ type: "assistant", text: encodePhoneControl(reply) });
|
|
39406
|
+
}
|
|
39407
|
+
function watchPhoneStream(opts, chatId, threadId, generation) {
|
|
39408
|
+
let parts = [];
|
|
39409
|
+
let timer = null;
|
|
39410
|
+
let lastKey = "";
|
|
39411
|
+
let closed = false;
|
|
39412
|
+
const alive = () => !closed && chatGeneration.get(chatId) === generation;
|
|
39413
|
+
const flush2 = () => {
|
|
39414
|
+
timer = null;
|
|
39415
|
+
if (!alive()) return;
|
|
39416
|
+
const frame = phoneStreamFrame(parts);
|
|
39417
|
+
const key = `${frame.activity}
|
|
39418
|
+
${frame.text}`;
|
|
39419
|
+
if (key === lastKey) return;
|
|
39420
|
+
if (!frame.text && !frame.activity) return;
|
|
39421
|
+
lastKey = key;
|
|
39422
|
+
emitControl(opts, { op: "stream", chatId, text: frame.text, activity: frame.activity });
|
|
39423
|
+
};
|
|
39424
|
+
const schedule = (event) => {
|
|
39425
|
+
if (phoneStreamFlushNow(event) || lastKey === "") {
|
|
39426
|
+
if (timer) {
|
|
39427
|
+
clearTimeout(timer);
|
|
39428
|
+
timer = null;
|
|
39429
|
+
}
|
|
39430
|
+
flush2();
|
|
39431
|
+
return;
|
|
39432
|
+
}
|
|
39433
|
+
if (timer) return;
|
|
39434
|
+
timer = setTimeout(flush2, PHONE_STREAM_FLUSH_MS);
|
|
39435
|
+
};
|
|
39436
|
+
const off = getOrchestrator().on((event) => {
|
|
39437
|
+
if (!alive()) return;
|
|
39438
|
+
if (event.type !== "turn_output" || event.threadId !== threadId) return;
|
|
39439
|
+
const next = phoneStreamParts(parts, event.event);
|
|
39440
|
+
if (!next) return;
|
|
39441
|
+
parts = next;
|
|
39442
|
+
schedule(event.event);
|
|
39443
|
+
});
|
|
39444
|
+
return () => {
|
|
39445
|
+
closed = true;
|
|
39446
|
+
if (timer) clearTimeout(timer);
|
|
39447
|
+
off();
|
|
39448
|
+
};
|
|
39449
|
+
}
|
|
39450
|
+
function interruptChat(chatId, log) {
|
|
39451
|
+
try {
|
|
39452
|
+
const thread = readPhoneThread(chatId);
|
|
39453
|
+
if (!thread) return false;
|
|
39454
|
+
if (thread.status !== "running" && thread.status !== "queued") return false;
|
|
39455
|
+
getOrchestrator().stop(thread.id, { clearQueue: true });
|
|
39456
|
+
log(`interrupt phone \u2192 chat ${thread.id.slice(0, 8)} (${thread.status})`);
|
|
39457
|
+
return true;
|
|
39458
|
+
} catch (err) {
|
|
39459
|
+
log(`interrupt chat: ${err instanceof Error ? err.message : String(err)}`);
|
|
39460
|
+
return false;
|
|
39461
|
+
}
|
|
39462
|
+
}
|
|
39463
|
+
function emitChatTurn(opts, chatId, threadId) {
|
|
39464
|
+
const thread = readThread(threadId);
|
|
39465
|
+
if (!thread) {
|
|
39466
|
+
emitControl(opts, { op: "error", chatId, message: "That agent is not on this Mac." });
|
|
39467
|
+
return;
|
|
39468
|
+
}
|
|
39469
|
+
if (thread.status === "stopped") {
|
|
39470
|
+
emitControl(opts, { op: "stopped", chatId });
|
|
39471
|
+
return;
|
|
39472
|
+
}
|
|
39473
|
+
const result = getOrchestrator().getTurnResult(thread.id);
|
|
39474
|
+
const pending = extractPendingPlanQuestions(
|
|
39475
|
+
[...thread.messages].reverse().find((m) => m.role === "agent")?.parts
|
|
39476
|
+
);
|
|
39477
|
+
if (result.taskState === "input-required" && pending) {
|
|
39478
|
+
emitControl(opts, {
|
|
39479
|
+
op: "ask",
|
|
39480
|
+
chatId,
|
|
39481
|
+
text: result.text.trim(),
|
|
39482
|
+
questions: pending.questions.map((q) => ({
|
|
39483
|
+
question: q.question,
|
|
39484
|
+
options: q.options.map(
|
|
39485
|
+
(o) => o.description ? { label: o.label, description: o.description } : { label: o.label }
|
|
39486
|
+
)
|
|
39487
|
+
}))
|
|
39488
|
+
});
|
|
39489
|
+
return;
|
|
39490
|
+
}
|
|
39491
|
+
const reply = outboundReplyFromTurn(result, {
|
|
39492
|
+
inputRequired: "Sideboard is waiting for an answer.",
|
|
39493
|
+
canceled: REMOTE_STOPPED_REPLY,
|
|
39494
|
+
completedEmpty: () => "",
|
|
39495
|
+
failed: (detail) => detail ? `Sideboard failed: ${detail}` : "Sideboard failed before producing a result."
|
|
39496
|
+
});
|
|
39497
|
+
emitControl(opts, { op: "assistant", chatId, text: reply.trim() });
|
|
39498
|
+
}
|
|
39499
|
+
function phoneError(err) {
|
|
39500
|
+
return err instanceof Error ? err.message : String(err);
|
|
39501
|
+
}
|
|
39502
|
+
function deliverPhoneTurn(opts, chatId) {
|
|
39503
|
+
const thread = readPhoneThread(chatId);
|
|
39504
|
+
if (!thread || thread.status !== "running" && thread.status !== "queued") return;
|
|
39505
|
+
const id = thread.id;
|
|
39506
|
+
const claimed = phoneTurnGen.get(id);
|
|
39507
|
+
if (claimed != null && claimed === chatGeneration.get(id)) return;
|
|
39508
|
+
const generation = bumpChat(id);
|
|
39509
|
+
phoneTurnGen.set(id, generation);
|
|
39510
|
+
enqueueChat(id, async () => {
|
|
39511
|
+
if (chatGeneration.get(id) !== generation) return;
|
|
39512
|
+
const stopStream = watchPhoneStream(opts, id, id, generation);
|
|
39513
|
+
try {
|
|
39514
|
+
await getOrchestrator().waitForTurn(id, 14 * 60 * 1e3);
|
|
39515
|
+
} catch (err) {
|
|
39516
|
+
stopStream();
|
|
39517
|
+
finishPhoneTurn(id, generation);
|
|
39518
|
+
if (chatGeneration.get(id) !== generation) return;
|
|
39519
|
+
emitControl(opts, { op: "error", chatId: id, message: `Sideboard failed: ${phoneError(err)}` });
|
|
39520
|
+
return;
|
|
39521
|
+
}
|
|
39522
|
+
stopStream();
|
|
39523
|
+
finishPhoneTurn(id, generation);
|
|
39524
|
+
if (chatGeneration.get(id) !== generation) return;
|
|
39525
|
+
emitChatTurn(opts, id, id);
|
|
39526
|
+
});
|
|
39527
|
+
}
|
|
39528
|
+
|
|
39529
|
+
// src/remote/listen.ts
|
|
38761
39530
|
function formatRemotePrompt(text6) {
|
|
38762
39531
|
return `Phone
|
|
38763
39532
|
|
|
@@ -38789,10 +39558,312 @@ function interruptRemoteCoordinator(deviceId, log = () => void 0) {
|
|
|
38789
39558
|
return false;
|
|
38790
39559
|
}
|
|
38791
39560
|
}
|
|
39561
|
+
function publishOpenedChat(opts, chatId) {
|
|
39562
|
+
const opened = openPhoneChat(chatId);
|
|
39563
|
+
if (!opened) {
|
|
39564
|
+
emitControl(opts, { op: "error", message: "That agent is not on this Mac." });
|
|
39565
|
+
return;
|
|
39566
|
+
}
|
|
39567
|
+
emitControl(opts, { op: "opened", ...opened });
|
|
39568
|
+
if (opened.chat.status === "running" || opened.chat.status === "queued") {
|
|
39569
|
+
deliverPhoneTurn(opts, opened.chat.id);
|
|
39570
|
+
}
|
|
39571
|
+
}
|
|
39572
|
+
function applyPhoneOptions(chatId, draft) {
|
|
39573
|
+
const patch = {};
|
|
39574
|
+
if (draft.agent) patch.agent = draft.agent;
|
|
39575
|
+
if ("model" in draft) patch.model = draft.model ?? null;
|
|
39576
|
+
if (draft.effort) patch.effort = draft.effort;
|
|
39577
|
+
if (draft.fast !== void 0) patch.fast = draft.fast;
|
|
39578
|
+
if (draft.planMode !== void 0) patch.planMode = draft.planMode;
|
|
39579
|
+
if (draft.autonomy) patch.autonomy = draft.autonomy;
|
|
39580
|
+
if (Object.keys(patch).length === 0) return;
|
|
39581
|
+
getOrchestrator().setThreadOptions(chatId, patch);
|
|
39582
|
+
}
|
|
39583
|
+
function stagePhoneTurnFiles(worktreePath, draft) {
|
|
39584
|
+
const links = phoneTurnAttachments({ links: draft.links });
|
|
39585
|
+
const files = stageBuffersAsAttachments(
|
|
39586
|
+
worktreePath,
|
|
39587
|
+
(draft.files ?? []).map((file) => ({ name: file.name, dataBase64: file.dataBase64 }))
|
|
39588
|
+
);
|
|
39589
|
+
return [...links, ...files];
|
|
39590
|
+
}
|
|
39591
|
+
function handlePhoneControl(cmd, opts) {
|
|
39592
|
+
const log = opts.onLog ?? (() => void 0);
|
|
39593
|
+
if (cmd.op === "list") {
|
|
39594
|
+
emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
|
|
39595
|
+
return;
|
|
39596
|
+
}
|
|
39597
|
+
if (cmd.op === "history") {
|
|
39598
|
+
const page = listPhoneHistory({ query: cmd.query, after: cmd.after });
|
|
39599
|
+
emitControl(opts, {
|
|
39600
|
+
op: "history",
|
|
39601
|
+
chats: page.chats,
|
|
39602
|
+
total: page.total,
|
|
39603
|
+
...page.next ? { next: page.next } : {},
|
|
39604
|
+
...cmd.query ? { query: cmd.query } : {},
|
|
39605
|
+
...cmd.after ? { after: cmd.after } : {}
|
|
39606
|
+
});
|
|
39607
|
+
return;
|
|
39608
|
+
}
|
|
39609
|
+
if (cmd.op === "sources") {
|
|
39610
|
+
const repoPath = cmd.repoPath;
|
|
39611
|
+
const query = cmd.query;
|
|
39612
|
+
void listPhoneSources(repoPath, query).then(
|
|
39613
|
+
(page) => emitControl(opts, {
|
|
39614
|
+
op: "sources",
|
|
39615
|
+
repoPath,
|
|
39616
|
+
...query ? { query } : {},
|
|
39617
|
+
...page
|
|
39618
|
+
})
|
|
39619
|
+
).catch((err) => {
|
|
39620
|
+
emitControl(opts, { op: "error", message: phoneError(err) });
|
|
39621
|
+
});
|
|
39622
|
+
return;
|
|
39623
|
+
}
|
|
39624
|
+
if (cmd.op === "models") {
|
|
39625
|
+
const agent = cmd.agent;
|
|
39626
|
+
void listPhoneModels(agent).then((models) => emitControl(opts, { op: "models", agent, models })).catch((err) => {
|
|
39627
|
+
emitControl(opts, { op: "error", message: phoneError(err) });
|
|
39628
|
+
});
|
|
39629
|
+
return;
|
|
39630
|
+
}
|
|
39631
|
+
if (cmd.op === "dictate") {
|
|
39632
|
+
const id = cmd.id;
|
|
39633
|
+
const transcribe = opts.transcribeWav;
|
|
39634
|
+
if (!transcribe) {
|
|
39635
|
+
emitControl(opts, { op: "error", message: "Speech is not available on this Mac." });
|
|
39636
|
+
return;
|
|
39637
|
+
}
|
|
39638
|
+
void transcribe(cmd.audioBase64).then((text6) => emitControl(opts, { op: "dictated", id, text: text6.trim() })).catch((err) => {
|
|
39639
|
+
emitControl(opts, { op: "error", message: phoneError(err) });
|
|
39640
|
+
});
|
|
39641
|
+
return;
|
|
39642
|
+
}
|
|
39643
|
+
if (cmd.op === "options") {
|
|
39644
|
+
const thread = readPhoneThread(cmd.chatId);
|
|
39645
|
+
if (!thread) {
|
|
39646
|
+
emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
|
|
39647
|
+
return;
|
|
39648
|
+
}
|
|
39649
|
+
try {
|
|
39650
|
+
applyPhoneOptions(thread.id, cmd);
|
|
39651
|
+
const fresh = readPhoneThread(thread.id) ?? thread;
|
|
39652
|
+
emitControl(opts, { op: "options", chatId: fresh.id, ...phoneComposerOptions(fresh) });
|
|
39653
|
+
} catch (err) {
|
|
39654
|
+
emitControl(opts, { op: "error", chatId: cmd.chatId, message: phoneError(err) });
|
|
39655
|
+
}
|
|
39656
|
+
return;
|
|
39657
|
+
}
|
|
39658
|
+
if (cmd.op === "create") {
|
|
39659
|
+
if (cmd.where === "worktree") {
|
|
39660
|
+
void (async () => {
|
|
39661
|
+
try {
|
|
39662
|
+
const opened = createPhoneWorktreeAgent(cmd.chatId, cmd);
|
|
39663
|
+
const prompt = cmd.prompt?.trim() || (cmd.files?.length || cmd.links?.length ? "See the attached files." : "");
|
|
39664
|
+
if (prompt) {
|
|
39665
|
+
try {
|
|
39666
|
+
await getOrchestrator().send(opened.chat.id, formatRemotePrompt(prompt));
|
|
39667
|
+
} catch (err) {
|
|
39668
|
+
emitControl(opts, { op: "error", chatId: opened.chat.id, message: phoneError(err) });
|
|
39669
|
+
}
|
|
39670
|
+
}
|
|
39671
|
+
const fresh = openPhoneChat(opened.chat.id) ?? opened;
|
|
39672
|
+
emitControl(opts, { op: "opened", ...fresh });
|
|
39673
|
+
if (prompt) deliverPhoneTurn(opts, fresh.chat.id);
|
|
39674
|
+
} catch (err) {
|
|
39675
|
+
emitControl(opts, { op: "error", message: phoneError(err) });
|
|
39676
|
+
}
|
|
39677
|
+
})();
|
|
39678
|
+
return;
|
|
39679
|
+
}
|
|
39680
|
+
if (cmd.where === "project") {
|
|
39681
|
+
void createPhoneProjectWorktree(cmd).then(async (opened) => {
|
|
39682
|
+
const prompt = resolveCreateFirstPrompt({
|
|
39683
|
+
sourceType: cmd.sourceType ?? "branch",
|
|
39684
|
+
prompt: cmd.prompt
|
|
39685
|
+
}) || (cmd.files?.length || cmd.links?.length ? "See the attached files." : "");
|
|
39686
|
+
if (prompt) {
|
|
39687
|
+
try {
|
|
39688
|
+
await getOrchestrator().send(opened.chat.id, formatRemotePrompt(prompt));
|
|
39689
|
+
} catch (err) {
|
|
39690
|
+
emitControl(opts, { op: "error", chatId: opened.chat.id, message: phoneError(err) });
|
|
39691
|
+
}
|
|
39692
|
+
}
|
|
39693
|
+
const fresh = openPhoneChat(opened.chat.id) ?? opened;
|
|
39694
|
+
emitControl(opts, { op: "opened", ...fresh });
|
|
39695
|
+
if (prompt) deliverPhoneTurn(opts, fresh.chat.id);
|
|
39696
|
+
}).catch((err) => {
|
|
39697
|
+
emitControl(opts, { op: "error", message: phoneError(err) });
|
|
39698
|
+
});
|
|
39699
|
+
return;
|
|
39700
|
+
}
|
|
39701
|
+
const goal = cmd.goal?.trim() || cmd.prompt?.trim() || "";
|
|
39702
|
+
if (goal || cmd.files?.length || cmd.links?.length) {
|
|
39703
|
+
void createPhoneOrchestration({ ...cmd, goal }).then((opened) => {
|
|
39704
|
+
emitControl(opts, { op: "opened", ...opened });
|
|
39705
|
+
deliverPhoneTurn(opts, opened.chat.id);
|
|
39706
|
+
}).catch((err) => {
|
|
39707
|
+
emitControl(opts, { op: "error", message: phoneError(err) });
|
|
39708
|
+
});
|
|
39709
|
+
return;
|
|
39710
|
+
}
|
|
39711
|
+
try {
|
|
39712
|
+
const agent = coerceOrchestratorAgent(
|
|
39713
|
+
cmd.agent ?? opts.agent ?? resolveOrchestratorDefaults().agent
|
|
39714
|
+
);
|
|
39715
|
+
emitControl(opts, { op: "opened", ...createPhoneChat(agent) });
|
|
39716
|
+
} catch (err) {
|
|
39717
|
+
emitControl(opts, { op: "error", message: phoneError(err) });
|
|
39718
|
+
}
|
|
39719
|
+
return;
|
|
39720
|
+
}
|
|
39721
|
+
if (cmd.op === "open") {
|
|
39722
|
+
const thread = findPhoneChat(cmd.chatId);
|
|
39723
|
+
if (!thread) {
|
|
39724
|
+
emitControl(opts, { op: "error", message: "That agent is not on this Mac." });
|
|
39725
|
+
return;
|
|
39726
|
+
}
|
|
39727
|
+
if (thread.status === "archived") {
|
|
39728
|
+
void getOrchestrator().restore(thread.id).then(() => {
|
|
39729
|
+
emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
|
|
39730
|
+
publishOpenedChat(opts, thread.id);
|
|
39731
|
+
}).catch((err) => {
|
|
39732
|
+
emitControl(opts, { op: "error", message: phoneError(err) });
|
|
39733
|
+
});
|
|
39734
|
+
return;
|
|
39735
|
+
}
|
|
39736
|
+
publishOpenedChat(opts, thread.id);
|
|
39737
|
+
return;
|
|
39738
|
+
}
|
|
39739
|
+
if (cmd.op === "archive") {
|
|
39740
|
+
const thread = readPhoneThread(cmd.chatId);
|
|
39741
|
+
if (!thread) {
|
|
39742
|
+
emitControl(opts, { op: "error", message: "That agent is not on this Mac." });
|
|
39743
|
+
return;
|
|
39744
|
+
}
|
|
39745
|
+
bumpChat(thread.id);
|
|
39746
|
+
interruptChat(thread.id, log);
|
|
39747
|
+
archivePhoneChat(thread.id);
|
|
39748
|
+
emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
|
|
39749
|
+
return;
|
|
39750
|
+
}
|
|
39751
|
+
if (cmd.op === "restore") {
|
|
39752
|
+
const chatId2 = cmd.chatId;
|
|
39753
|
+
void getOrchestrator().restore(chatId2).then(() => {
|
|
39754
|
+
emitControl(opts, { op: "sidebar", ...listPhoneSidebar() });
|
|
39755
|
+
emitControl(opts, { op: "restored", chatId: chatId2 });
|
|
39756
|
+
}).catch((err) => {
|
|
39757
|
+
emitControl(opts, {
|
|
39758
|
+
op: "error",
|
|
39759
|
+
chatId: chatId2,
|
|
39760
|
+
message: err instanceof Error ? err.message : String(err)
|
|
39761
|
+
});
|
|
39762
|
+
});
|
|
39763
|
+
return;
|
|
39764
|
+
}
|
|
39765
|
+
if (cmd.op === "media") {
|
|
39766
|
+
const thread = readPhoneThread(cmd.chatId);
|
|
39767
|
+
const dataUrl = thread ? phoneImageDataUrl(thread.worktreePath, cmd.path) : null;
|
|
39768
|
+
emitControl(opts, {
|
|
39769
|
+
op: "media",
|
|
39770
|
+
chatId: cmd.chatId,
|
|
39771
|
+
path: cmd.path,
|
|
39772
|
+
...dataUrl ? { dataUrl } : {}
|
|
39773
|
+
});
|
|
39774
|
+
return;
|
|
39775
|
+
}
|
|
39776
|
+
if (cmd.op === "open-file") {
|
|
39777
|
+
const thread = readPhoneThread(cmd.chatId);
|
|
39778
|
+
const relative = thread ? phoneWorktreeRelative(thread.worktreePath, cmd.path) : null;
|
|
39779
|
+
if (!thread || !relative) {
|
|
39780
|
+
emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That file is not in this worktree." });
|
|
39781
|
+
return;
|
|
39782
|
+
}
|
|
39783
|
+
void getOrchestrator().statPath(thread.id, relative).then((kind) => {
|
|
39784
|
+
if (kind === "missing") {
|
|
39785
|
+
emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That file is not in this worktree." });
|
|
39786
|
+
return;
|
|
39787
|
+
}
|
|
39788
|
+
opts.onOpenWorktreeFile?.({
|
|
39789
|
+
threadId: thread.id,
|
|
39790
|
+
path: relative,
|
|
39791
|
+
...kind === "dir" ? { directory: true } : {},
|
|
39792
|
+
...cmd.startLine != null ? { startLine: cmd.startLine } : {},
|
|
39793
|
+
...cmd.endLine != null ? { endLine: cmd.endLine } : {}
|
|
39794
|
+
});
|
|
39795
|
+
}).catch(() => {
|
|
39796
|
+
emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That file is not in this worktree." });
|
|
39797
|
+
});
|
|
39798
|
+
return;
|
|
39799
|
+
}
|
|
39800
|
+
if (cmd.op === "stop") {
|
|
39801
|
+
if (!readPhoneThread(cmd.chatId)) {
|
|
39802
|
+
emitControl(opts, { op: "error", chatId: cmd.chatId, message: "That agent is not on this Mac." });
|
|
39803
|
+
return;
|
|
39804
|
+
}
|
|
39805
|
+
bumpChat(cmd.chatId);
|
|
39806
|
+
interruptChat(cmd.chatId, log);
|
|
39807
|
+
emitControl(opts, { op: "stopped", chatId: cmd.chatId });
|
|
39808
|
+
return;
|
|
39809
|
+
}
|
|
39810
|
+
const chatId = cmd.chatId;
|
|
39811
|
+
const generation = bumpChat(chatId);
|
|
39812
|
+
phoneTurnGen.set(chatId, generation);
|
|
39813
|
+
interruptChat(chatId, log);
|
|
39814
|
+
enqueueChat(chatId, async () => {
|
|
39815
|
+
if (chatGeneration.get(chatId) !== generation) return;
|
|
39816
|
+
const thread = readPhoneThread(chatId);
|
|
39817
|
+
if (!thread) {
|
|
39818
|
+
emitControl(opts, { op: "error", chatId, message: "That agent is not on this Mac." });
|
|
39819
|
+
return;
|
|
39820
|
+
}
|
|
39821
|
+
if (thread.status === "running" || thread.status === "queued") {
|
|
39822
|
+
try {
|
|
39823
|
+
getOrchestrator().stop(thread.id, { clearQueue: true });
|
|
39824
|
+
} catch {
|
|
39825
|
+
}
|
|
39826
|
+
}
|
|
39827
|
+
if (chatGeneration.get(chatId) !== generation) return;
|
|
39828
|
+
const stopStream = watchPhoneStream(opts, chatId, thread.id, generation);
|
|
39829
|
+
try {
|
|
39830
|
+
try {
|
|
39831
|
+
applyPhoneOptions(thread.id, cmd);
|
|
39832
|
+
} catch (err) {
|
|
39833
|
+
emitControl(opts, { op: "error", chatId, message: phoneError(err) });
|
|
39834
|
+
}
|
|
39835
|
+
const live = readPhoneThread(thread.id) ?? thread;
|
|
39836
|
+
const staged = stagePhoneTurnFiles(live.worktreePath, cmd);
|
|
39837
|
+
const text6 = cmd.text.trim() || (staged.length ? "See the attached files." : "");
|
|
39838
|
+
const formatted = formatRemotePrompt(text6);
|
|
39839
|
+
await getOrchestrator().send(thread.id, formatted, { attachments: staged });
|
|
39840
|
+
await getOrchestrator().waitForTurn(thread.id, 14 * 60 * 1e3);
|
|
39841
|
+
} catch (err) {
|
|
39842
|
+
if (chatGeneration.get(chatId) !== generation) return;
|
|
39843
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
39844
|
+
emitControl(opts, { op: "error", chatId, message: `Sideboard failed: ${message}` });
|
|
39845
|
+
return;
|
|
39846
|
+
} finally {
|
|
39847
|
+
stopStream();
|
|
39848
|
+
finishPhoneTurn(chatId, generation);
|
|
39849
|
+
}
|
|
39850
|
+
if (chatGeneration.get(chatId) !== generation) return;
|
|
39851
|
+
emitChatTurn(opts, chatId, thread.id);
|
|
39852
|
+
});
|
|
39853
|
+
}
|
|
38792
39854
|
function handleRemoteInbound(text6, opts) {
|
|
38793
|
-
const generation = ++inboundGeneration;
|
|
38794
39855
|
const log = opts.onLog ?? (() => void 0);
|
|
38795
39856
|
const body = text6.trim();
|
|
39857
|
+
const control = takePhoneControl(body);
|
|
39858
|
+
if (control) {
|
|
39859
|
+
if (control === "invalid") {
|
|
39860
|
+
emitControl(opts, { op: "error", message: "invalid phone command" });
|
|
39861
|
+
return;
|
|
39862
|
+
}
|
|
39863
|
+
handlePhoneControl(control, opts);
|
|
39864
|
+
return;
|
|
39865
|
+
}
|
|
39866
|
+
const generation = ++inboundGeneration;
|
|
38796
39867
|
if (body) interruptRemoteCoordinator(opts.deviceId, log);
|
|
38797
39868
|
enqueue(async () => {
|
|
38798
39869
|
if (generation !== inboundGeneration) return;
|
|
@@ -38907,7 +39978,8 @@ function runRemoteHost(opts) {
|
|
|
38907
39978
|
type: "host_register",
|
|
38908
39979
|
deviceId: opts.deviceId,
|
|
38909
39980
|
deviceLabel,
|
|
38910
|
-
hostSecret: opts.hostSecret
|
|
39981
|
+
hostSecret: opts.hostSecret,
|
|
39982
|
+
...opts.accountToken?.trim() ? { accountToken: opts.accountToken.trim() } : {}
|
|
38911
39983
|
});
|
|
38912
39984
|
};
|
|
38913
39985
|
const loop = async () => {
|
|
@@ -39015,7 +40087,8 @@ function runRemoteHost(opts) {
|
|
|
39015
40087
|
handleRemoteInbound("stop", {
|
|
39016
40088
|
deviceId: opts.deviceId,
|
|
39017
40089
|
onLog: log,
|
|
39018
|
-
onOutbound: emit
|
|
40090
|
+
onOutbound: emit,
|
|
40091
|
+
onOpenWorktreeFile: opts.onOpenWorktreeFile
|
|
39019
40092
|
});
|
|
39020
40093
|
return;
|
|
39021
40094
|
}
|
|
@@ -39023,7 +40096,9 @@ function runRemoteHost(opts) {
|
|
|
39023
40096
|
handleRemoteInbound(msg.text, {
|
|
39024
40097
|
deviceId: opts.deviceId,
|
|
39025
40098
|
onLog: log,
|
|
39026
|
-
onOutbound: emit
|
|
40099
|
+
onOutbound: emit,
|
|
40100
|
+
transcribeWav: opts.transcribeWav,
|
|
40101
|
+
onOpenWorktreeFile: opts.onOpenWorktreeFile
|
|
39027
40102
|
});
|
|
39028
40103
|
}
|
|
39029
40104
|
};
|
|
@@ -39050,6 +40125,671 @@ function runRemoteHost(opts) {
|
|
|
39050
40125
|
};
|
|
39051
40126
|
}
|
|
39052
40127
|
|
|
40128
|
+
// src/relay/account-login.ts
|
|
40129
|
+
var import_node_crypto27 = require("crypto");
|
|
40130
|
+
|
|
40131
|
+
// src/relay/accounts.ts
|
|
40132
|
+
var import_node_crypto26 = require("crypto");
|
|
40133
|
+
var import_node_fs70 = require("fs");
|
|
40134
|
+
var import_node_module7 = require("module");
|
|
40135
|
+
var import_node_path68 = require("path");
|
|
40136
|
+
var GITHUB_ACCOUNT_ISSUER = "https://github.com";
|
|
40137
|
+
var RELAY_DEVICE_CAP = 3;
|
|
40138
|
+
function thisModuleFile3() {
|
|
40139
|
+
const cjsFile = typeof __filename !== "undefined" ? __filename : "";
|
|
40140
|
+
return cjsFile || process.argv[1] || (0, import_node_path68.join)(process.cwd(), "package.json");
|
|
40141
|
+
}
|
|
40142
|
+
function openDatabase(filePath) {
|
|
40143
|
+
const req = (0, import_node_module7.createRequire)(thisModuleFile3());
|
|
40144
|
+
const specifier = "node:sqlite";
|
|
40145
|
+
const { DatabaseSync: SqliteDatabase } = req(specifier);
|
|
40146
|
+
const db = new SqliteDatabase(filePath);
|
|
40147
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
40148
|
+
return db;
|
|
40149
|
+
}
|
|
40150
|
+
function sha256(value) {
|
|
40151
|
+
return (0, import_node_crypto26.createHash)("sha256").update(value).digest("hex");
|
|
40152
|
+
}
|
|
40153
|
+
function sameHash(a, b) {
|
|
40154
|
+
const left = Buffer.from(a);
|
|
40155
|
+
const right = Buffer.from(b);
|
|
40156
|
+
return left.length === right.length && (0, import_node_crypto26.timingSafeEqual)(left, right);
|
|
40157
|
+
}
|
|
40158
|
+
function normalizeEmail(email) {
|
|
40159
|
+
const trimmed = email?.trim().toLowerCase() ?? "";
|
|
40160
|
+
return trimmed || null;
|
|
40161
|
+
}
|
|
40162
|
+
function longestMountPoint(dir, mounts) {
|
|
40163
|
+
let best = "/";
|
|
40164
|
+
for (const line of mounts.split("\n")) {
|
|
40165
|
+
const point = line.split(" ")[1]?.replace(/\\040/g, " ");
|
|
40166
|
+
if (!point) continue;
|
|
40167
|
+
if (dir === point || dir.startsWith(point.endsWith("/") ? point : `${point}/`)) {
|
|
40168
|
+
if (point.length > best.length) best = point;
|
|
40169
|
+
}
|
|
40170
|
+
}
|
|
40171
|
+
return best;
|
|
40172
|
+
}
|
|
40173
|
+
function assertRelayAccountsDurable(filePath) {
|
|
40174
|
+
if (process.env.SIDEBOARD_RELAY_ACCOUNTS_EPHEMERAL === "1") return;
|
|
40175
|
+
if (process.env.NODE_ENV !== "production") return;
|
|
40176
|
+
if (process.platform !== "linux") return;
|
|
40177
|
+
const dir = (0, import_node_fs70.realpathSync)((0, import_node_path68.dirname)(filePath));
|
|
40178
|
+
const mounts = (0, import_node_fs70.readFileSync)("/proc/mounts", "utf8");
|
|
40179
|
+
const point = longestMountPoint(dir, mounts);
|
|
40180
|
+
if (point === "/") {
|
|
40181
|
+
throw new Error(
|
|
40182
|
+
`Relay accounts at ${filePath} are on the machine root filesystem. Mount a Fly volume so sign-ins survive deploys.`
|
|
40183
|
+
);
|
|
40184
|
+
}
|
|
40185
|
+
}
|
|
40186
|
+
var RelayAccountStore = class {
|
|
40187
|
+
db;
|
|
40188
|
+
constructor(filePath) {
|
|
40189
|
+
(0, import_node_fs70.mkdirSync)((0, import_node_path68.dirname)(filePath), { recursive: true });
|
|
40190
|
+
this.db = openDatabase(filePath);
|
|
40191
|
+
this.db.exec(`
|
|
40192
|
+
CREATE TABLE IF NOT EXISTS accounts (
|
|
40193
|
+
id TEXT PRIMARY KEY,
|
|
40194
|
+
created_at INTEGER NOT NULL,
|
|
40195
|
+
device_cap INTEGER NOT NULL
|
|
40196
|
+
);
|
|
40197
|
+
CREATE TABLE IF NOT EXISTS identities (
|
|
40198
|
+
issuer TEXT NOT NULL,
|
|
40199
|
+
subject TEXT NOT NULL,
|
|
40200
|
+
account_id TEXT NOT NULL,
|
|
40201
|
+
email TEXT,
|
|
40202
|
+
email_verified INTEGER NOT NULL,
|
|
40203
|
+
PRIMARY KEY (issuer, subject)
|
|
40204
|
+
);
|
|
40205
|
+
CREATE INDEX IF NOT EXISTS identities_email ON identities(email);
|
|
40206
|
+
CREATE TABLE IF NOT EXISTS credentials (
|
|
40207
|
+
token_hash TEXT PRIMARY KEY,
|
|
40208
|
+
account_id TEXT NOT NULL,
|
|
40209
|
+
created_at INTEGER NOT NULL
|
|
40210
|
+
);
|
|
40211
|
+
CREATE TABLE IF NOT EXISTS devices (
|
|
40212
|
+
device_id TEXT PRIMARY KEY,
|
|
40213
|
+
account_id TEXT NOT NULL,
|
|
40214
|
+
host_secret_hash TEXT NOT NULL,
|
|
40215
|
+
device_label TEXT NOT NULL,
|
|
40216
|
+
created_at INTEGER NOT NULL
|
|
40217
|
+
);
|
|
40218
|
+
`);
|
|
40219
|
+
}
|
|
40220
|
+
close() {
|
|
40221
|
+
this.db.close();
|
|
40222
|
+
}
|
|
40223
|
+
/**
|
|
40224
|
+
* First sign-in creates an account. The same issuer+subject returns that
|
|
40225
|
+
* account. A verified email match attaches a second provider. `linkAccountId`
|
|
40226
|
+
* attaches while that account’s credential is already signed in.
|
|
40227
|
+
* Each call mints a new Mac credential. Older credentials stay valid.
|
|
40228
|
+
*/
|
|
40229
|
+
signIn(identity, linkAccountId) {
|
|
40230
|
+
const issuer = identity.issuer.trim();
|
|
40231
|
+
const subject = identity.subject.trim();
|
|
40232
|
+
if (!issuer || !subject) throw new Error("Sign-in is missing an identity.");
|
|
40233
|
+
const email = normalizeEmail(identity.email);
|
|
40234
|
+
const verified = Boolean(identity.emailVerified && email);
|
|
40235
|
+
const link = linkAccountId?.trim() || null;
|
|
40236
|
+
this.db.exec("BEGIN");
|
|
40237
|
+
try {
|
|
40238
|
+
const existing = this.db.prepare("SELECT account_id, email FROM identities WHERE issuer = ? AND subject = ?").get(issuer, subject);
|
|
40239
|
+
let accountId;
|
|
40240
|
+
if (existing) {
|
|
40241
|
+
if (link && link !== existing.account_id) {
|
|
40242
|
+
throw new Error("That sign-in already belongs to another Sideboard account.");
|
|
40243
|
+
}
|
|
40244
|
+
accountId = existing.account_id;
|
|
40245
|
+
this.requireAccount(accountId);
|
|
40246
|
+
if (email) {
|
|
40247
|
+
this.db.prepare("UPDATE identities SET email = ?, email_verified = ? WHERE issuer = ? AND subject = ?").run(email, verified ? 1 : 0, issuer, subject);
|
|
40248
|
+
}
|
|
40249
|
+
} else if (link) {
|
|
40250
|
+
this.requireAccount(link);
|
|
40251
|
+
accountId = link;
|
|
40252
|
+
this.insertIdentity(issuer, subject, accountId, email, verified);
|
|
40253
|
+
} else {
|
|
40254
|
+
const byEmail = verified ? this.accountIdForVerifiedEmail(email) : void 0;
|
|
40255
|
+
accountId = byEmail ?? (0, import_node_crypto26.randomUUID)();
|
|
40256
|
+
if (!byEmail) {
|
|
40257
|
+
this.db.prepare("INSERT INTO accounts (id, created_at, device_cap) VALUES (?, ?, ?)").run(accountId, Date.now(), RELAY_DEVICE_CAP);
|
|
40258
|
+
}
|
|
40259
|
+
this.insertIdentity(issuer, subject, accountId, email, verified);
|
|
40260
|
+
}
|
|
40261
|
+
const credential = this.mintCredential(accountId);
|
|
40262
|
+
const stored = this.db.prepare("SELECT email FROM identities WHERE account_id = ? AND email IS NOT NULL ORDER BY email_verified DESC LIMIT 1").get(accountId);
|
|
40263
|
+
this.db.exec("COMMIT");
|
|
40264
|
+
return { accountId, credential, email: stored?.email ?? email };
|
|
40265
|
+
} catch (err) {
|
|
40266
|
+
this.db.exec("ROLLBACK");
|
|
40267
|
+
throw err;
|
|
40268
|
+
}
|
|
40269
|
+
}
|
|
40270
|
+
verifyCredential(token) {
|
|
40271
|
+
const trimmed = token.trim();
|
|
40272
|
+
if (!trimmed) return null;
|
|
40273
|
+
const row = this.db.prepare("SELECT account_id FROM credentials WHERE token_hash = ?").get(sha256(trimmed));
|
|
40274
|
+
return row?.account_id ?? null;
|
|
40275
|
+
}
|
|
40276
|
+
/** Drop one Mac credential. The account and its other Macs stay. */
|
|
40277
|
+
logout(token) {
|
|
40278
|
+
const trimmed = token.trim();
|
|
40279
|
+
if (!trimmed) return;
|
|
40280
|
+
this.db.prepare("DELETE FROM credentials WHERE token_hash = ?").run(sha256(trimmed));
|
|
40281
|
+
}
|
|
40282
|
+
/** Delete the account and every identity, credential, and Mac bound to it. */
|
|
40283
|
+
revokeAccount(accountId) {
|
|
40284
|
+
const id = accountId.trim();
|
|
40285
|
+
if (!id) return;
|
|
40286
|
+
this.db.exec("BEGIN");
|
|
40287
|
+
try {
|
|
40288
|
+
this.db.prepare("DELETE FROM devices WHERE account_id = ?").run(id);
|
|
40289
|
+
this.db.prepare("DELETE FROM credentials WHERE account_id = ?").run(id);
|
|
40290
|
+
this.db.prepare("DELETE FROM identities WHERE account_id = ?").run(id);
|
|
40291
|
+
this.db.prepare("DELETE FROM accounts WHERE id = ?").run(id);
|
|
40292
|
+
this.db.exec("COMMIT");
|
|
40293
|
+
} catch (err) {
|
|
40294
|
+
this.db.exec("ROLLBACK");
|
|
40295
|
+
throw err;
|
|
40296
|
+
}
|
|
40297
|
+
}
|
|
40298
|
+
/**
|
|
40299
|
+
* Accept `host_register` for a signed-in account. The device row survives
|
|
40300
|
+
* process restart, so a later register with a different host secret is rejected
|
|
40301
|
+
* and a new device id counts against the cap.
|
|
40302
|
+
*/
|
|
40303
|
+
authorizeHost(input) {
|
|
40304
|
+
const deviceId = input.deviceId.trim();
|
|
40305
|
+
const hostSecret = input.hostSecret.trim();
|
|
40306
|
+
const deviceLabel = input.deviceLabel.trim() || "This Mac";
|
|
40307
|
+
if (!deviceId || !hostSecret) return { ok: false, message: "sign in required" };
|
|
40308
|
+
const accountId = this.verifyCredential(input.accountToken);
|
|
40309
|
+
if (!accountId) return { ok: false, message: "sign in required" };
|
|
40310
|
+
this.db.exec("BEGIN");
|
|
40311
|
+
try {
|
|
40312
|
+
const account = this.db.prepare("SELECT id, device_cap FROM accounts WHERE id = ?").get(accountId);
|
|
40313
|
+
if (!account) {
|
|
40314
|
+
this.db.exec("ROLLBACK");
|
|
40315
|
+
return { ok: false, message: "sign in required" };
|
|
40316
|
+
}
|
|
40317
|
+
const device = this.db.prepare("SELECT device_id, account_id, host_secret_hash FROM devices WHERE device_id = ?").get(deviceId);
|
|
40318
|
+
const secretHash = sha256(hostSecret);
|
|
40319
|
+
if (!device) {
|
|
40320
|
+
const count = this.db.prepare("SELECT COUNT(*) AS n FROM devices WHERE account_id = ?").get(accountId);
|
|
40321
|
+
if (count.n >= account.device_cap) {
|
|
40322
|
+
this.db.exec("ROLLBACK");
|
|
40323
|
+
return { ok: false, message: "This account already has the maximum number of Macs." };
|
|
40324
|
+
}
|
|
40325
|
+
this.db.prepare(
|
|
40326
|
+
"INSERT INTO devices (device_id, account_id, host_secret_hash, device_label, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
40327
|
+
).run(deviceId, accountId, secretHash, deviceLabel, Date.now());
|
|
40328
|
+
this.db.exec("COMMIT");
|
|
40329
|
+
return { ok: true, accountId };
|
|
40330
|
+
}
|
|
40331
|
+
if (device.account_id !== accountId) {
|
|
40332
|
+
this.db.exec("ROLLBACK");
|
|
40333
|
+
return { ok: false, message: "This Mac is registered to another account." };
|
|
40334
|
+
}
|
|
40335
|
+
if (!sameHash(device.host_secret_hash, secretHash)) {
|
|
40336
|
+
this.db.exec("ROLLBACK");
|
|
40337
|
+
return { ok: false, message: "host secret rejected" };
|
|
40338
|
+
}
|
|
40339
|
+
this.db.prepare("UPDATE devices SET device_label = ? WHERE device_id = ?").run(deviceLabel, deviceId);
|
|
40340
|
+
this.db.exec("COMMIT");
|
|
40341
|
+
return { ok: true, accountId };
|
|
40342
|
+
} catch (err) {
|
|
40343
|
+
this.db.exec("ROLLBACK");
|
|
40344
|
+
throw err;
|
|
40345
|
+
}
|
|
40346
|
+
}
|
|
40347
|
+
requireAccount(accountId) {
|
|
40348
|
+
const row = this.db.prepare("SELECT id FROM accounts WHERE id = ?").get(accountId);
|
|
40349
|
+
if (!row) throw new Error("That Sideboard account is no longer on the relay.");
|
|
40350
|
+
}
|
|
40351
|
+
accountIdForVerifiedEmail(email) {
|
|
40352
|
+
const row = this.db.prepare(
|
|
40353
|
+
"SELECT account_id FROM identities WHERE email_verified = 1 AND email = ? LIMIT 1"
|
|
40354
|
+
).get(email);
|
|
40355
|
+
return row?.account_id;
|
|
40356
|
+
}
|
|
40357
|
+
insertIdentity(issuer, subject, accountId, email, verified) {
|
|
40358
|
+
this.db.prepare(
|
|
40359
|
+
"INSERT INTO identities (issuer, subject, account_id, email, email_verified) VALUES (?, ?, ?, ?, ?)"
|
|
40360
|
+
).run(issuer, subject, accountId, email, verified ? 1 : 0);
|
|
40361
|
+
}
|
|
40362
|
+
mintCredential(accountId) {
|
|
40363
|
+
const credential = (0, import_node_crypto26.randomBytes)(32).toString("hex");
|
|
40364
|
+
this.db.prepare("INSERT INTO credentials (token_hash, account_id, created_at) VALUES (?, ?, ?)").run(sha256(credential), accountId, Date.now());
|
|
40365
|
+
return credential;
|
|
40366
|
+
}
|
|
40367
|
+
};
|
|
40368
|
+
|
|
40369
|
+
// src/relay/account-oauth.ts
|
|
40370
|
+
var RELAY_GIT_HOSTS = ["github"];
|
|
40371
|
+
var REMOTE_LOGIN_PATH = "/remote/login";
|
|
40372
|
+
var REMOTE_OAUTH_CALLBACK_PATH = "/remote/oauth/callback";
|
|
40373
|
+
var REMOTE_OAUTH_RESULT_PATH = "/remote/oauth/result";
|
|
40374
|
+
var REMOTE_LOGOUT_PATH = "/remote/logout";
|
|
40375
|
+
var STATE_RE = /^[A-Za-z0-9_-]{16,128}$/;
|
|
40376
|
+
var LOGIN_TTL_MS = 10 * 60 * 1e3;
|
|
40377
|
+
var RelayLoginSessions = class {
|
|
40378
|
+
waiting = /* @__PURE__ */ new Map();
|
|
40379
|
+
results = /* @__PURE__ */ new Map();
|
|
40380
|
+
begin(state, provider, accountId) {
|
|
40381
|
+
this.sweep();
|
|
40382
|
+
this.waiting.set(state, { provider, accountId, expiresAt: Date.now() + LOGIN_TTL_MS });
|
|
40383
|
+
}
|
|
40384
|
+
takeLogin(state) {
|
|
40385
|
+
this.sweep();
|
|
40386
|
+
const row = this.waiting.get(state);
|
|
40387
|
+
if (!row) return null;
|
|
40388
|
+
this.waiting.delete(state);
|
|
40389
|
+
if (row.expiresAt < Date.now()) return null;
|
|
40390
|
+
return row;
|
|
40391
|
+
}
|
|
40392
|
+
putResult(state, value) {
|
|
40393
|
+
this.results.set(state, { value, expiresAt: Date.now() + LOGIN_TTL_MS });
|
|
40394
|
+
}
|
|
40395
|
+
/** `undefined` means the browser flow has not finished. */
|
|
40396
|
+
takeResult(state) {
|
|
40397
|
+
this.sweep();
|
|
40398
|
+
const row = this.results.get(state);
|
|
40399
|
+
if (!row) return this.waiting.has(state) ? void 0 : null;
|
|
40400
|
+
this.results.delete(state);
|
|
40401
|
+
if (row.expiresAt < Date.now()) return null;
|
|
40402
|
+
return row.value;
|
|
40403
|
+
}
|
|
40404
|
+
sweep() {
|
|
40405
|
+
const now = Date.now();
|
|
40406
|
+
for (const [state, row] of this.waiting) {
|
|
40407
|
+
if (row.expiresAt < now) this.waiting.delete(state);
|
|
40408
|
+
}
|
|
40409
|
+
for (const [state, row] of this.results) {
|
|
40410
|
+
if (row.expiresAt < now) this.results.delete(state);
|
|
40411
|
+
}
|
|
40412
|
+
}
|
|
40413
|
+
};
|
|
40414
|
+
var GIT_HOST_AUTH = {
|
|
40415
|
+
github: {
|
|
40416
|
+
label: "GitHub",
|
|
40417
|
+
authorizeUrl: "https://github.com/login/oauth/authorize",
|
|
40418
|
+
scope: "user:email",
|
|
40419
|
+
fetchIdentity: fetchGithubIdentity
|
|
40420
|
+
}
|
|
40421
|
+
};
|
|
40422
|
+
function providerOf(value) {
|
|
40423
|
+
return RELAY_GIT_HOSTS.includes(value) ? value : null;
|
|
40424
|
+
}
|
|
40425
|
+
function relayAuthorizeUrl(provider, client, redirectUri, state) {
|
|
40426
|
+
const host = GIT_HOST_AUTH[provider];
|
|
40427
|
+
const url = new URL(host.authorizeUrl);
|
|
40428
|
+
url.searchParams.set("client_id", client.clientId);
|
|
40429
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
40430
|
+
url.searchParams.set("scope", host.scope);
|
|
40431
|
+
url.searchParams.set("state", state);
|
|
40432
|
+
return url.toString();
|
|
40433
|
+
}
|
|
40434
|
+
async function readJson2(res) {
|
|
40435
|
+
const text6 = await res.text();
|
|
40436
|
+
if (!text6) return {};
|
|
40437
|
+
try {
|
|
40438
|
+
return JSON.parse(text6);
|
|
40439
|
+
} catch {
|
|
40440
|
+
throw new Error("Sign-in provider returned a non-JSON response.");
|
|
40441
|
+
}
|
|
40442
|
+
}
|
|
40443
|
+
function accessToken(body) {
|
|
40444
|
+
const token = body.access_token;
|
|
40445
|
+
if (typeof token !== "string" || !token.trim()) {
|
|
40446
|
+
const message = body.error_description;
|
|
40447
|
+
throw new Error(typeof message === "string" && message.trim() ? message : "Sign-in provider rejected the code.");
|
|
40448
|
+
}
|
|
40449
|
+
return token.trim();
|
|
40450
|
+
}
|
|
40451
|
+
async function fetchGithubIdentity(client, code, redirectUri, fetchImpl) {
|
|
40452
|
+
const tokenRes = await fetchImpl("https://github.com/login/oauth/access_token", {
|
|
40453
|
+
method: "POST",
|
|
40454
|
+
headers: { Accept: "application/json", "Content-Type": "application/json" },
|
|
40455
|
+
body: JSON.stringify({
|
|
40456
|
+
client_id: client.clientId,
|
|
40457
|
+
client_secret: client.clientSecret,
|
|
40458
|
+
code,
|
|
40459
|
+
redirect_uri: redirectUri
|
|
40460
|
+
})
|
|
40461
|
+
});
|
|
40462
|
+
const token = accessToken(await readJson2(tokenRes));
|
|
40463
|
+
const userRes = await fetchImpl("https://api.github.com/user", {
|
|
40464
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "User-Agent": "sideboard-relay" }
|
|
40465
|
+
});
|
|
40466
|
+
const user = await readJson2(userRes);
|
|
40467
|
+
const subject = typeof user.id === "number" ? String(user.id) : "";
|
|
40468
|
+
if (!subject) throw new Error("GitHub did not return an account id.");
|
|
40469
|
+
const emailRes = await fetchImpl("https://api.github.com/user/emails", {
|
|
40470
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "User-Agent": "sideboard-relay" }
|
|
40471
|
+
});
|
|
40472
|
+
const emails = await readJson2(emailRes);
|
|
40473
|
+
const list = Array.isArray(emails) ? emails : [];
|
|
40474
|
+
const primary = list.find((row) => row.primary === true && row.verified === true);
|
|
40475
|
+
const anyVerified = list.find((row) => row.verified === true);
|
|
40476
|
+
const chosen = primary ?? anyVerified;
|
|
40477
|
+
const email = chosen && typeof chosen.email === "string" ? chosen.email : null;
|
|
40478
|
+
return { issuer: GITHUB_ACCOUNT_ISSUER, subject, email, emailVerified: Boolean(email) };
|
|
40479
|
+
}
|
|
40480
|
+
async function fetchRelayIdentity(provider, client, code, redirectUri, fetchImpl) {
|
|
40481
|
+
return GIT_HOST_AUTH[provider].fetchIdentity(client, code, redirectUri, fetchImpl);
|
|
40482
|
+
}
|
|
40483
|
+
async function readBody(req) {
|
|
40484
|
+
const chunks = [];
|
|
40485
|
+
let size = 0;
|
|
40486
|
+
for await (const chunk of req) {
|
|
40487
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
40488
|
+
size += buf.length;
|
|
40489
|
+
if (size > 8192) throw new Error("body too large");
|
|
40490
|
+
chunks.push(buf);
|
|
40491
|
+
}
|
|
40492
|
+
const text6 = Buffer.concat(chunks).toString("utf8").trim();
|
|
40493
|
+
if (!text6) return {};
|
|
40494
|
+
return JSON.parse(text6);
|
|
40495
|
+
}
|
|
40496
|
+
function sendJson2(res, status, body) {
|
|
40497
|
+
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
40498
|
+
res.end(JSON.stringify(body));
|
|
40499
|
+
}
|
|
40500
|
+
function relayOAuthReturnPage(title, body) {
|
|
40501
|
+
return `<!DOCTYPE html>
|
|
40502
|
+
<html lang="en">
|
|
40503
|
+
<head>
|
|
40504
|
+
<meta charset="utf-8">
|
|
40505
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
40506
|
+
<meta name="robots" content="noindex">
|
|
40507
|
+
<title>${escapeHtml2(title)}</title>
|
|
40508
|
+
<style>
|
|
40509
|
+
:root {
|
|
40510
|
+
--bg: #09090b;
|
|
40511
|
+
--card: #16171b;
|
|
40512
|
+
--line: rgba(255, 255, 255, 0.08);
|
|
40513
|
+
--text: #f4f5f7;
|
|
40514
|
+
--muted: #9aa3ad;
|
|
40515
|
+
--blue: #3b9eff;
|
|
40516
|
+
--blue-fill: #0f7ed4;
|
|
40517
|
+
}
|
|
40518
|
+
* { box-sizing: border-box }
|
|
40519
|
+
body {
|
|
40520
|
+
margin: 0;
|
|
40521
|
+
min-height: 100vh;
|
|
40522
|
+
display: grid;
|
|
40523
|
+
place-items: center;
|
|
40524
|
+
padding: 48px 24px;
|
|
40525
|
+
background:
|
|
40526
|
+
radial-gradient(closest-side, rgba(15, 126, 212, 0.22), transparent 72%) center -120px / 720px 480px no-repeat,
|
|
40527
|
+
var(--bg);
|
|
40528
|
+
color: var(--text);
|
|
40529
|
+
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", sans-serif;
|
|
40530
|
+
-webkit-font-smoothing: antialiased;
|
|
40531
|
+
}
|
|
40532
|
+
main {
|
|
40533
|
+
width: min(28rem, 100%);
|
|
40534
|
+
background: var(--card);
|
|
40535
|
+
border: 1px solid var(--line);
|
|
40536
|
+
border-radius: 16px;
|
|
40537
|
+
padding: 28px 28px 32px;
|
|
40538
|
+
}
|
|
40539
|
+
.brand {
|
|
40540
|
+
display: flex;
|
|
40541
|
+
align-items: center;
|
|
40542
|
+
gap: 10px;
|
|
40543
|
+
margin: 0 0 22px;
|
|
40544
|
+
font-weight: 650;
|
|
40545
|
+
font-size: 17px;
|
|
40546
|
+
letter-spacing: -0.02em;
|
|
40547
|
+
}
|
|
40548
|
+
.mark { position: relative; display: inline-block; width: 26px; height: 26px; flex: none }
|
|
40549
|
+
.mark i { position: absolute; width: 18px; height: 18px; border-radius: 3px; transform: rotate(14deg) }
|
|
40550
|
+
.mark i:first-child { left: 6px; top: 6px; background: var(--blue-fill) }
|
|
40551
|
+
.mark i:last-child { left: 1px; top: 1px; border: 2px solid #d7dbdf }
|
|
40552
|
+
h1 { margin: 0; font-size: 1.5rem; line-height: 1.2; letter-spacing: -0.03em; font-weight: 650 }
|
|
40553
|
+
p { margin: 10px 0 0; line-height: 1.55; color: var(--muted) }
|
|
40554
|
+
a { color: var(--blue) }
|
|
40555
|
+
</style>
|
|
40556
|
+
</head>
|
|
40557
|
+
<body>
|
|
40558
|
+
<main>
|
|
40559
|
+
<div class="brand"><span class="mark" aria-hidden="true"><i></i><i></i></span>Sideboard</div>
|
|
40560
|
+
${body}
|
|
40561
|
+
</main>
|
|
40562
|
+
</body>
|
|
40563
|
+
</html>`;
|
|
40564
|
+
}
|
|
40565
|
+
function sendHtml(res, status, title, body) {
|
|
40566
|
+
res.writeHead(status, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
40567
|
+
res.end(relayOAuthReturnPage(title, body));
|
|
40568
|
+
}
|
|
40569
|
+
async function handleRelayAccountHttp(opts) {
|
|
40570
|
+
const reqUrl = opts.req.url || "/";
|
|
40571
|
+
const path2 = reqUrl.split("?")[0];
|
|
40572
|
+
const method = opts.req.method || "GET";
|
|
40573
|
+
const origin = opts.publicOrigin.replace(/\/+$/, "");
|
|
40574
|
+
const redirectUri = `${origin}${REMOTE_OAUTH_CALLBACK_PATH}`;
|
|
40575
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
40576
|
+
if (method === "POST" && path2 === REMOTE_LOGIN_PATH) {
|
|
40577
|
+
let body;
|
|
40578
|
+
try {
|
|
40579
|
+
body = await readBody(opts.req);
|
|
40580
|
+
} catch {
|
|
40581
|
+
sendJson2(opts.res, 400, { ok: false, error: "invalid login request" });
|
|
40582
|
+
return true;
|
|
40583
|
+
}
|
|
40584
|
+
const provider = providerOf(typeof body.provider === "string" ? body.provider : "");
|
|
40585
|
+
const state = typeof body.state === "string" ? body.state.trim() : "";
|
|
40586
|
+
if (!provider || !STATE_RE.test(state)) {
|
|
40587
|
+
sendJson2(opts.res, 400, { ok: false, error: "invalid login request" });
|
|
40588
|
+
return true;
|
|
40589
|
+
}
|
|
40590
|
+
const client = opts.oauth[provider];
|
|
40591
|
+
if (!client?.clientId || !client.clientSecret) {
|
|
40592
|
+
sendJson2(opts.res, 503, {
|
|
40593
|
+
ok: false,
|
|
40594
|
+
error: `${GIT_HOST_AUTH[provider].label} sign-in is not configured on this relay.`
|
|
40595
|
+
});
|
|
40596
|
+
return true;
|
|
40597
|
+
}
|
|
40598
|
+
let accountId = null;
|
|
40599
|
+
const linkToken = typeof body.accountToken === "string" ? body.accountToken.trim() : "";
|
|
40600
|
+
if (linkToken) {
|
|
40601
|
+
accountId = opts.store.verifyCredential(linkToken);
|
|
40602
|
+
if (!accountId) {
|
|
40603
|
+
sendJson2(opts.res, 401, { ok: false, error: "sign in required" });
|
|
40604
|
+
return true;
|
|
40605
|
+
}
|
|
40606
|
+
}
|
|
40607
|
+
opts.sessions.begin(state, provider, accountId);
|
|
40608
|
+
sendJson2(opts.res, 200, { ok: true, url: relayAuthorizeUrl(provider, client, redirectUri, state) });
|
|
40609
|
+
return true;
|
|
40610
|
+
}
|
|
40611
|
+
if (method === "GET" && path2 === REMOTE_OAUTH_CALLBACK_PATH) {
|
|
40612
|
+
const url = new URL(reqUrl, origin);
|
|
40613
|
+
const state = url.searchParams.get("state")?.trim() ?? "";
|
|
40614
|
+
const code = url.searchParams.get("code")?.trim() ?? "";
|
|
40615
|
+
const providerError = url.searchParams.get("error")?.trim() ?? "";
|
|
40616
|
+
const pending = STATE_RE.test(state) ? opts.sessions.takeLogin(state) : null;
|
|
40617
|
+
if (!pending) {
|
|
40618
|
+
sendHtml(opts.res, 400, "Sideboard", "<h1>Sign-in expired</h1><p>Return to Sideboard and try again.</p>");
|
|
40619
|
+
return true;
|
|
40620
|
+
}
|
|
40621
|
+
if (providerError || !code) {
|
|
40622
|
+
const message = providerError || "Sign-in was cancelled.";
|
|
40623
|
+
opts.sessions.putResult(state, { ok: false, error: message });
|
|
40624
|
+
sendHtml(opts.res, 400, "Sideboard", `<h1>Could not sign in</h1><p>${escapeHtml2(message)}</p>`);
|
|
40625
|
+
return true;
|
|
40626
|
+
}
|
|
40627
|
+
const client = opts.oauth[pending.provider];
|
|
40628
|
+
if (!client) {
|
|
40629
|
+
opts.sessions.putResult(state, { ok: false, error: "Sign-in is not configured on this relay." });
|
|
40630
|
+
sendHtml(opts.res, 503, "Sideboard", "<h1>Sign-in is not configured</h1>");
|
|
40631
|
+
return true;
|
|
40632
|
+
}
|
|
40633
|
+
try {
|
|
40634
|
+
const identity = await fetchRelayIdentity(pending.provider, client, code, redirectUri, fetchImpl);
|
|
40635
|
+
const signed = opts.store.signIn(identity, pending.accountId);
|
|
40636
|
+
opts.sessions.putResult(state, {
|
|
40637
|
+
ok: true,
|
|
40638
|
+
credential: signed.credential,
|
|
40639
|
+
email: signed.email,
|
|
40640
|
+
accountId: signed.accountId
|
|
40641
|
+
});
|
|
40642
|
+
sendHtml(
|
|
40643
|
+
opts.res,
|
|
40644
|
+
200,
|
|
40645
|
+
"Sideboard",
|
|
40646
|
+
"<h1>Signed in</h1><p>Return to Sideboard. This window can close.</p>"
|
|
40647
|
+
);
|
|
40648
|
+
} catch (err) {
|
|
40649
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
40650
|
+
opts.sessions.putResult(state, { ok: false, error: message });
|
|
40651
|
+
sendHtml(opts.res, 400, "Sideboard", `<h1>Could not sign in</h1><p>${escapeHtml2(message)}</p>`);
|
|
40652
|
+
}
|
|
40653
|
+
return true;
|
|
40654
|
+
}
|
|
40655
|
+
if (method === "GET" && path2 === REMOTE_OAUTH_RESULT_PATH) {
|
|
40656
|
+
const url = new URL(reqUrl, origin);
|
|
40657
|
+
const state = url.searchParams.get("state")?.trim() ?? "";
|
|
40658
|
+
if (!STATE_RE.test(state)) {
|
|
40659
|
+
sendJson2(opts.res, 400, { ok: false, error: "invalid login request" });
|
|
40660
|
+
return true;
|
|
40661
|
+
}
|
|
40662
|
+
const result = opts.sessions.takeResult(state);
|
|
40663
|
+
if (result === void 0 || result === null) {
|
|
40664
|
+
sendJson2(opts.res, 404, { ok: false, error: "pending" });
|
|
40665
|
+
return true;
|
|
40666
|
+
}
|
|
40667
|
+
if (!result.ok) {
|
|
40668
|
+
sendJson2(opts.res, 400, { ok: false, error: result.error });
|
|
40669
|
+
return true;
|
|
40670
|
+
}
|
|
40671
|
+
sendJson2(opts.res, 200, {
|
|
40672
|
+
ok: true,
|
|
40673
|
+
credential: result.credential,
|
|
40674
|
+
email: result.email,
|
|
40675
|
+
accountId: result.accountId
|
|
40676
|
+
});
|
|
40677
|
+
return true;
|
|
40678
|
+
}
|
|
40679
|
+
if (method === "POST" && path2 === REMOTE_LOGOUT_PATH) {
|
|
40680
|
+
let body;
|
|
40681
|
+
try {
|
|
40682
|
+
body = await readBody(opts.req);
|
|
40683
|
+
} catch {
|
|
40684
|
+
sendJson2(opts.res, 400, { ok: false, error: "invalid logout request" });
|
|
40685
|
+
return true;
|
|
40686
|
+
}
|
|
40687
|
+
const token = typeof body.accountToken === "string" ? body.accountToken : "";
|
|
40688
|
+
opts.store.logout(token);
|
|
40689
|
+
sendJson2(opts.res, 200, { ok: true });
|
|
40690
|
+
return true;
|
|
40691
|
+
}
|
|
40692
|
+
return false;
|
|
40693
|
+
}
|
|
40694
|
+
|
|
40695
|
+
// src/relay/account-login.ts
|
|
40696
|
+
var RemoteAccountLoginCancelled = class extends Error {
|
|
40697
|
+
constructor() {
|
|
40698
|
+
super("Sign-in cancelled");
|
|
40699
|
+
this.name = "RemoteAccountLoginCancelled";
|
|
40700
|
+
}
|
|
40701
|
+
};
|
|
40702
|
+
function isRemoteAccountLoginCancelled(err) {
|
|
40703
|
+
return err instanceof RemoteAccountLoginCancelled;
|
|
40704
|
+
}
|
|
40705
|
+
function remoteAccountHttpOrigin(wsUrl = remoteRelayUrl()) {
|
|
40706
|
+
const url = new URL(wsUrl);
|
|
40707
|
+
url.protocol = url.protocol === "wss:" ? "https:" : "http:";
|
|
40708
|
+
url.pathname = "";
|
|
40709
|
+
url.search = "";
|
|
40710
|
+
url.hash = "";
|
|
40711
|
+
return url.origin;
|
|
40712
|
+
}
|
|
40713
|
+
function sleep2(ms, signal) {
|
|
40714
|
+
return new Promise((resolve2, reject) => {
|
|
40715
|
+
if (signal?.aborted) {
|
|
40716
|
+
reject(new RemoteAccountLoginCancelled());
|
|
40717
|
+
return;
|
|
40718
|
+
}
|
|
40719
|
+
const timer = setTimeout(() => {
|
|
40720
|
+
signal?.removeEventListener("abort", onAbort);
|
|
40721
|
+
resolve2();
|
|
40722
|
+
}, ms);
|
|
40723
|
+
const onAbort = () => {
|
|
40724
|
+
clearTimeout(timer);
|
|
40725
|
+
reject(new RemoteAccountLoginCancelled());
|
|
40726
|
+
};
|
|
40727
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
40728
|
+
});
|
|
40729
|
+
}
|
|
40730
|
+
async function startRemoteAccountLogin(opts) {
|
|
40731
|
+
if (opts.signal?.aborted) throw new RemoteAccountLoginCancelled();
|
|
40732
|
+
const origin = (opts.origin ?? remoteAccountHttpOrigin()).replace(/\/+$/, "");
|
|
40733
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
40734
|
+
const state = (0, import_node_crypto27.randomBytes)(16).toString("hex");
|
|
40735
|
+
const timeoutMs = opts.timeoutMs ?? 5 * 6e4;
|
|
40736
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 400;
|
|
40737
|
+
const linkToken = opts.linkToken?.trim() ?? "";
|
|
40738
|
+
const started = await fetchImpl(`${origin}${REMOTE_LOGIN_PATH}`, {
|
|
40739
|
+
method: "POST",
|
|
40740
|
+
headers: { "Content-Type": "application/json" },
|
|
40741
|
+
body: JSON.stringify({
|
|
40742
|
+
provider: opts.provider,
|
|
40743
|
+
state,
|
|
40744
|
+
...linkToken ? { accountToken: linkToken } : {}
|
|
40745
|
+
})
|
|
40746
|
+
});
|
|
40747
|
+
const startBody = await started.json();
|
|
40748
|
+
if (!started.ok || !startBody.url) {
|
|
40749
|
+
throw new Error(startBody.error || "Could not start sign-in.");
|
|
40750
|
+
}
|
|
40751
|
+
await Promise.resolve(opts.openUrl?.(startBody.url));
|
|
40752
|
+
const deadline = Date.now() + timeoutMs;
|
|
40753
|
+
const resultUrl = `${origin}${REMOTE_OAUTH_RESULT_PATH}?state=${encodeURIComponent(state)}`;
|
|
40754
|
+
while (Date.now() < deadline) {
|
|
40755
|
+
if (opts.signal?.aborted) throw new RemoteAccountLoginCancelled();
|
|
40756
|
+
let res;
|
|
40757
|
+
try {
|
|
40758
|
+
res = await fetchImpl(resultUrl);
|
|
40759
|
+
} catch {
|
|
40760
|
+
if (opts.signal?.aborted) throw new RemoteAccountLoginCancelled();
|
|
40761
|
+
await sleep2(pollIntervalMs, opts.signal);
|
|
40762
|
+
continue;
|
|
40763
|
+
}
|
|
40764
|
+
let data;
|
|
40765
|
+
try {
|
|
40766
|
+
data = await res.json();
|
|
40767
|
+
} catch {
|
|
40768
|
+
await sleep2(pollIntervalMs, opts.signal);
|
|
40769
|
+
continue;
|
|
40770
|
+
}
|
|
40771
|
+
if (data.ok && data.credential && data.accountId) {
|
|
40772
|
+
return {
|
|
40773
|
+
credential: data.credential,
|
|
40774
|
+
email: data.email ?? null,
|
|
40775
|
+
accountId: data.accountId
|
|
40776
|
+
};
|
|
40777
|
+
}
|
|
40778
|
+
if (data.error && data.error !== "pending") throw new Error(data.error);
|
|
40779
|
+
await sleep2(pollIntervalMs, opts.signal);
|
|
40780
|
+
}
|
|
40781
|
+
throw new Error("Sign-in timed out.");
|
|
40782
|
+
}
|
|
40783
|
+
async function disconnectRemoteAccount(opts) {
|
|
40784
|
+
const origin = (opts.origin ?? remoteAccountHttpOrigin()).replace(/\/+$/, "");
|
|
40785
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
40786
|
+
await fetchImpl(`${origin}${REMOTE_LOGOUT_PATH}`, {
|
|
40787
|
+
method: "POST",
|
|
40788
|
+
headers: { "Content-Type": "application/json" },
|
|
40789
|
+
body: JSON.stringify({ accountToken: opts.accountToken })
|
|
40790
|
+
});
|
|
40791
|
+
}
|
|
40792
|
+
|
|
39053
40793
|
// src/slack/app-home.ts
|
|
39054
40794
|
var SLACK_APP_HOME_DOWNLOAD = "https://sideboard-downloads.t3.tigrisfiles.io/Sideboard-latest-arm64.dmg";
|
|
39055
40795
|
var SLACK_APP_HOME_LANDING = "https://www.sideboard.cloud/slack/";
|
|
@@ -39431,9 +41171,9 @@ var import_node_http5 = require("http");
|
|
|
39431
41171
|
var import_ws6 = require("ws");
|
|
39432
41172
|
|
|
39433
41173
|
// src/relay/static.ts
|
|
39434
|
-
var
|
|
41174
|
+
var import_node_fs71 = require("fs");
|
|
39435
41175
|
var import_promises = require("fs/promises");
|
|
39436
|
-
var
|
|
41176
|
+
var import_node_path69 = __toESM(require("path"), 1);
|
|
39437
41177
|
var TYPES = {
|
|
39438
41178
|
".css": "text/css; charset=utf-8",
|
|
39439
41179
|
".html": "text/html; charset=utf-8",
|
|
@@ -39464,9 +41204,9 @@ function resolveStaticPath(root, requestUrl) {
|
|
|
39464
41204
|
return null;
|
|
39465
41205
|
}
|
|
39466
41206
|
if (!pathname.startsWith("/") || pathname.includes("\0")) return null;
|
|
39467
|
-
const rootResolved =
|
|
39468
|
-
const candidate =
|
|
39469
|
-
if (candidate !== rootResolved && !candidate.startsWith(rootResolved +
|
|
41207
|
+
const rootResolved = import_node_path69.default.resolve(root);
|
|
41208
|
+
const candidate = import_node_path69.default.resolve(rootResolved, `.${pathname}`);
|
|
41209
|
+
if (candidate !== rootResolved && !candidate.startsWith(rootResolved + import_node_path69.default.sep)) {
|
|
39470
41210
|
return null;
|
|
39471
41211
|
}
|
|
39472
41212
|
return candidate;
|
|
@@ -39480,7 +41220,7 @@ async function fileSize(file) {
|
|
|
39480
41220
|
}
|
|
39481
41221
|
}
|
|
39482
41222
|
function sendFile(req, res, file, size) {
|
|
39483
|
-
const ext =
|
|
41223
|
+
const ext = import_node_path69.default.extname(file).toLowerCase();
|
|
39484
41224
|
res.writeHead(200, {
|
|
39485
41225
|
"Content-Type": TYPES[ext] ?? "application/octet-stream",
|
|
39486
41226
|
"Content-Length": size,
|
|
@@ -39490,7 +41230,7 @@ function sendFile(req, res, file, size) {
|
|
|
39490
41230
|
res.end();
|
|
39491
41231
|
return true;
|
|
39492
41232
|
}
|
|
39493
|
-
(0,
|
|
41233
|
+
(0, import_node_fs71.createReadStream)(file).pipe(res);
|
|
39494
41234
|
return true;
|
|
39495
41235
|
}
|
|
39496
41236
|
async function tryServeStatic(req, res, root) {
|
|
@@ -39499,7 +41239,7 @@ async function tryServeStatic(req, res, root) {
|
|
|
39499
41239
|
if (!candidate) return false;
|
|
39500
41240
|
const direct = await fileSize(candidate);
|
|
39501
41241
|
if (direct != null) return sendFile(req, res, candidate, direct);
|
|
39502
|
-
const asIndex =
|
|
41242
|
+
const asIndex = import_node_path69.default.join(candidate, "index.html");
|
|
39503
41243
|
const indexSize = await fileSize(asIndex);
|
|
39504
41244
|
if (indexSize != null) return sendFile(req, res, asIndex, indexSize);
|
|
39505
41245
|
return false;
|
|
@@ -39507,14 +41247,19 @@ async function tryServeStatic(req, res, root) {
|
|
|
39507
41247
|
|
|
39508
41248
|
// src/relay/server.ts
|
|
39509
41249
|
init_abletime_oauth();
|
|
39510
|
-
function
|
|
41250
|
+
function sendHtml2(res, status, title, body) {
|
|
39511
41251
|
res.writeHead(status, { "Content-Type": "text/html; charset=utf-8" });
|
|
39512
41252
|
res.end(slackOAuthHtmlPage(title, body));
|
|
39513
41253
|
}
|
|
39514
|
-
function
|
|
41254
|
+
function sendJson3(res, status, body) {
|
|
39515
41255
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
39516
41256
|
res.end(JSON.stringify(body));
|
|
39517
41257
|
}
|
|
41258
|
+
function relayClientIp(req) {
|
|
41259
|
+
const fly = req.headers["fly-client-ip"];
|
|
41260
|
+
if (typeof fly === "string" && fly.trim()) return fly.trim();
|
|
41261
|
+
return req.socket.remoteAddress ?? "unknown";
|
|
41262
|
+
}
|
|
39518
41263
|
function startRelaySocketPing(ws, intervalMs) {
|
|
39519
41264
|
const ping = setInterval(() => {
|
|
39520
41265
|
if (ws.readyState !== ws.OPEN) {
|
|
@@ -39542,11 +41287,26 @@ async function startRelayServer(opts) {
|
|
|
39542
41287
|
const clientSecret = (opts.clientSecret ?? process.env.SIDEBOARD_SLACK_CLIENT_SECRET ?? "").trim();
|
|
39543
41288
|
const redirectUri = (opts.oauthRedirectUri ?? slackOAuthRelayRedirectUri()).trim();
|
|
39544
41289
|
const pending = new SlackOAuthPendingStore();
|
|
41290
|
+
let boundPort = 0;
|
|
39545
41291
|
const hub = opts.hub ?? new SlackRelayHub({
|
|
39546
41292
|
fetchImpl: opts.fetchImpl ?? fetch,
|
|
39547
41293
|
onLog: log
|
|
39548
41294
|
});
|
|
39549
|
-
const
|
|
41295
|
+
const accountsPath = opts.accountsPath?.trim() ?? "";
|
|
41296
|
+
if (accountsPath) assertRelayAccountsDurable(accountsPath);
|
|
41297
|
+
const accountStore = accountsPath ? new RelayAccountStore(accountsPath) : null;
|
|
41298
|
+
const accountSessions = new RelayLoginSessions();
|
|
41299
|
+
const relayOAuth = opts.relayOAuth ?? {};
|
|
41300
|
+
const authMarks = /* @__PURE__ */ new Map();
|
|
41301
|
+
const remoteByIp = /* @__PURE__ */ new Map();
|
|
41302
|
+
let remoteSockets = 0;
|
|
41303
|
+
const remoteHub = new RemoteHub({
|
|
41304
|
+
authorizeHost: accountStore ? (input) => {
|
|
41305
|
+
const decision = accountStore.authorizeHost(input);
|
|
41306
|
+
return decision.ok ? { ok: true } : { ok: false, message: decision.message };
|
|
41307
|
+
} : void 0,
|
|
41308
|
+
onAuthorized: (socket) => authMarks.get(socket)?.()
|
|
41309
|
+
});
|
|
39550
41310
|
const handleCallback = async (reqUrl, res) => {
|
|
39551
41311
|
const url2 = parseSlackOAuthCallbackUrl(reqUrl);
|
|
39552
41312
|
if (!url2) return false;
|
|
@@ -39555,7 +41315,7 @@ async function startRelayServer(opts) {
|
|
|
39555
41315
|
const code = url2.searchParams.get("code")?.trim() ?? "";
|
|
39556
41316
|
if (error) {
|
|
39557
41317
|
if (state) pending.put(state, { ok: false, error: `Slack OAuth: ${error}` });
|
|
39558
|
-
|
|
41318
|
+
sendHtml2(
|
|
39559
41319
|
res,
|
|
39560
41320
|
400,
|
|
39561
41321
|
"Slack",
|
|
@@ -39564,7 +41324,7 @@ async function startRelayServer(opts) {
|
|
|
39564
41324
|
return true;
|
|
39565
41325
|
}
|
|
39566
41326
|
if (!state || !code) {
|
|
39567
|
-
|
|
41327
|
+
sendHtml2(res, 400, "Slack", "<h1>Invalid callback</h1><p>State or code missing.</p>");
|
|
39568
41328
|
return true;
|
|
39569
41329
|
}
|
|
39570
41330
|
if (!clientSecret) {
|
|
@@ -39572,7 +41332,7 @@ async function startRelayServer(opts) {
|
|
|
39572
41332
|
ok: false,
|
|
39573
41333
|
error: "Relay is missing SIDEBOARD_SLACK_CLIENT_SECRET"
|
|
39574
41334
|
});
|
|
39575
|
-
|
|
41335
|
+
sendHtml2(
|
|
39576
41336
|
res,
|
|
39577
41337
|
500,
|
|
39578
41338
|
"Slack",
|
|
@@ -39595,7 +41355,7 @@ async function startRelayServer(opts) {
|
|
|
39595
41355
|
void hub.handleAppHomeOpened(payload.team_id, payload.user_id);
|
|
39596
41356
|
}
|
|
39597
41357
|
}
|
|
39598
|
-
|
|
41358
|
+
sendHtml2(
|
|
39599
41359
|
res,
|
|
39600
41360
|
200,
|
|
39601
41361
|
"Slack connected",
|
|
@@ -39604,7 +41364,7 @@ async function startRelayServer(opts) {
|
|
|
39604
41364
|
} catch (err) {
|
|
39605
41365
|
const message = err instanceof Error ? err.message : String(err);
|
|
39606
41366
|
pending.put(state, { ok: false, error: message });
|
|
39607
|
-
|
|
41367
|
+
sendHtml2(res, 400, "Slack", `<h1>Could not connect Slack</h1><p>${escapeHtml2(message)}</p>`);
|
|
39608
41368
|
}
|
|
39609
41369
|
return true;
|
|
39610
41370
|
};
|
|
@@ -39614,19 +41374,30 @@ async function startRelayServer(opts) {
|
|
|
39614
41374
|
const state = url2.searchParams.get("state")?.trim() ?? "";
|
|
39615
41375
|
const value = pending.take(state);
|
|
39616
41376
|
if (!value) {
|
|
39617
|
-
|
|
41377
|
+
sendJson3(res, 404, { ok: false, error: "pending" });
|
|
39618
41378
|
return true;
|
|
39619
41379
|
}
|
|
39620
41380
|
if (!value.ok) {
|
|
39621
|
-
|
|
41381
|
+
sendJson3(res, 400, { ok: false, error: value.error });
|
|
39622
41382
|
return true;
|
|
39623
41383
|
}
|
|
39624
|
-
|
|
41384
|
+
sendJson3(res, 200, { ok: true, ...value.payload });
|
|
39625
41385
|
return true;
|
|
39626
41386
|
};
|
|
39627
41387
|
const httpServer = (0, import_node_http5.createServer)((req, res) => {
|
|
39628
41388
|
void (async () => {
|
|
39629
41389
|
const reqUrl = req.url || "/";
|
|
41390
|
+
if (accountStore && await handleRelayAccountHttp({
|
|
41391
|
+
req,
|
|
41392
|
+
res,
|
|
41393
|
+
store: accountStore,
|
|
41394
|
+
oauth: relayOAuth,
|
|
41395
|
+
sessions: accountSessions,
|
|
41396
|
+
publicOrigin: opts.relayPublicOrigin?.trim() || process.env.SIDEBOARD_RELAY_PUBLIC_ORIGIN?.trim() || `http://127.0.0.1:${boundPort}`,
|
|
41397
|
+
fetchImpl: opts.fetchImpl
|
|
41398
|
+
})) {
|
|
41399
|
+
return;
|
|
41400
|
+
}
|
|
39630
41401
|
if (await handleCallback(reqUrl, res)) return;
|
|
39631
41402
|
if (handleResult(reqUrl, res)) return;
|
|
39632
41403
|
const ableTimeBounce = ableTimeOAuthLocalBounceUrl(reqUrl);
|
|
@@ -39640,7 +41411,7 @@ async function startRelayServer(opts) {
|
|
|
39640
41411
|
return;
|
|
39641
41412
|
}
|
|
39642
41413
|
if (req.url === "/health") {
|
|
39643
|
-
|
|
41414
|
+
sendJson3(res, 200, {
|
|
39644
41415
|
ok: true,
|
|
39645
41416
|
service: "sideboard-relay",
|
|
39646
41417
|
sessions: hub.listSessions().length,
|
|
@@ -39659,7 +41430,7 @@ async function startRelayServer(opts) {
|
|
|
39659
41430
|
return;
|
|
39660
41431
|
}
|
|
39661
41432
|
if (req.url === "/") {
|
|
39662
|
-
|
|
41433
|
+
sendJson3(res, 200, {
|
|
39663
41434
|
ok: true,
|
|
39664
41435
|
service: "sideboard-relay",
|
|
39665
41436
|
sessions: hub.listSessions().length,
|
|
@@ -39686,6 +41457,14 @@ async function startRelayServer(opts) {
|
|
|
39686
41457
|
socket.destroy();
|
|
39687
41458
|
return;
|
|
39688
41459
|
}
|
|
41460
|
+
if (target === remoteWss) {
|
|
41461
|
+
const ip = relayClientIp(req);
|
|
41462
|
+
const count = remoteByIp.get(ip) ?? 0;
|
|
41463
|
+
if (count >= 20 || remoteSockets >= 4e3) {
|
|
41464
|
+
socket.destroy();
|
|
41465
|
+
return;
|
|
41466
|
+
}
|
|
41467
|
+
}
|
|
39689
41468
|
target.handleUpgrade(req, socket, head, (ws) => {
|
|
39690
41469
|
target.emit("connection", ws, req);
|
|
39691
41470
|
});
|
|
@@ -39720,7 +41499,10 @@ async function startRelayServer(opts) {
|
|
|
39720
41499
|
hub.detachSocket(socket);
|
|
39721
41500
|
});
|
|
39722
41501
|
});
|
|
39723
|
-
remoteWss.on("connection", (ws) => {
|
|
41502
|
+
remoteWss.on("connection", (ws, req) => {
|
|
41503
|
+
const ip = relayClientIp(req);
|
|
41504
|
+
remoteByIp.set(ip, (remoteByIp.get(ip) ?? 0) + 1);
|
|
41505
|
+
remoteSockets += 1;
|
|
39724
41506
|
const socket = {
|
|
39725
41507
|
send: (data) => {
|
|
39726
41508
|
if (ws.readyState === ws.OPEN) ws.send(data);
|
|
@@ -39732,14 +41514,35 @@ async function startRelayServer(opts) {
|
|
|
39732
41514
|
}
|
|
39733
41515
|
}
|
|
39734
41516
|
};
|
|
41517
|
+
let released = false;
|
|
41518
|
+
const authTimer = accountStore ? setTimeout(() => {
|
|
41519
|
+
try {
|
|
41520
|
+
ws.close(4001, "sign in required");
|
|
41521
|
+
} catch {
|
|
41522
|
+
}
|
|
41523
|
+
}, 12e4) : null;
|
|
41524
|
+
authMarks.set(socket, () => {
|
|
41525
|
+
if (authTimer) clearTimeout(authTimer);
|
|
41526
|
+
});
|
|
41527
|
+
const release = () => {
|
|
41528
|
+
if (released) return;
|
|
41529
|
+
released = true;
|
|
41530
|
+
if (authTimer) clearTimeout(authTimer);
|
|
41531
|
+
authMarks.delete(socket);
|
|
41532
|
+
remoteSockets -= 1;
|
|
41533
|
+
const next = (remoteByIp.get(ip) ?? 1) - 1;
|
|
41534
|
+
if (next <= 0) remoteByIp.delete(ip);
|
|
41535
|
+
else remoteByIp.set(ip, next);
|
|
41536
|
+
remoteHub.detachSocket(socket);
|
|
41537
|
+
};
|
|
39735
41538
|
startRelaySocketPing(ws, REMOTE_PING_INTERVAL_MS);
|
|
39736
41539
|
log("remote client connected");
|
|
39737
41540
|
ws.on("message", (data) => {
|
|
39738
41541
|
const raw = typeof data === "string" ? data : data.toString("utf8");
|
|
39739
41542
|
remoteHub.handleClientMessage(socket, raw);
|
|
39740
41543
|
});
|
|
39741
|
-
ws.on("close",
|
|
39742
|
-
ws.on("error",
|
|
41544
|
+
ws.on("close", release);
|
|
41545
|
+
ws.on("error", release);
|
|
39743
41546
|
});
|
|
39744
41547
|
const port = opts.port ?? 0;
|
|
39745
41548
|
const host = opts.host ?? "0.0.0.0";
|
|
@@ -39748,7 +41551,7 @@ async function startRelayServer(opts) {
|
|
|
39748
41551
|
httpServer.listen(port, host, () => resolve2());
|
|
39749
41552
|
});
|
|
39750
41553
|
const address = httpServer.address();
|
|
39751
|
-
|
|
41554
|
+
boundPort = typeof address === "object" && address ? address.port : typeof port === "number" ? port : 0;
|
|
39752
41555
|
const url = `ws://${host === "0.0.0.0" ? "127.0.0.1" : host}:${boundPort}${SLACK_RELAY_DESKTOP_PATH}`;
|
|
39753
41556
|
log(`relay listening on ${url}`);
|
|
39754
41557
|
const ac = new AbortController();
|
|
@@ -39791,13 +41594,14 @@ async function startRelayServer(opts) {
|
|
|
39791
41594
|
httpServer.close((err) => err ? reject(err) : resolve2());
|
|
39792
41595
|
});
|
|
39793
41596
|
await socketModeDone.catch(() => void 0);
|
|
41597
|
+
accountStore?.close();
|
|
39794
41598
|
}
|
|
39795
41599
|
};
|
|
39796
41600
|
}
|
|
39797
41601
|
|
|
39798
41602
|
// src/slack/outbound-watch.ts
|
|
39799
|
-
var
|
|
39800
|
-
var
|
|
41603
|
+
var import_node_fs72 = require("fs");
|
|
41604
|
+
var import_node_path70 = require("path");
|
|
39801
41605
|
init_paths();
|
|
39802
41606
|
init_private_file();
|
|
39803
41607
|
init_secure_file();
|
|
@@ -39810,7 +41614,7 @@ var POLL_INTERVAL_MS = 12e3;
|
|
|
39810
41614
|
var lastPollMs = 0;
|
|
39811
41615
|
var nameCache = /* @__PURE__ */ new Map();
|
|
39812
41616
|
function storePath4() {
|
|
39813
|
-
return (0,
|
|
41617
|
+
return (0, import_node_path70.join)(appDataDir(), "slack-outbound-watch.json");
|
|
39814
41618
|
}
|
|
39815
41619
|
function watchId(teamId, channelId, ts) {
|
|
39816
41620
|
return `${teamId}:${channelId}:${ts}`;
|
|
@@ -39823,9 +41627,9 @@ function tsNewer(a, b) {
|
|
|
39823
41627
|
}
|
|
39824
41628
|
function readStore4() {
|
|
39825
41629
|
const path2 = storePath4();
|
|
39826
|
-
if (!(0,
|
|
41630
|
+
if (!(0, import_node_fs72.existsSync)(path2)) return [];
|
|
39827
41631
|
try {
|
|
39828
|
-
const parsed = isSecureFileEncrypted(path2) ? readSecureJson(path2) : JSON.parse((0,
|
|
41632
|
+
const parsed = isSecureFileEncrypted(path2) ? readSecureJson(path2) : JSON.parse((0, import_node_fs72.readFileSync)(path2, "utf8"));
|
|
39829
41633
|
return Array.isArray(parsed?.watches) ? parsed.watches : [];
|
|
39830
41634
|
} catch {
|
|
39831
41635
|
return [];
|
|
@@ -40470,6 +42274,7 @@ async function pollSlackOutboundWatches(opts) {
|
|
|
40470
42274
|
disconnectLinear,
|
|
40471
42275
|
disconnectLinearConnection,
|
|
40472
42276
|
disconnectOptionalService,
|
|
42277
|
+
disconnectRemoteAccount,
|
|
40473
42278
|
disconnectSlackWorkspace,
|
|
40474
42279
|
discoverSkills,
|
|
40475
42280
|
downloadAbleTimeAttachment,
|
|
@@ -40726,6 +42531,7 @@ async function pollSlackOutboundWatches(opts) {
|
|
|
40726
42531
|
isPrNotMergeableError,
|
|
40727
42532
|
isPresentPlanToolName,
|
|
40728
42533
|
isPrimaryCheckoutThread,
|
|
42534
|
+
isRemoteAccountLoginCancelled,
|
|
40729
42535
|
isRemoteStopCommand,
|
|
40730
42536
|
isSelfWrittenRecord,
|
|
40731
42537
|
isSessionQuotaLimit,
|
|
@@ -41087,6 +42893,7 @@ async function pollSlackOutboundWatches(opts) {
|
|
|
41087
42893
|
startMcpServer,
|
|
41088
42894
|
startOrchestration,
|
|
41089
42895
|
startRelayServer,
|
|
42896
|
+
startRemoteAccountLogin,
|
|
41090
42897
|
startSlackOAuth,
|
|
41091
42898
|
statWorktreePath,
|
|
41092
42899
|
stopAgentRpcServer,
|