flightdesk 0.3.0 → 0.3.2
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/README.md +8 -30
- package/main.js +45 -1167
- package/main.js.map +4 -4
- package/package.json +1 -4
package/main.js
CHANGED
|
@@ -958,8 +958,8 @@ var require_command = __commonJS({
|
|
|
958
958
|
"node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/command.js"(exports2) {
|
|
959
959
|
var EventEmitter = require("node:events").EventEmitter;
|
|
960
960
|
var childProcess = require("node:child_process");
|
|
961
|
-
var
|
|
962
|
-
var
|
|
961
|
+
var path3 = require("node:path");
|
|
962
|
+
var fs3 = require("node:fs");
|
|
963
963
|
var process2 = require("node:process");
|
|
964
964
|
var { Argument: Argument2, humanReadableArgName } = require_argument();
|
|
965
965
|
var { CommanderError: CommanderError2 } = require_error();
|
|
@@ -1891,11 +1891,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
1891
1891
|
let launchWithNode = false;
|
|
1892
1892
|
const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
|
|
1893
1893
|
function findFile(baseDir, baseName) {
|
|
1894
|
-
const localBin =
|
|
1895
|
-
if (
|
|
1896
|
-
if (sourceExt.includes(
|
|
1894
|
+
const localBin = path3.resolve(baseDir, baseName);
|
|
1895
|
+
if (fs3.existsSync(localBin)) return localBin;
|
|
1896
|
+
if (sourceExt.includes(path3.extname(baseName))) return void 0;
|
|
1897
1897
|
const foundExt = sourceExt.find(
|
|
1898
|
-
(ext) =>
|
|
1898
|
+
(ext) => fs3.existsSync(`${localBin}${ext}`)
|
|
1899
1899
|
);
|
|
1900
1900
|
if (foundExt) return `${localBin}${foundExt}`;
|
|
1901
1901
|
return void 0;
|
|
@@ -1907,21 +1907,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
1907
1907
|
if (this._scriptPath) {
|
|
1908
1908
|
let resolvedScriptPath;
|
|
1909
1909
|
try {
|
|
1910
|
-
resolvedScriptPath =
|
|
1910
|
+
resolvedScriptPath = fs3.realpathSync(this._scriptPath);
|
|
1911
1911
|
} catch (err) {
|
|
1912
1912
|
resolvedScriptPath = this._scriptPath;
|
|
1913
1913
|
}
|
|
1914
|
-
executableDir =
|
|
1915
|
-
|
|
1914
|
+
executableDir = path3.resolve(
|
|
1915
|
+
path3.dirname(resolvedScriptPath),
|
|
1916
1916
|
executableDir
|
|
1917
1917
|
);
|
|
1918
1918
|
}
|
|
1919
1919
|
if (executableDir) {
|
|
1920
1920
|
let localFile = findFile(executableDir, executableFile);
|
|
1921
1921
|
if (!localFile && !subcommand._executableFile && this._scriptPath) {
|
|
1922
|
-
const legacyName =
|
|
1922
|
+
const legacyName = path3.basename(
|
|
1923
1923
|
this._scriptPath,
|
|
1924
|
-
|
|
1924
|
+
path3.extname(this._scriptPath)
|
|
1925
1925
|
);
|
|
1926
1926
|
if (legacyName !== this._name) {
|
|
1927
1927
|
localFile = findFile(
|
|
@@ -1932,7 +1932,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
1932
1932
|
}
|
|
1933
1933
|
executableFile = localFile || executableFile;
|
|
1934
1934
|
}
|
|
1935
|
-
launchWithNode = sourceExt.includes(
|
|
1935
|
+
launchWithNode = sourceExt.includes(path3.extname(executableFile));
|
|
1936
1936
|
let proc;
|
|
1937
1937
|
if (process2.platform !== "win32") {
|
|
1938
1938
|
if (launchWithNode) {
|
|
@@ -2772,7 +2772,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2772
2772
|
* @return {Command}
|
|
2773
2773
|
*/
|
|
2774
2774
|
nameFromFilename(filename) {
|
|
2775
|
-
this._name =
|
|
2775
|
+
this._name = path3.basename(filename, path3.extname(filename));
|
|
2776
2776
|
return this;
|
|
2777
2777
|
}
|
|
2778
2778
|
/**
|
|
@@ -2786,9 +2786,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2786
2786
|
* @param {string} [path]
|
|
2787
2787
|
* @return {(string|null|Command)}
|
|
2788
2788
|
*/
|
|
2789
|
-
executableDir(
|
|
2790
|
-
if (
|
|
2791
|
-
this._executableDir =
|
|
2789
|
+
executableDir(path4) {
|
|
2790
|
+
if (path4 === void 0) return this._executableDir;
|
|
2791
|
+
this._executableDir = path4;
|
|
2792
2792
|
return this;
|
|
2793
2793
|
}
|
|
2794
2794
|
/**
|
|
@@ -3582,526 +3582,11 @@ Select active organization (1-${organizations.length}): `);
|
|
|
3582
3582
|
}
|
|
3583
3583
|
}
|
|
3584
3584
|
|
|
3585
|
-
// apps/cli/src/lib/session-monitor.ts
|
|
3586
|
-
var path2 = __toESM(require("node:path"));
|
|
3587
|
-
var os2 = __toESM(require("node:os"));
|
|
3588
|
-
var fs2 = __toESM(require("node:fs"));
|
|
3589
|
-
var readline2 = __toESM(require("node:readline"));
|
|
3590
|
-
var import_node_child_process = require("node:child_process");
|
|
3591
|
-
var playwright = null;
|
|
3592
|
-
var PlaywrightBrowserNotInstalledError = class extends Error {
|
|
3593
|
-
constructor(autoInstallError) {
|
|
3594
|
-
const baseMessage = "Playwright browser not installed.";
|
|
3595
|
-
const autoInstallInfo = autoInstallError ? `
|
|
3596
|
-
|
|
3597
|
-
Auto-install failed: ${autoInstallError}
|
|
3598
|
-
` : "\n\n";
|
|
3599
|
-
super(
|
|
3600
|
-
baseMessage + autoInstallInfo + "Run one of the following commands:\n\n npx playwright install chromium # Just Chromium (recommended)\n npx playwright install # All browsers\n\nThen retry your command."
|
|
3601
|
-
);
|
|
3602
|
-
this.name = "PlaywrightBrowserNotInstalledError";
|
|
3603
|
-
}
|
|
3604
|
-
};
|
|
3605
|
-
function isBrowserNotInstalledError(error) {
|
|
3606
|
-
if (!(error instanceof Error)) return false;
|
|
3607
|
-
return error.message.includes("Executable doesn't exist") || error.message.includes("browserType.launch") || error.message.includes("npx playwright install");
|
|
3608
|
-
}
|
|
3609
|
-
var autoInstallAttempted = false;
|
|
3610
|
-
function tryAutoInstallBrowsers() {
|
|
3611
|
-
if (autoInstallAttempted) {
|
|
3612
|
-
return false;
|
|
3613
|
-
}
|
|
3614
|
-
autoInstallAttempted = true;
|
|
3615
|
-
console.log("");
|
|
3616
|
-
console.log("\u{1F4E6} Playwright browser not found. Installing automatically...");
|
|
3617
|
-
console.log("");
|
|
3618
|
-
try {
|
|
3619
|
-
(0, import_node_child_process.execSync)("npx playwright install chromium", {
|
|
3620
|
-
stdio: "inherit",
|
|
3621
|
-
timeout: 12e4
|
|
3622
|
-
// 2 minute timeout
|
|
3623
|
-
});
|
|
3624
|
-
console.log("");
|
|
3625
|
-
console.log("\u2705 Browser installed successfully!");
|
|
3626
|
-
console.log("");
|
|
3627
|
-
return true;
|
|
3628
|
-
} catch (error) {
|
|
3629
|
-
console.error("");
|
|
3630
|
-
console.error("\u274C Auto-install failed:", error instanceof Error ? error.message : String(error));
|
|
3631
|
-
console.error("");
|
|
3632
|
-
return false;
|
|
3633
|
-
}
|
|
3634
|
-
}
|
|
3635
|
-
var USER_DATA_DIR = path2.join(os2.homedir(), ".flightdesk", "chromium-profile");
|
|
3636
|
-
var STORAGE_STATE_FILE = path2.join(os2.homedir(), ".flightdesk", "auth-state.json");
|
|
3637
|
-
var PersistentBrowser = class {
|
|
3638
|
-
constructor(headless = true) {
|
|
3639
|
-
this.browser = null;
|
|
3640
|
-
this.context = null;
|
|
3641
|
-
this.page = null;
|
|
3642
|
-
this.headless = headless;
|
|
3643
|
-
}
|
|
3644
|
-
/**
|
|
3645
|
-
* Get page, throwing if not initialized
|
|
3646
|
-
*/
|
|
3647
|
-
get activePage() {
|
|
3648
|
-
if (!this.page) {
|
|
3649
|
-
throw new Error("Browser not initialized. Call init() first.");
|
|
3650
|
-
}
|
|
3651
|
-
return this.page;
|
|
3652
|
-
}
|
|
3653
|
-
/**
|
|
3654
|
-
* Initialize the browser context (if not already initialized)
|
|
3655
|
-
*/
|
|
3656
|
-
async init() {
|
|
3657
|
-
if (this.context) return;
|
|
3658
|
-
if (!await isPlaywrightAvailable() || !playwright) {
|
|
3659
|
-
throw new Error("Playwright not available");
|
|
3660
|
-
}
|
|
3661
|
-
ensureUserDataDir();
|
|
3662
|
-
try {
|
|
3663
|
-
this.browser = await playwright.chromium.launch({
|
|
3664
|
-
headless: this.headless
|
|
3665
|
-
});
|
|
3666
|
-
} catch (error) {
|
|
3667
|
-
if (isBrowserNotInstalledError(error)) {
|
|
3668
|
-
if (tryAutoInstallBrowsers()) {
|
|
3669
|
-
try {
|
|
3670
|
-
this.browser = await playwright.chromium.launch({
|
|
3671
|
-
headless: this.headless
|
|
3672
|
-
});
|
|
3673
|
-
} catch (retryError) {
|
|
3674
|
-
throw new PlaywrightBrowserNotInstalledError(
|
|
3675
|
-
retryError instanceof Error ? retryError.message : String(retryError)
|
|
3676
|
-
);
|
|
3677
|
-
}
|
|
3678
|
-
} else {
|
|
3679
|
-
throw new PlaywrightBrowserNotInstalledError();
|
|
3680
|
-
}
|
|
3681
|
-
} else {
|
|
3682
|
-
throw error;
|
|
3683
|
-
}
|
|
3684
|
-
}
|
|
3685
|
-
const hasAuthState = fs2.existsSync(STORAGE_STATE_FILE);
|
|
3686
|
-
const contextOptions = {
|
|
3687
|
-
viewport: { width: 1280, height: 720 },
|
|
3688
|
-
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
3689
|
-
};
|
|
3690
|
-
if (hasAuthState) {
|
|
3691
|
-
contextOptions.storageState = STORAGE_STATE_FILE;
|
|
3692
|
-
}
|
|
3693
|
-
this.context = await this.browser.newContext(contextOptions);
|
|
3694
|
-
this.page = await this.context.newPage();
|
|
3695
|
-
this.page.setDefaultTimeout(3e4);
|
|
3696
|
-
}
|
|
3697
|
-
/**
|
|
3698
|
-
* Check if user is logged into Claude
|
|
3699
|
-
*/
|
|
3700
|
-
async checkAuth() {
|
|
3701
|
-
await this.init();
|
|
3702
|
-
try {
|
|
3703
|
-
await this.activePage.goto("https://claude.ai/", { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
3704
|
-
await this.activePage.waitForTimeout(2e3);
|
|
3705
|
-
const url = this.activePage.url();
|
|
3706
|
-
console.log(" Final URL:", url);
|
|
3707
|
-
return !url.includes("/login") && !url.includes("/oauth") && !url.includes("accounts.google") && url.includes("claude.ai");
|
|
3708
|
-
} catch (error) {
|
|
3709
|
-
console.error("Auth check failed:", error.message);
|
|
3710
|
-
return false;
|
|
3711
|
-
}
|
|
3712
|
-
}
|
|
3713
|
-
/**
|
|
3714
|
-
* Monitor a Claude Code session URL
|
|
3715
|
-
* Uses shared scrapeSession helper for consistent behavior with one-shot mode.
|
|
3716
|
-
*/
|
|
3717
|
-
async monitorSession(sessionUrl, options = {}) {
|
|
3718
|
-
await this.init();
|
|
3719
|
-
if (!this.page) {
|
|
3720
|
-
return { status: "error", error: "Browser page not initialized" };
|
|
3721
|
-
}
|
|
3722
|
-
return scrapeSession(this.page, sessionUrl, options);
|
|
3723
|
-
}
|
|
3724
|
-
/**
|
|
3725
|
-
* Close the browser context and browser
|
|
3726
|
-
*/
|
|
3727
|
-
async close() {
|
|
3728
|
-
if (this.context) {
|
|
3729
|
-
try {
|
|
3730
|
-
await this.context.close();
|
|
3731
|
-
} catch {
|
|
3732
|
-
}
|
|
3733
|
-
this.context = null;
|
|
3734
|
-
this.page = null;
|
|
3735
|
-
}
|
|
3736
|
-
if (this.browser) {
|
|
3737
|
-
try {
|
|
3738
|
-
await this.browser.close();
|
|
3739
|
-
} catch {
|
|
3740
|
-
}
|
|
3741
|
-
this.browser = null;
|
|
3742
|
-
}
|
|
3743
|
-
}
|
|
3744
|
-
};
|
|
3745
|
-
async function isPlaywrightAvailable() {
|
|
3746
|
-
try {
|
|
3747
|
-
playwright = await import("playwright");
|
|
3748
|
-
return true;
|
|
3749
|
-
} catch {
|
|
3750
|
-
return false;
|
|
3751
|
-
}
|
|
3752
|
-
}
|
|
3753
|
-
function ensureUserDataDir() {
|
|
3754
|
-
if (!fs2.existsSync(USER_DATA_DIR)) {
|
|
3755
|
-
fs2.mkdirSync(USER_DATA_DIR, { recursive: true, mode: 448 });
|
|
3756
|
-
}
|
|
3757
|
-
}
|
|
3758
|
-
async function ensureStorageDir() {
|
|
3759
|
-
const storageDir = path2.dirname(STORAGE_STATE_FILE);
|
|
3760
|
-
try {
|
|
3761
|
-
await fs2.promises.mkdir(storageDir, { recursive: true, mode: 448 });
|
|
3762
|
-
} catch (err) {
|
|
3763
|
-
if (err?.code !== "EEXIST") {
|
|
3764
|
-
throw err;
|
|
3765
|
-
}
|
|
3766
|
-
}
|
|
3767
|
-
}
|
|
3768
|
-
async function setStorageFilePermissions() {
|
|
3769
|
-
try {
|
|
3770
|
-
await fs2.promises.chmod(STORAGE_STATE_FILE, 384);
|
|
3771
|
-
} catch {
|
|
3772
|
-
}
|
|
3773
|
-
}
|
|
3774
|
-
async function launchBrowser(headless) {
|
|
3775
|
-
if (!playwright) {
|
|
3776
|
-
throw new Error("Playwright not available");
|
|
3777
|
-
}
|
|
3778
|
-
ensureUserDataDir();
|
|
3779
|
-
let browser;
|
|
3780
|
-
try {
|
|
3781
|
-
browser = await playwright.chromium.launch({ headless });
|
|
3782
|
-
} catch (error) {
|
|
3783
|
-
if (isBrowserNotInstalledError(error)) {
|
|
3784
|
-
if (tryAutoInstallBrowsers()) {
|
|
3785
|
-
try {
|
|
3786
|
-
browser = await playwright.chromium.launch({ headless });
|
|
3787
|
-
} catch (retryError) {
|
|
3788
|
-
throw new PlaywrightBrowserNotInstalledError(
|
|
3789
|
-
retryError instanceof Error ? retryError.message : String(retryError)
|
|
3790
|
-
);
|
|
3791
|
-
}
|
|
3792
|
-
} else {
|
|
3793
|
-
throw new PlaywrightBrowserNotInstalledError();
|
|
3794
|
-
}
|
|
3795
|
-
} else {
|
|
3796
|
-
throw error;
|
|
3797
|
-
}
|
|
3798
|
-
}
|
|
3799
|
-
const hasAuthState = fs2.existsSync(STORAGE_STATE_FILE);
|
|
3800
|
-
const contextOptions = {
|
|
3801
|
-
viewport: { width: 1280, height: 720 },
|
|
3802
|
-
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
3803
|
-
};
|
|
3804
|
-
if (hasAuthState) {
|
|
3805
|
-
contextOptions.storageState = STORAGE_STATE_FILE;
|
|
3806
|
-
}
|
|
3807
|
-
const context = await browser.newContext(contextOptions);
|
|
3808
|
-
return { browser, context };
|
|
3809
|
-
}
|
|
3810
|
-
async function checkAuth() {
|
|
3811
|
-
if (!await isPlaywrightAvailable()) {
|
|
3812
|
-
throw new Error("Playwright not installed");
|
|
3813
|
-
}
|
|
3814
|
-
const { browser, context } = await launchBrowser(true);
|
|
3815
|
-
try {
|
|
3816
|
-
const page = await context.newPage();
|
|
3817
|
-
page.setDefaultTimeout(3e4);
|
|
3818
|
-
await page.goto("https://claude.ai/", { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
3819
|
-
await page.waitForTimeout(2e3);
|
|
3820
|
-
const url = page.url();
|
|
3821
|
-
console.log(" Final URL:", url);
|
|
3822
|
-
const isLoggedIn = !url.includes("/login") && !url.includes("/oauth") && !url.includes("accounts.google") && url.includes("claude.ai");
|
|
3823
|
-
return isLoggedIn;
|
|
3824
|
-
} catch (error) {
|
|
3825
|
-
console.error("Auth check failed:", error.message);
|
|
3826
|
-
return false;
|
|
3827
|
-
} finally {
|
|
3828
|
-
await context.close();
|
|
3829
|
-
await browser.close();
|
|
3830
|
-
}
|
|
3831
|
-
}
|
|
3832
|
-
async function openForLogin() {
|
|
3833
|
-
if (!await isPlaywrightAvailable()) {
|
|
3834
|
-
throw new Error("Playwright not installed");
|
|
3835
|
-
}
|
|
3836
|
-
console.log("Opening browser for Claude login...");
|
|
3837
|
-
const { context } = await launchBrowser(false);
|
|
3838
|
-
try {
|
|
3839
|
-
const page = await context.newPage();
|
|
3840
|
-
await page.goto("https://claude.ai/", { waitUntil: "domcontentloaded", timeout: 6e4 });
|
|
3841
|
-
console.log("\n\u{1F449} Press ENTER here when you have logged in...\n");
|
|
3842
|
-
await waitForEnter();
|
|
3843
|
-
console.log("Verifying login in current session...");
|
|
3844
|
-
await page.goto("https://claude.ai/", { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
3845
|
-
await page.waitForTimeout(2e3);
|
|
3846
|
-
const url = page.url();
|
|
3847
|
-
console.log(" Final URL:", url);
|
|
3848
|
-
const isLoggedIn = !url.includes("/login") && !url.includes("/oauth") && !url.includes("accounts.google") && url.includes("claude.ai");
|
|
3849
|
-
if (isLoggedIn) {
|
|
3850
|
-
await ensureStorageDir();
|
|
3851
|
-
console.log("Saving session state...");
|
|
3852
|
-
await context.storageState({ path: STORAGE_STATE_FILE });
|
|
3853
|
-
await setStorageFilePermissions();
|
|
3854
|
-
console.log(` Saved to: ${STORAGE_STATE_FILE}`);
|
|
3855
|
-
}
|
|
3856
|
-
console.log("Closing browser...");
|
|
3857
|
-
return isLoggedIn;
|
|
3858
|
-
} finally {
|
|
3859
|
-
try {
|
|
3860
|
-
await context.close();
|
|
3861
|
-
} catch {
|
|
3862
|
-
}
|
|
3863
|
-
}
|
|
3864
|
-
}
|
|
3865
|
-
function waitForEnter() {
|
|
3866
|
-
return new Promise((resolve) => {
|
|
3867
|
-
const rl = readline2.createInterface({
|
|
3868
|
-
input: process.stdin,
|
|
3869
|
-
output: process.stdout
|
|
3870
|
-
});
|
|
3871
|
-
rl.question("", () => {
|
|
3872
|
-
rl.close();
|
|
3873
|
-
resolve();
|
|
3874
|
-
});
|
|
3875
|
-
});
|
|
3876
|
-
}
|
|
3877
|
-
async function navigateAndPrepare(page, sessionUrl, timeout, debug) {
|
|
3878
|
-
if (debug) console.log(` [DEBUG] Navigating to: ${sessionUrl}`);
|
|
3879
|
-
await page.goto(sessionUrl, { waitUntil: "domcontentloaded", timeout });
|
|
3880
|
-
const dismissBtn = await page.$(`button:has-text("Don't ask me again"), button:has-text("Got it")`);
|
|
3881
|
-
if (dismissBtn) {
|
|
3882
|
-
if (debug) console.log(" [DEBUG] Dismissing notification modal");
|
|
3883
|
-
await dismissBtn.click();
|
|
3884
|
-
await page.waitForTimeout(500);
|
|
3885
|
-
}
|
|
3886
|
-
const matchedElement = await page.waitForSelector('[data-testid="conversation-turn"], .cursor-pointer.bg-bg-300, button:has-text("Create PR")', {
|
|
3887
|
-
timeout: 1e4
|
|
3888
|
-
}).catch(() => null);
|
|
3889
|
-
if (debug) {
|
|
3890
|
-
if (matchedElement) {
|
|
3891
|
-
const tagName = await matchedElement.evaluate((el) => `${el.tagName}.${el.className}`);
|
|
3892
|
-
console.log(` [DEBUG] waitForSelector matched: ${tagName}`);
|
|
3893
|
-
} else {
|
|
3894
|
-
console.log(" [DEBUG] waitForSelector timed out - no expected elements found");
|
|
3895
|
-
}
|
|
3896
|
-
}
|
|
3897
|
-
}
|
|
3898
|
-
async function captureDebugInfo(page) {
|
|
3899
|
-
console.log(` [DEBUG] Current URL after navigation: ${page.url()}`);
|
|
3900
|
-
const screenshotDir = path2.join(os2.homedir(), ".flightdesk", "debug-screenshots");
|
|
3901
|
-
if (!fs2.existsSync(screenshotDir)) fs2.mkdirSync(screenshotDir, { recursive: true });
|
|
3902
|
-
const screenshotPath = path2.join(screenshotDir, `session-${Date.now()}.png`);
|
|
3903
|
-
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
3904
|
-
console.log(` [DEBUG] Screenshot saved: ${screenshotPath}`);
|
|
3905
|
-
}
|
|
3906
|
-
async function autoCreatePr(page, createPrButton) {
|
|
3907
|
-
console.log(' Clicking "Create PR" button...');
|
|
3908
|
-
await createPrButton.click();
|
|
3909
|
-
await page.waitForTimeout(5e3);
|
|
3910
|
-
return await extractPrUrl(page) ?? void 0;
|
|
3911
|
-
}
|
|
3912
|
-
async function scrapeSession(page, sessionUrl, options = {}) {
|
|
3913
|
-
const { timeout = 3e4, autoPr = false } = options;
|
|
3914
|
-
try {
|
|
3915
|
-
page.setDefaultTimeout(timeout);
|
|
3916
|
-
const debug = process.env.FLIGHTDESK_DEBUG === "1";
|
|
3917
|
-
await navigateAndPrepare(page, sessionUrl, timeout, debug);
|
|
3918
|
-
if (debug) await captureDebugInfo(page);
|
|
3919
|
-
const archiveIndicator = await page.$("text=This session has been archived");
|
|
3920
|
-
if (archiveIndicator) return { status: "archived" };
|
|
3921
|
-
const url = page.url();
|
|
3922
|
-
if (url.includes("/login") || url.includes("/oauth")) {
|
|
3923
|
-
return { status: "error", error: "Not logged in to Claude. Run: flightdesk auth" };
|
|
3924
|
-
}
|
|
3925
|
-
const result = { status: "active" };
|
|
3926
|
-
result.isActive = await detectActiveSpinner(page, debug);
|
|
3927
|
-
const branchName = await extractBranchName(page);
|
|
3928
|
-
if (branchName) result.branchName = branchName;
|
|
3929
|
-
const createPrButton = await page.$('button:has-text("Create PR"):not([aria-haspopup])');
|
|
3930
|
-
const viewPrButton = await page.$('button:has-text("View PR")');
|
|
3931
|
-
result.hasPrButton = !!createPrButton && !viewPrButton;
|
|
3932
|
-
if (autoPr && createPrButton && !viewPrButton) {
|
|
3933
|
-
result.prUrl = await autoCreatePr(page, createPrButton);
|
|
3934
|
-
}
|
|
3935
|
-
return result;
|
|
3936
|
-
} catch (error) {
|
|
3937
|
-
return {
|
|
3938
|
-
status: "error",
|
|
3939
|
-
error: error instanceof Error ? error.message : String(error)
|
|
3940
|
-
};
|
|
3941
|
-
}
|
|
3942
|
-
}
|
|
3943
|
-
function isValidBranchName(text) {
|
|
3944
|
-
if (!text) return false;
|
|
3945
|
-
const cleaned = text.trim();
|
|
3946
|
-
return cleaned.length > 2 && !cleaned.includes(" ");
|
|
3947
|
-
}
|
|
3948
|
-
function matchesBranchPattern(text) {
|
|
3949
|
-
return /^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+/.test(text) && !text.includes(" ");
|
|
3950
|
-
}
|
|
3951
|
-
async function tryExtractFromElement(page, selector) {
|
|
3952
|
-
try {
|
|
3953
|
-
const element = await page.$(selector);
|
|
3954
|
-
if (!element) return null;
|
|
3955
|
-
const text = await element.textContent();
|
|
3956
|
-
const cleaned = text?.trim() ?? null;
|
|
3957
|
-
return isValidBranchName(cleaned) ? cleaned : null;
|
|
3958
|
-
} catch {
|
|
3959
|
-
return null;
|
|
3960
|
-
}
|
|
3961
|
-
}
|
|
3962
|
-
async function extractBranchName(page) {
|
|
3963
|
-
if (!page) return null;
|
|
3964
|
-
const primarySelectors = [
|
|
3965
|
-
String.raw`button.group\/copy span.truncate`,
|
|
3966
|
-
// New branch name (verified)
|
|
3967
|
-
'button[class*="group/copy"] span.truncate',
|
|
3968
|
-
// Alternative escaping
|
|
3969
|
-
String.raw`button[class*="group\/copy"] span.truncate`
|
|
3970
|
-
// CSS escape
|
|
3971
|
-
];
|
|
3972
|
-
for (const selector of primarySelectors) {
|
|
3973
|
-
const result = await tryExtractFromElement(page, selector);
|
|
3974
|
-
if (result) return result;
|
|
3975
|
-
}
|
|
3976
|
-
return await tryExtractBranchFromSpans(page);
|
|
3977
|
-
}
|
|
3978
|
-
async function tryExtractBranchFromSpans(page) {
|
|
3979
|
-
try {
|
|
3980
|
-
const allSpans = await page.$$("span.truncate");
|
|
3981
|
-
for (const span of allSpans) {
|
|
3982
|
-
const text = await span.textContent();
|
|
3983
|
-
const cleaned = text?.trim();
|
|
3984
|
-
if (cleaned && matchesBranchPattern(cleaned)) {
|
|
3985
|
-
return cleaned;
|
|
3986
|
-
}
|
|
3987
|
-
}
|
|
3988
|
-
} catch {
|
|
3989
|
-
}
|
|
3990
|
-
return null;
|
|
3991
|
-
}
|
|
3992
|
-
async function extractPrUrl(page) {
|
|
3993
|
-
const selectors = [
|
|
3994
|
-
'a[href*="github.com"][href*="/pull/"]',
|
|
3995
|
-
'a[href*="gitlab.com"][href*="/merge_requests/"]',
|
|
3996
|
-
'a[href*="bitbucket.org"][href*="/pull-requests/"]'
|
|
3997
|
-
];
|
|
3998
|
-
for (const selector of selectors) {
|
|
3999
|
-
try {
|
|
4000
|
-
const element = await page.$(selector);
|
|
4001
|
-
if (element) {
|
|
4002
|
-
const href = await element.getAttribute("href");
|
|
4003
|
-
if (href) {
|
|
4004
|
-
return href;
|
|
4005
|
-
}
|
|
4006
|
-
}
|
|
4007
|
-
} catch {
|
|
4008
|
-
}
|
|
4009
|
-
}
|
|
4010
|
-
try {
|
|
4011
|
-
const pageContent = await page.content();
|
|
4012
|
-
const prPatterns = [
|
|
4013
|
-
/(https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+)/,
|
|
4014
|
-
/(https:\/\/gitlab\.com\/[^/]+\/[^/]+\/-\/merge_requests\/\d+)/
|
|
4015
|
-
];
|
|
4016
|
-
for (const pattern of prPatterns) {
|
|
4017
|
-
const match = pageContent.match(pattern);
|
|
4018
|
-
if (match && match[1]) {
|
|
4019
|
-
return match[1];
|
|
4020
|
-
}
|
|
4021
|
-
}
|
|
4022
|
-
} catch {
|
|
4023
|
-
}
|
|
4024
|
-
return null;
|
|
4025
|
-
}
|
|
4026
|
-
async function detectActiveSpinner(page, debug = false) {
|
|
4027
|
-
try {
|
|
4028
|
-
const selectedRowSpinner = await page.$(".cursor-pointer.bg-bg-300 .code-spinner-animate");
|
|
4029
|
-
if (debug) {
|
|
4030
|
-
const globalSpinner = await page.$(".code-spinner-animate");
|
|
4031
|
-
console.log(` [DEBUG] .code-spinner-animate global: ${!!globalSpinner}, in selected row: ${!!selectedRowSpinner}`);
|
|
4032
|
-
}
|
|
4033
|
-
if (selectedRowSpinner) {
|
|
4034
|
-
if (debug) console.log(" [DEBUG] Selected sidebar row has spinner: ACTIVE");
|
|
4035
|
-
return true;
|
|
4036
|
-
}
|
|
4037
|
-
const stopButton = await page.$('button:has-text("Stop")');
|
|
4038
|
-
if (debug) console.log(` [DEBUG] Stop button found: ${!!stopButton}`);
|
|
4039
|
-
if (stopButton) {
|
|
4040
|
-
if (debug) console.log(" [DEBUG] Stop button detected: ACTIVE");
|
|
4041
|
-
return true;
|
|
4042
|
-
}
|
|
4043
|
-
if (debug) console.log(" [DEBUG] No activity indicators: IDLE");
|
|
4044
|
-
return false;
|
|
4045
|
-
} catch (error) {
|
|
4046
|
-
if (debug) console.log(` [DEBUG] Spinner detection error: ${error}`);
|
|
4047
|
-
return false;
|
|
4048
|
-
}
|
|
4049
|
-
}
|
|
4050
|
-
|
|
4051
|
-
// apps/cli/src/commands/auth.ts
|
|
4052
|
-
async function authCommand() {
|
|
4053
|
-
console.log("\u{1F510} FlightDesk Authentication\n");
|
|
4054
|
-
const playwrightAvailable = await isPlaywrightAvailable();
|
|
4055
|
-
if (!playwrightAvailable) {
|
|
4056
|
-
console.error("Playwright is not installed.");
|
|
4057
|
-
console.error("Install with: pnpm add playwright && npx playwright install chromium");
|
|
4058
|
-
process.exit(1);
|
|
4059
|
-
}
|
|
4060
|
-
console.log(`Profile directory: ${USER_DATA_DIR}
|
|
4061
|
-
`);
|
|
4062
|
-
console.log("Checking current authentication status...");
|
|
4063
|
-
try {
|
|
4064
|
-
const isAuthenticated = await checkAuth();
|
|
4065
|
-
if (isAuthenticated) {
|
|
4066
|
-
console.log("\u2705 Already logged in to Claude!");
|
|
4067
|
-
console.log("\nThe watch daemon will be able to monitor your sessions.");
|
|
4068
|
-
return;
|
|
4069
|
-
}
|
|
4070
|
-
} catch (error) {
|
|
4071
|
-
if (error instanceof PlaywrightBrowserNotInstalledError) {
|
|
4072
|
-
console.error("");
|
|
4073
|
-
console.error("\u274C " + error.message);
|
|
4074
|
-
process.exit(1);
|
|
4075
|
-
}
|
|
4076
|
-
throw error;
|
|
4077
|
-
}
|
|
4078
|
-
console.log("\u274C Not logged in to Claude.\n");
|
|
4079
|
-
console.log("Opening browser for login...");
|
|
4080
|
-
console.log("Please log in to your Claude account.\n");
|
|
4081
|
-
try {
|
|
4082
|
-
const loginSuccessful = await openForLogin();
|
|
4083
|
-
if (loginSuccessful) {
|
|
4084
|
-
console.log("\n\u2705 Successfully logged in!");
|
|
4085
|
-
console.log("The watch daemon can now monitor your Claude Code sessions.");
|
|
4086
|
-
} else {
|
|
4087
|
-
console.log("\n\u274C Login was not detected.");
|
|
4088
|
-
console.log("Please try again with: flightdesk auth");
|
|
4089
|
-
}
|
|
4090
|
-
} catch (error) {
|
|
4091
|
-
if (error instanceof PlaywrightBrowserNotInstalledError) {
|
|
4092
|
-
console.error("");
|
|
4093
|
-
console.error("\u274C " + error.message);
|
|
4094
|
-
process.exit(1);
|
|
4095
|
-
}
|
|
4096
|
-
throw error;
|
|
4097
|
-
}
|
|
4098
|
-
}
|
|
4099
|
-
|
|
4100
3585
|
// apps/cli/src/lib/git.ts
|
|
4101
|
-
var
|
|
3586
|
+
var import_node_child_process = require("node:child_process");
|
|
4102
3587
|
function detectGitRepo() {
|
|
4103
3588
|
try {
|
|
4104
|
-
const remoteUrl = (0,
|
|
3589
|
+
const remoteUrl = (0, import_node_child_process.execSync)("git remote get-url origin", {
|
|
4105
3590
|
encoding: "utf-8",
|
|
4106
3591
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4107
3592
|
}).trim();
|
|
@@ -4109,7 +3594,7 @@ function detectGitRepo() {
|
|
|
4109
3594
|
if (!repoFullName) {
|
|
4110
3595
|
return null;
|
|
4111
3596
|
}
|
|
4112
|
-
const branch = (0,
|
|
3597
|
+
const branch = (0, import_node_child_process.execSync)("git rev-parse --abbrev-ref HEAD", {
|
|
4113
3598
|
encoding: "utf-8",
|
|
4114
3599
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4115
3600
|
}).trim();
|
|
@@ -4365,219 +3850,6 @@ function getStatusEmoji(status) {
|
|
|
4365
3850
|
}
|
|
4366
3851
|
}
|
|
4367
3852
|
|
|
4368
|
-
// apps/cli/src/commands/watch.ts
|
|
4369
|
-
async function watchCommand(options) {
|
|
4370
|
-
const { config, org: org2 } = requireActiveOrg();
|
|
4371
|
-
const intervalMinutes = parseInt(options.interval, 10);
|
|
4372
|
-
if (isNaN(intervalMinutes) || intervalMinutes < 1) {
|
|
4373
|
-
console.error("Invalid interval. Must be a positive number of minutes.");
|
|
4374
|
-
process.exit(1);
|
|
4375
|
-
}
|
|
4376
|
-
console.log("\u{1F6EB} FlightDesk Watch Daemon");
|
|
4377
|
-
console.log(` Organization: ${org2.name}`);
|
|
4378
|
-
console.log(` Interval: ${intervalMinutes} minutes`);
|
|
4379
|
-
console.log(` Auto-PR: ${options.autoPr ? "enabled" : "disabled"}`);
|
|
4380
|
-
console.log("");
|
|
4381
|
-
const playwrightAvailable = await isPlaywrightAvailable();
|
|
4382
|
-
let browser = null;
|
|
4383
|
-
if (!playwrightAvailable) {
|
|
4384
|
-
console.log("\u26A0\uFE0F Playwright not installed. Session monitoring disabled.");
|
|
4385
|
-
console.log(" Install with: pnpm add playwright && npx playwright install chromium");
|
|
4386
|
-
console.log("");
|
|
4387
|
-
} else {
|
|
4388
|
-
browser = new PersistentBrowser(options.headless !== false);
|
|
4389
|
-
console.log("Checking Claude authentication...");
|
|
4390
|
-
try {
|
|
4391
|
-
const isAuthenticated = await browser.checkAuth();
|
|
4392
|
-
if (!isAuthenticated) {
|
|
4393
|
-
console.log("\u26A0\uFE0F Not logged into Claude. Run: flightdesk auth");
|
|
4394
|
-
console.log("");
|
|
4395
|
-
await browser.close();
|
|
4396
|
-
browser = null;
|
|
4397
|
-
} else {
|
|
4398
|
-
console.log("\u2705 Playwright ready, Claude authenticated");
|
|
4399
|
-
console.log(" (Browser session kept alive for monitoring)");
|
|
4400
|
-
console.log("");
|
|
4401
|
-
}
|
|
4402
|
-
} catch (error) {
|
|
4403
|
-
if (error instanceof PlaywrightBrowserNotInstalledError) {
|
|
4404
|
-
console.error("");
|
|
4405
|
-
console.error("\u274C " + error.message);
|
|
4406
|
-
process.exit(1);
|
|
4407
|
-
}
|
|
4408
|
-
throw error;
|
|
4409
|
-
}
|
|
4410
|
-
}
|
|
4411
|
-
const api = FlightDeskAPI.fromConfig(config, org2);
|
|
4412
|
-
async function runCheck() {
|
|
4413
|
-
const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
4414
|
-
console.log(`[${timestamp}] Checking tasks...`);
|
|
4415
|
-
try {
|
|
4416
|
-
const ACTIVE_STATUSES = ["PENDING", "DISPATCHED", "IN_PROGRESS", "BRANCH_CREATED"];
|
|
4417
|
-
const tasks = await api.listTasks({
|
|
4418
|
-
status: ACTIVE_STATUSES,
|
|
4419
|
-
limit: 200
|
|
4420
|
-
});
|
|
4421
|
-
const activeTasks = tasks.filter(
|
|
4422
|
-
(t) => ["DISPATCHED", "IN_PROGRESS", "BRANCH_CREATED"].includes(t.status) || t.status === "PENDING" && t.sessionViewUrl
|
|
4423
|
-
);
|
|
4424
|
-
if (activeTasks.length === 0) {
|
|
4425
|
-
console.log(" No active tasks to monitor");
|
|
4426
|
-
return;
|
|
4427
|
-
}
|
|
4428
|
-
const needsReconciliation = activeTasks.filter((t) => {
|
|
4429
|
-
if (t.prUrl && !["PR_OPEN", "MERGED", "ARCHIVED"].includes(t.status)) return true;
|
|
4430
|
-
if (t.branchName && t.status === "PENDING") return true;
|
|
4431
|
-
if (t.sessionViewUrl && t.status === "PENDING") return true;
|
|
4432
|
-
return false;
|
|
4433
|
-
});
|
|
4434
|
-
if (needsReconciliation.length > 0) {
|
|
4435
|
-
console.log(` \u26A0\uFE0F ${needsReconciliation.length} task(s) need status reconciliation`);
|
|
4436
|
-
}
|
|
4437
|
-
console.log(` Found ${activeTasks.length} active task(s)`);
|
|
4438
|
-
for (const task2 of activeTasks) {
|
|
4439
|
-
console.log(`
|
|
4440
|
-
\u{1F4CB} ${task2.title}`);
|
|
4441
|
-
console.log(` Status: ${task2.status}`);
|
|
4442
|
-
const reconciled = await reconcileTaskStatus(api, task2);
|
|
4443
|
-
if (reconciled) {
|
|
4444
|
-
continue;
|
|
4445
|
-
}
|
|
4446
|
-
if (task2.sessionViewUrl && browser) {
|
|
4447
|
-
console.log(` Checking session...`);
|
|
4448
|
-
const sessionInfo = await browser.monitorSession(task2.sessionViewUrl, {
|
|
4449
|
-
autoPr: options.autoPr && !task2.prUrl
|
|
4450
|
-
// Only auto-PR if no PR exists
|
|
4451
|
-
});
|
|
4452
|
-
await processSessionInfo(api, task2, sessionInfo);
|
|
4453
|
-
} else if (!task2.sessionViewUrl) {
|
|
4454
|
-
console.log(" No session URL registered");
|
|
4455
|
-
} else if (!browser) {
|
|
4456
|
-
console.log(" Browser not available - cannot monitor session");
|
|
4457
|
-
}
|
|
4458
|
-
}
|
|
4459
|
-
} catch (error) {
|
|
4460
|
-
console.error(` Error: ${error}`);
|
|
4461
|
-
}
|
|
4462
|
-
}
|
|
4463
|
-
try {
|
|
4464
|
-
await runCheck();
|
|
4465
|
-
if (options.once) {
|
|
4466
|
-
console.log("\nDone (--once flag specified)");
|
|
4467
|
-
return;
|
|
4468
|
-
}
|
|
4469
|
-
console.log(`
|
|
4470
|
-
Watching... (Ctrl+C to stop)
|
|
4471
|
-
`);
|
|
4472
|
-
const intervalId = setInterval(runCheck, intervalMinutes * 60 * 1e3);
|
|
4473
|
-
process.on("SIGINT", async () => {
|
|
4474
|
-
console.log("\nShutting down...");
|
|
4475
|
-
clearInterval(intervalId);
|
|
4476
|
-
if (browser) {
|
|
4477
|
-
console.log("Closing browser...");
|
|
4478
|
-
await browser.close();
|
|
4479
|
-
}
|
|
4480
|
-
process.exit(0);
|
|
4481
|
-
});
|
|
4482
|
-
await new Promise((_resolve) => {
|
|
4483
|
-
});
|
|
4484
|
-
} finally {
|
|
4485
|
-
if (browser) {
|
|
4486
|
-
await browser.close();
|
|
4487
|
-
}
|
|
4488
|
-
}
|
|
4489
|
-
}
|
|
4490
|
-
async function reconcileTaskStatus(api, task2) {
|
|
4491
|
-
let expectedStatus = null;
|
|
4492
|
-
let reason = "";
|
|
4493
|
-
if (task2.prUrl) {
|
|
4494
|
-
if (!["PR_OPEN", "PREVIEW_STARTING", "PREVIEW_READY", "MERGED", "ARCHIVED", "REVIEW_RUNNING", "REVIEW_DONE", "QA_READY", "QA_APPROVED"].includes(task2.status)) {
|
|
4495
|
-
expectedStatus = "PR_OPEN";
|
|
4496
|
-
reason = `has PR URL but status is ${task2.status}`;
|
|
4497
|
-
}
|
|
4498
|
-
} else if (task2.branchName) {
|
|
4499
|
-
if (["PENDING", "DISPATCHED"].includes(task2.status)) {
|
|
4500
|
-
expectedStatus = "IN_PROGRESS";
|
|
4501
|
-
reason = `has branch but status is ${task2.status}`;
|
|
4502
|
-
}
|
|
4503
|
-
} else if (task2.sessionViewUrl) {
|
|
4504
|
-
if (task2.status === "PENDING") {
|
|
4505
|
-
expectedStatus = "DISPATCHED";
|
|
4506
|
-
reason = `has session URL but status is PENDING`;
|
|
4507
|
-
}
|
|
4508
|
-
}
|
|
4509
|
-
if (expectedStatus) {
|
|
4510
|
-
console.log(` \u{1F527} Status reconciliation: ${reason}`);
|
|
4511
|
-
console.log(` Updating: ${task2.status} \u2192 ${expectedStatus}`);
|
|
4512
|
-
try {
|
|
4513
|
-
await api.updateTask(task2.id, { status: expectedStatus });
|
|
4514
|
-
console.log(" \u2705 Status reconciled");
|
|
4515
|
-
return true;
|
|
4516
|
-
} catch (error) {
|
|
4517
|
-
console.log(` \u274C Failed to reconcile: ${error}`);
|
|
4518
|
-
return false;
|
|
4519
|
-
}
|
|
4520
|
-
}
|
|
4521
|
-
return false;
|
|
4522
|
-
}
|
|
4523
|
-
async function processSessionInfo(api, task2, info) {
|
|
4524
|
-
if (info.status === "error") {
|
|
4525
|
-
console.log(` \u274C Error: ${info.error}`);
|
|
4526
|
-
return;
|
|
4527
|
-
}
|
|
4528
|
-
if (info.status === "archived") {
|
|
4529
|
-
console.log(" \u{1F4E6} Session archived");
|
|
4530
|
-
return;
|
|
4531
|
-
}
|
|
4532
|
-
let activityIcon;
|
|
4533
|
-
let activityLabel;
|
|
4534
|
-
if (info.isActive === true) {
|
|
4535
|
-
activityIcon = "\u26A1";
|
|
4536
|
-
activityLabel = "Claude is working";
|
|
4537
|
-
} else if (info.isActive === false) {
|
|
4538
|
-
activityIcon = "\u{1F4A4}";
|
|
4539
|
-
activityLabel = "Waiting for input";
|
|
4540
|
-
} else {
|
|
4541
|
-
activityIcon = "\u2754";
|
|
4542
|
-
activityLabel = "Activity unknown";
|
|
4543
|
-
}
|
|
4544
|
-
console.log(` ${activityIcon} ${activityLabel}`);
|
|
4545
|
-
const updates = {
|
|
4546
|
-
// Update session activity state and timestamp
|
|
4547
|
-
// Preserve null when activity detection fails (info.isActive is undefined)
|
|
4548
|
-
sessionActive: info.isActive ?? null,
|
|
4549
|
-
sessionCheckedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4550
|
-
};
|
|
4551
|
-
if (info.branchName && info.branchName !== task2.branchName) {
|
|
4552
|
-
console.log(` \u{1F33F} Branch detected: ${info.branchName}`);
|
|
4553
|
-
updates.branchName = info.branchName;
|
|
4554
|
-
if (task2.status === "DISPATCHED") {
|
|
4555
|
-
updates.status = "IN_PROGRESS";
|
|
4556
|
-
}
|
|
4557
|
-
}
|
|
4558
|
-
if (info.prUrl && info.prUrl !== task2.prUrl) {
|
|
4559
|
-
console.log(` \u{1F517} PR detected: ${info.prUrl}`);
|
|
4560
|
-
updates.prUrl = info.prUrl;
|
|
4561
|
-
const prMatch = info.prUrl.match(/\/pull\/(\d+)/);
|
|
4562
|
-
if (prMatch) {
|
|
4563
|
-
updates.prNumber = parseInt(prMatch[1], 10);
|
|
4564
|
-
}
|
|
4565
|
-
updates.status = "PR_OPEN";
|
|
4566
|
-
}
|
|
4567
|
-
if (info.hasPrButton && !info.prUrl && !task2.prUrl) {
|
|
4568
|
-
console.log(' \u{1F4DD} "Create PR" button available');
|
|
4569
|
-
}
|
|
4570
|
-
const hasNonActivityUpdates = Object.keys(updates).some((k) => !["sessionActive", "sessionCheckedAt"].includes(k));
|
|
4571
|
-
try {
|
|
4572
|
-
await api.updateTask(task2.id, updates);
|
|
4573
|
-
if (hasNonActivityUpdates) {
|
|
4574
|
-
console.log(" \u2705 Task updated");
|
|
4575
|
-
}
|
|
4576
|
-
} catch (error) {
|
|
4577
|
-
console.log(` \u274C Failed to update task: ${error}`);
|
|
4578
|
-
}
|
|
4579
|
-
}
|
|
4580
|
-
|
|
4581
3853
|
// apps/cli/src/commands/task.ts
|
|
4582
3854
|
async function taskCommand(action, options) {
|
|
4583
3855
|
const { config, org: org2 } = requireActiveOrg();
|
|
@@ -4676,7 +3948,13 @@ async function handleUpdate(api, options) {
|
|
|
4676
3948
|
const input = {};
|
|
4677
3949
|
if (options.status) input.status = options.status;
|
|
4678
3950
|
if (options.branch) input.branchName = options.branch;
|
|
4679
|
-
if (options.prUrl)
|
|
3951
|
+
if (options.prUrl) {
|
|
3952
|
+
input.prUrl = options.prUrl;
|
|
3953
|
+
const prNumberMatch = options.prUrl.match(/\/pull\/(\d+)/);
|
|
3954
|
+
if (prNumberMatch) {
|
|
3955
|
+
input.prNumber = parseInt(prNumberMatch[1], 10);
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
4680
3958
|
if (options.session) {
|
|
4681
3959
|
const sessionId = parseSessionId(options.session);
|
|
4682
3960
|
input.sessionViewUrl = `https://claude.ai/code/${sessionId}`;
|
|
@@ -4982,403 +4260,6 @@ async function syncCommand() {
|
|
|
4982
4260
|
}
|
|
4983
4261
|
}
|
|
4984
4262
|
|
|
4985
|
-
// apps/cli/src/lib/claude-selectors.ts
|
|
4986
|
-
var ClaudeSelectors = {
|
|
4987
|
-
// ============================================================================
|
|
4988
|
-
// Sidebar - Session List (Verified 2026-02-23)
|
|
4989
|
-
// ============================================================================
|
|
4990
|
-
sidebar: {
|
|
4991
|
-
/** New task input (textarea in sidebar) */
|
|
4992
|
-
newTaskInput: 'textarea[placeholder="Ask Claude to write code..."]',
|
|
4993
|
-
/** Session list container */
|
|
4994
|
-
sessionList: String.raw`.flex.flex-col.gap-0\.5.px-1`,
|
|
4995
|
-
/** Individual session items - use :has-text() with title */
|
|
4996
|
-
sessionItem: (title) => `.cursor-pointer:has-text("${title}")`,
|
|
4997
|
-
/** All session items (div elements with cursor-pointer class) */
|
|
4998
|
-
allSessions: String.raw`.flex.flex-col.gap-0\.5.px-1 .cursor-pointer`,
|
|
4999
|
-
/** Session by index (0-indexed) */
|
|
5000
|
-
sessionByIndex: (index) => String.raw`.flex.flex-col.gap-0\.5.px-1` + ` > div:nth-child(${index + 1}) .cursor-pointer`,
|
|
5001
|
-
/** Active/selected session (has bg-bg-300 class) */
|
|
5002
|
-
activeSession: ".cursor-pointer.bg-bg-300",
|
|
5003
|
-
/** Session title within a session item */
|
|
5004
|
-
sessionTitle: "button.text-sm.font-medium.truncate",
|
|
5005
|
-
/** Session archive button (icon-only, no aria-label) */
|
|
5006
|
-
sessionArchiveButton: (title) => `.cursor-pointer:has-text("${title}") button`,
|
|
5007
|
-
/** Search sessions button */
|
|
5008
|
-
searchButton: 'button[aria-label="Search \u2318K"]',
|
|
5009
|
-
/** User profile button */
|
|
5010
|
-
userProfileButton: '[data-testid="code-user-menu-button"]'
|
|
5011
|
-
},
|
|
5012
|
-
// ============================================================================
|
|
5013
|
-
// Branch Bar (between chat messages and chat input) - Verified 2026-02-23
|
|
5014
|
-
// ============================================================================
|
|
5015
|
-
branchBar: {
|
|
5016
|
-
/** Container for branch bar */
|
|
5017
|
-
container: ".flex.items-center.gap-2.w-full.p-2",
|
|
5018
|
-
/** Branch name display - the copyable span */
|
|
5019
|
-
branchName: "button.group\\/copy span.truncate",
|
|
5020
|
-
/** Base/target branch dropdown (e.g., "develop") */
|
|
5021
|
-
baseBranchSelector: '.flex.items-center.gap-2.w-full.p-2 button[aria-haspopup="menu"]',
|
|
5022
|
-
/** Copy branch button (click to copy branch name) */
|
|
5023
|
-
copyBranchButton: "button.group\\/copy",
|
|
5024
|
-
/** Diff stats button (+N -M) */
|
|
5025
|
-
diffStatsButton: String.raw`.flex.items-center.gap-2.w-full.p-2 button.border-0\.5`
|
|
5026
|
-
},
|
|
5027
|
-
// ============================================================================
|
|
5028
|
-
// Pull Request Controls - Verified 2026-02-23
|
|
5029
|
-
// ============================================================================
|
|
5030
|
-
pullRequest: {
|
|
5031
|
-
/** Create PR button (left half - immediate action) - CAREFUL: clicks immediately! */
|
|
5032
|
-
createPrButton: 'button:has-text("Create PR"):not([aria-haspopup])',
|
|
5033
|
-
/** PR dropdown trigger (right half - shows options) - SAFE to click */
|
|
5034
|
-
prDropdown: 'button.rounded-r-md[aria-haspopup="menu"]',
|
|
5035
|
-
/** PR status indicator when PR exists */
|
|
5036
|
-
prStatus: 'a[href*="/pull/"], a[href*="/merge_requests/"]',
|
|
5037
|
-
/** View PR link */
|
|
5038
|
-
viewPrLink: 'a:has-text("View PR"), a:has-text("View Pull Request")'
|
|
5039
|
-
},
|
|
5040
|
-
// ============================================================================
|
|
5041
|
-
// Chat Area - Verified 2026-02-23
|
|
5042
|
-
// ============================================================================
|
|
5043
|
-
chat: {
|
|
5044
|
-
/** Main chat container */
|
|
5045
|
-
container: "main",
|
|
5046
|
-
/** Chat messages */
|
|
5047
|
-
messages: 'div[data-testid="chat-message"], div.prose',
|
|
5048
|
-
/** User messages */
|
|
5049
|
-
userMessages: 'div[data-testid="user-message"]',
|
|
5050
|
-
/** Assistant messages */
|
|
5051
|
-
assistantMessages: 'div[data-testid="assistant-message"]',
|
|
5052
|
-
/** Chat input (ProseMirror contenteditable) - NOT a textarea */
|
|
5053
|
-
input: '[aria-label="Enter your turn"]',
|
|
5054
|
-
/** Chat input fallback */
|
|
5055
|
-
inputFallback: 'div.tiptap.ProseMirror[contenteditable="true"]',
|
|
5056
|
-
/** Toggle menu button (+) */
|
|
5057
|
-
toggleMenuButton: 'button[aria-label="Toggle menu"]',
|
|
5058
|
-
/** Send button */
|
|
5059
|
-
sendButton: 'form.w-full button[aria-label="Submit"]',
|
|
5060
|
-
/** Stop button (during generation) */
|
|
5061
|
-
stopButton: 'button:has-text("Stop")'
|
|
5062
|
-
},
|
|
5063
|
-
// ============================================================================
|
|
5064
|
-
// Model Selector - Verified 2026-02-23
|
|
5065
|
-
// ============================================================================
|
|
5066
|
-
model: {
|
|
5067
|
-
/** Model selector dropdown (sidebar) */
|
|
5068
|
-
sidebarSelector: 'form:not(.w-full) [data-testid="model-selector-dropdown"]',
|
|
5069
|
-
/** Model selector dropdown (main chat) */
|
|
5070
|
-
chatSelector: 'form.w-full [data-testid="model-selector-dropdown"]',
|
|
5071
|
-
/** Model options in dropdown (role="menuitem") */
|
|
5072
|
-
options: '[role="menuitem"]',
|
|
5073
|
-
/** Specific model option */
|
|
5074
|
-
option: (modelName) => `[role="menuitem"]:has-text("${modelName}")`,
|
|
5075
|
-
/** Quick model selectors */
|
|
5076
|
-
opus: '[role="menuitem"]:has-text("Opus")',
|
|
5077
|
-
sonnet: '[role="menuitem"]:has-text("Sonnet")',
|
|
5078
|
-
haiku: '[role="menuitem"]:has-text("Haiku")'
|
|
5079
|
-
},
|
|
5080
|
-
// ============================================================================
|
|
5081
|
-
// Mode Toggle - Verified 2026-02-23
|
|
5082
|
-
// Text alternates between "Auto accept edits" and "Plan mode"
|
|
5083
|
-
// ============================================================================
|
|
5084
|
-
mode: {
|
|
5085
|
-
/** Mode toggle button in sidebar - simple toggle, not dropdown */
|
|
5086
|
-
sidebarToggle: 'form:not(.w-full) button:has-text("Auto accept edits"), form:not(.w-full) button:has-text("Plan mode")',
|
|
5087
|
-
/** Mode toggle button in main chat */
|
|
5088
|
-
chatToggle: 'form.w-full button:has-text("Auto accept edits"), form.w-full button:has-text("Plan mode")'
|
|
5089
|
-
},
|
|
5090
|
-
// ============================================================================
|
|
5091
|
-
// Repository Connection - Verified 2026-02-23
|
|
5092
|
-
// ============================================================================
|
|
5093
|
-
repository: {
|
|
5094
|
-
/** Repo button in sidebar (shows current repo name) */
|
|
5095
|
-
repoButton: "form:not(.w-full) button.flex.items-center.gap-1.min-w-0.rounded",
|
|
5096
|
-
/** Repo button by name */
|
|
5097
|
-
repoButtonByName: (repoName) => `form:not(.w-full) button:has-text("${repoName}")`,
|
|
5098
|
-
/** Add repository button (hidden until hover) */
|
|
5099
|
-
addRepoButton: 'button[aria-label="Add repository"]',
|
|
5100
|
-
/** Select repository dropdown */
|
|
5101
|
-
selectRepoDropdown: 'form:not(.w-full) button[aria-haspopup="menu"]:has-text("Select repository")'
|
|
5102
|
-
},
|
|
5103
|
-
// ============================================================================
|
|
5104
|
-
// Session Archive
|
|
5105
|
-
// ============================================================================
|
|
5106
|
-
archive: {
|
|
5107
|
-
/** Archive indicator text */
|
|
5108
|
-
indicator: "text=This session has been archived",
|
|
5109
|
-
/** Archive button (if available) */
|
|
5110
|
-
archiveButton: 'button:has-text("Archive")'
|
|
5111
|
-
},
|
|
5112
|
-
// ============================================================================
|
|
5113
|
-
// Authentication
|
|
5114
|
-
// ============================================================================
|
|
5115
|
-
auth: {
|
|
5116
|
-
/** Login page indicators */
|
|
5117
|
-
loginPage: 'text=Log in, text=Sign in, form[action*="login"]',
|
|
5118
|
-
/** OAuth redirect */
|
|
5119
|
-
oauthRedirect: "text=Redirecting"
|
|
5120
|
-
},
|
|
5121
|
-
// ============================================================================
|
|
5122
|
-
// Dropdowns (Radix UI pattern)
|
|
5123
|
-
// ============================================================================
|
|
5124
|
-
dropdown: {
|
|
5125
|
-
/** Open dropdown menu */
|
|
5126
|
-
menu: 'div[role="menu"]',
|
|
5127
|
-
/** Menu items */
|
|
5128
|
-
items: 'div[role="menuitem"]',
|
|
5129
|
-
/** Specific menu item */
|
|
5130
|
-
item: (text) => `div[role="menuitem"]:has-text("${text}")`,
|
|
5131
|
-
/** Listbox (for selects) */
|
|
5132
|
-
listbox: 'div[role="listbox"]',
|
|
5133
|
-
/** Listbox options */
|
|
5134
|
-
listboxOptions: 'div[role="option"]'
|
|
5135
|
-
}
|
|
5136
|
-
};
|
|
5137
|
-
|
|
5138
|
-
// apps/cli/src/commands/import.ts
|
|
5139
|
-
var fs3 = __toESM(require("fs"));
|
|
5140
|
-
var playwright2 = null;
|
|
5141
|
-
async function importCommand(options) {
|
|
5142
|
-
const { config, org: org2 } = requireActiveOrg();
|
|
5143
|
-
if (!await isPlaywrightAvailable()) {
|
|
5144
|
-
console.error("Playwright is required for import. Install it with:");
|
|
5145
|
-
console.error(" npm install -g playwright");
|
|
5146
|
-
console.error(" npx playwright install chromium");
|
|
5147
|
-
process.exit(1);
|
|
5148
|
-
}
|
|
5149
|
-
playwright2 = await import("playwright");
|
|
5150
|
-
const api = FlightDeskAPI.fromConfig(config, org2);
|
|
5151
|
-
console.log("\n\u{1F50D} FlightDesk Import - Scanning Claude Sessions\n");
|
|
5152
|
-
if (options.dryRun) {
|
|
5153
|
-
console.log("\u{1F4CB} DRY RUN - No changes will be made\n");
|
|
5154
|
-
}
|
|
5155
|
-
let existingProjects = [];
|
|
5156
|
-
try {
|
|
5157
|
-
existingProjects = await api.listProjects();
|
|
5158
|
-
if (options.verbose) {
|
|
5159
|
-
console.log(`Found ${existingProjects.length} existing projects`);
|
|
5160
|
-
}
|
|
5161
|
-
} catch (error) {
|
|
5162
|
-
console.error("Warning: Could not fetch existing projects:", error);
|
|
5163
|
-
}
|
|
5164
|
-
const sessions = await scanClaudeSessions({
|
|
5165
|
-
headless: options.headless !== false,
|
|
5166
|
-
limit: options.limit || 50,
|
|
5167
|
-
verbose: options.verbose
|
|
5168
|
-
});
|
|
5169
|
-
if (sessions.length === 0) {
|
|
5170
|
-
console.log("No sessions found. Make sure you are logged into Claude.");
|
|
5171
|
-
console.log("Run: flightdesk auth");
|
|
5172
|
-
return;
|
|
5173
|
-
}
|
|
5174
|
-
console.log(`
|
|
5175
|
-
Found ${sessions.length} sessions:
|
|
5176
|
-
`);
|
|
5177
|
-
const sessionsByRepo = /* @__PURE__ */ new Map();
|
|
5178
|
-
const noRepoSessions = [];
|
|
5179
|
-
for (const session of sessions) {
|
|
5180
|
-
if (session.repoName) {
|
|
5181
|
-
const existing = sessionsByRepo.get(session.repoName) || [];
|
|
5182
|
-
existing.push(session);
|
|
5183
|
-
sessionsByRepo.set(session.repoName, existing);
|
|
5184
|
-
} else {
|
|
5185
|
-
noRepoSessions.push(session);
|
|
5186
|
-
}
|
|
5187
|
-
}
|
|
5188
|
-
for (const [repoName, repoSessions] of sessionsByRepo) {
|
|
5189
|
-
const matchingProject = existingProjects.find(
|
|
5190
|
-
(p) => p.githubRepo?.endsWith(`/${repoName}`)
|
|
5191
|
-
);
|
|
5192
|
-
if (matchingProject) {
|
|
5193
|
-
console.log(`\u{1F4C1} ${repoName} \u2192 Project: ${matchingProject.name} (${matchingProject.id.slice(0, 8)})`);
|
|
5194
|
-
} else {
|
|
5195
|
-
console.log(`\u{1F4C1} ${repoName} \u2192 No matching project`);
|
|
5196
|
-
}
|
|
5197
|
-
for (const session of repoSessions) {
|
|
5198
|
-
const status = session.archived ? "\u{1F4E6}" : session.branchName ? "\u{1F33F}" : "\u{1F4AC}";
|
|
5199
|
-
console.log(` ${status} ${session.title.slice(0, 50)}${session.title.length > 50 ? "..." : ""}`);
|
|
5200
|
-
if (session.branchName) {
|
|
5201
|
-
console.log(` Branch: ${session.branchName}`);
|
|
5202
|
-
}
|
|
5203
|
-
}
|
|
5204
|
-
console.log("");
|
|
5205
|
-
}
|
|
5206
|
-
if (noRepoSessions.length > 0) {
|
|
5207
|
-
console.log(`\u{1F4C1} No Repository (${noRepoSessions.length} sessions)`);
|
|
5208
|
-
for (const session of noRepoSessions.slice(0, 5)) {
|
|
5209
|
-
const status = session.archived ? "\u{1F4E6}" : "\u{1F4AC}";
|
|
5210
|
-
console.log(` ${status} ${session.title.slice(0, 50)}${session.title.length > 50 ? "..." : ""}`);
|
|
5211
|
-
}
|
|
5212
|
-
if (noRepoSessions.length > 5) {
|
|
5213
|
-
console.log(` ... and ${noRepoSessions.length - 5} more`);
|
|
5214
|
-
}
|
|
5215
|
-
console.log("");
|
|
5216
|
-
}
|
|
5217
|
-
console.log("\u2500".repeat(60));
|
|
5218
|
-
console.log(`
|
|
5219
|
-
Summary:`);
|
|
5220
|
-
console.log(` Total sessions: ${sessions.length}`);
|
|
5221
|
-
console.log(` With repository: ${sessions.length - noRepoSessions.length}`);
|
|
5222
|
-
console.log(` Without repository: ${noRepoSessions.length}`);
|
|
5223
|
-
console.log(` Archived: ${sessions.filter((s) => s.archived).length}`);
|
|
5224
|
-
console.log("");
|
|
5225
|
-
if (options.dryRun) {
|
|
5226
|
-
console.log("\u{1F4A1} Run without --dry-run to create tasks in FlightDesk");
|
|
5227
|
-
return;
|
|
5228
|
-
}
|
|
5229
|
-
const importableSessions = [];
|
|
5230
|
-
for (const [repoName, repoSessions] of sessionsByRepo) {
|
|
5231
|
-
const matchingProject = existingProjects.find(
|
|
5232
|
-
(p) => p.githubRepo?.endsWith(`/${repoName}`)
|
|
5233
|
-
);
|
|
5234
|
-
if (matchingProject) {
|
|
5235
|
-
for (const session of repoSessions) {
|
|
5236
|
-
importableSessions.push({ session, project: matchingProject });
|
|
5237
|
-
}
|
|
5238
|
-
}
|
|
5239
|
-
}
|
|
5240
|
-
if (importableSessions.length === 0) {
|
|
5241
|
-
console.log("No sessions can be imported (no matching projects).");
|
|
5242
|
-
console.log("Create projects in FlightDesk first for the repositories you want to track.");
|
|
5243
|
-
return;
|
|
5244
|
-
}
|
|
5245
|
-
console.log(`
|
|
5246
|
-
\u{1F680} Importing ${importableSessions.length} sessions...
|
|
5247
|
-
`);
|
|
5248
|
-
let created = 0;
|
|
5249
|
-
let failed = 0;
|
|
5250
|
-
for (const { session, project: project2 } of importableSessions) {
|
|
5251
|
-
try {
|
|
5252
|
-
const task2 = await api.createTask({
|
|
5253
|
-
projectId: project2.id,
|
|
5254
|
-
title: session.title,
|
|
5255
|
-
description: `Imported from Claude Code session`
|
|
5256
|
-
});
|
|
5257
|
-
let status = "DISPATCHED";
|
|
5258
|
-
if (session.archived) {
|
|
5259
|
-
status = "ARCHIVED";
|
|
5260
|
-
} else if (session.branchName) {
|
|
5261
|
-
status = "IN_PROGRESS";
|
|
5262
|
-
}
|
|
5263
|
-
await api.updateTask(task2.id, {
|
|
5264
|
-
branchName: session.branchName,
|
|
5265
|
-
sessionViewUrl: session.url,
|
|
5266
|
-
status
|
|
5267
|
-
});
|
|
5268
|
-
console.log(` \u2705 ${session.title.slice(0, 50)}`);
|
|
5269
|
-
created++;
|
|
5270
|
-
} catch (error) {
|
|
5271
|
-
console.error(` \u274C ${session.title.slice(0, 50)}: ${error.message}`);
|
|
5272
|
-
failed++;
|
|
5273
|
-
}
|
|
5274
|
-
}
|
|
5275
|
-
console.log("");
|
|
5276
|
-
console.log("\u2500".repeat(60));
|
|
5277
|
-
console.log(`
|
|
5278
|
-
Import complete:`);
|
|
5279
|
-
console.log(` Created: ${created}`);
|
|
5280
|
-
console.log(` Failed: ${failed}`);
|
|
5281
|
-
console.log(` Skipped (no matching project): ${sessions.length - importableSessions.length}`);
|
|
5282
|
-
}
|
|
5283
|
-
async function scanClaudeSessions(options) {
|
|
5284
|
-
if (!playwright2) {
|
|
5285
|
-
throw new Error("Playwright not loaded");
|
|
5286
|
-
}
|
|
5287
|
-
if (!fs3.existsSync(USER_DATA_DIR)) {
|
|
5288
|
-
fs3.mkdirSync(USER_DATA_DIR, { recursive: true });
|
|
5289
|
-
}
|
|
5290
|
-
const context = await playwright2.chromium.launchPersistentContext(USER_DATA_DIR, {
|
|
5291
|
-
headless: options.headless,
|
|
5292
|
-
viewport: { width: 1280, height: 720 },
|
|
5293
|
-
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
|
|
5294
|
-
});
|
|
5295
|
-
try {
|
|
5296
|
-
const page = await context.newPage();
|
|
5297
|
-
page.setDefaultTimeout(6e4);
|
|
5298
|
-
console.log("Navigating to Claude Code...");
|
|
5299
|
-
await page.goto("https://claude.ai/code", { waitUntil: "domcontentloaded", timeout: 6e4 });
|
|
5300
|
-
if (options.verbose) {
|
|
5301
|
-
console.log("Waiting for page to load...");
|
|
5302
|
-
}
|
|
5303
|
-
await page.waitForTimeout(5e3);
|
|
5304
|
-
const url = page.url();
|
|
5305
|
-
if (options.verbose) {
|
|
5306
|
-
console.log("Current URL:", url);
|
|
5307
|
-
}
|
|
5308
|
-
if (url.includes("/login") || url.includes("/oauth")) {
|
|
5309
|
-
console.log("\n\u26A0\uFE0F Not logged into Claude. Run: flightdesk auth\n");
|
|
5310
|
-
return [];
|
|
5311
|
-
}
|
|
5312
|
-
console.log("Logged in. Scanning sessions...");
|
|
5313
|
-
await page.waitForTimeout(3e3);
|
|
5314
|
-
const sessions = [];
|
|
5315
|
-
const sessionItems = await page.$$(ClaudeSelectors.sidebar.allSessions);
|
|
5316
|
-
if (options.verbose) {
|
|
5317
|
-
console.log(`Found ${sessionItems.length} session items in sidebar`);
|
|
5318
|
-
}
|
|
5319
|
-
const limit = Math.min(sessionItems.length, options.limit);
|
|
5320
|
-
for (let i = 0; i < limit; i++) {
|
|
5321
|
-
const items = await page.$$(ClaudeSelectors.sidebar.allSessions);
|
|
5322
|
-
if (i >= items.length) break;
|
|
5323
|
-
const item = items[i];
|
|
5324
|
-
try {
|
|
5325
|
-
const titleElement = await item.$("span.text-text-100");
|
|
5326
|
-
const title = titleElement ? (await titleElement.textContent())?.trim() || "Untitled" : "Untitled";
|
|
5327
|
-
let repoName;
|
|
5328
|
-
try {
|
|
5329
|
-
const repoElement = await item.$("span.text-text-500 span.truncate");
|
|
5330
|
-
if (repoElement) {
|
|
5331
|
-
const repoText = (await repoElement.textContent())?.trim();
|
|
5332
|
-
if (repoText && repoText.length > 0) {
|
|
5333
|
-
repoName = repoText;
|
|
5334
|
-
}
|
|
5335
|
-
}
|
|
5336
|
-
} catch {
|
|
5337
|
-
}
|
|
5338
|
-
if (options.verbose) {
|
|
5339
|
-
const repoDisplay = repoName ? ` (${repoName})` : "";
|
|
5340
|
-
process.stdout.write(`\r Scanning ${i + 1}/${limit}: ${title.slice(0, 35)}${repoDisplay}...`);
|
|
5341
|
-
}
|
|
5342
|
-
try {
|
|
5343
|
-
await page.keyboard.press("Escape");
|
|
5344
|
-
await page.waitForTimeout(300);
|
|
5345
|
-
} catch {
|
|
5346
|
-
}
|
|
5347
|
-
await item.click();
|
|
5348
|
-
await page.waitForTimeout(2e3);
|
|
5349
|
-
const sessionUrl = page.url();
|
|
5350
|
-
let branchName;
|
|
5351
|
-
try {
|
|
5352
|
-
const branchElement = await page.$(ClaudeSelectors.branchBar.branchName);
|
|
5353
|
-
if (branchElement) {
|
|
5354
|
-
branchName = (await branchElement.textContent())?.trim();
|
|
5355
|
-
}
|
|
5356
|
-
} catch {
|
|
5357
|
-
}
|
|
5358
|
-
const archived = !!await page.$(ClaudeSelectors.archive.indicator);
|
|
5359
|
-
sessions.push({
|
|
5360
|
-
url: sessionUrl,
|
|
5361
|
-
title: title.trim(),
|
|
5362
|
-
branchName,
|
|
5363
|
-
repoName,
|
|
5364
|
-
archived
|
|
5365
|
-
});
|
|
5366
|
-
} catch (error) {
|
|
5367
|
-
if (options.verbose) {
|
|
5368
|
-
console.error(`
|
|
5369
|
-
Error scanning session ${i + 1}:`, error);
|
|
5370
|
-
}
|
|
5371
|
-
}
|
|
5372
|
-
}
|
|
5373
|
-
if (options.verbose) {
|
|
5374
|
-
console.log("\n");
|
|
5375
|
-
}
|
|
5376
|
-
return sessions;
|
|
5377
|
-
} finally {
|
|
5378
|
-
await context.close();
|
|
5379
|
-
}
|
|
5380
|
-
}
|
|
5381
|
-
|
|
5382
4263
|
// apps/cli/src/commands/project.ts
|
|
5383
4264
|
async function projectCommand(action, _options) {
|
|
5384
4265
|
const { config, org: org2 } = requireActiveOrg();
|
|
@@ -5411,10 +4292,10 @@ ${projects.length} project(s)`);
|
|
|
5411
4292
|
}
|
|
5412
4293
|
|
|
5413
4294
|
// apps/cli/src/commands/preview.ts
|
|
5414
|
-
var
|
|
5415
|
-
var
|
|
5416
|
-
var
|
|
5417
|
-
var
|
|
4295
|
+
var import_node_child_process2 = require("node:child_process");
|
|
4296
|
+
var path2 = __toESM(require("node:path"));
|
|
4297
|
+
var os2 = __toESM(require("node:os"));
|
|
4298
|
+
var fs2 = __toESM(require("node:fs"));
|
|
5418
4299
|
function isValidContainerId(id) {
|
|
5419
4300
|
return /^[a-fA-F0-9]+$/.test(id) && id.length >= 12 && id.length <= 64;
|
|
5420
4301
|
}
|
|
@@ -5525,7 +4406,7 @@ async function handleLogs(api, options) {
|
|
|
5525
4406
|
`);
|
|
5526
4407
|
validateSSHParams(instance);
|
|
5527
4408
|
const sshCommand = `docker logs -f ${instance.containerId}`;
|
|
5528
|
-
const ssh = (0,
|
|
4409
|
+
const ssh = (0, import_node_child_process2.spawn)("ssh", [
|
|
5529
4410
|
"-o",
|
|
5530
4411
|
"StrictHostKeyChecking=no",
|
|
5531
4412
|
"-o",
|
|
@@ -5565,7 +4446,7 @@ async function handleMount(api, options) {
|
|
|
5565
4446
|
await new Promise((resolve) => setTimeout(resolve, 3e3));
|
|
5566
4447
|
}
|
|
5567
4448
|
try {
|
|
5568
|
-
(0,
|
|
4449
|
+
(0, import_node_child_process2.execSync)("which sshfs", { stdio: "ignore" });
|
|
5569
4450
|
} catch {
|
|
5570
4451
|
console.error("\u274C sshfs is not installed.");
|
|
5571
4452
|
console.error("");
|
|
@@ -5580,13 +4461,13 @@ async function handleMount(api, options) {
|
|
|
5580
4461
|
process.exit(1);
|
|
5581
4462
|
}
|
|
5582
4463
|
const rawTaskIdPrefix = options.taskId.substring(0, 8);
|
|
5583
|
-
const safeTaskId =
|
|
5584
|
-
const mountDir = options.directory ||
|
|
5585
|
-
if (!
|
|
5586
|
-
|
|
4464
|
+
const safeTaskId = path2.basename(rawTaskIdPrefix).replaceAll(/[^a-zA-Z0-9_-]/g, "") || "task";
|
|
4465
|
+
const mountDir = options.directory || path2.join(os2.homedir(), "flightdesk-mounts", safeTaskId);
|
|
4466
|
+
if (!fs2.existsSync(mountDir)) {
|
|
4467
|
+
fs2.mkdirSync(mountDir, { recursive: true });
|
|
5587
4468
|
}
|
|
5588
4469
|
try {
|
|
5589
|
-
const mounted = (0,
|
|
4470
|
+
const mounted = (0, import_node_child_process2.execSync)("mount", { encoding: "utf8" });
|
|
5590
4471
|
if (mounted.includes(mountDir)) {
|
|
5591
4472
|
console.log(`\u{1F4C1} Already mounted at ${mountDir}`);
|
|
5592
4473
|
return;
|
|
@@ -5612,7 +4493,7 @@ async function handleMount(api, options) {
|
|
|
5612
4493
|
mountDir
|
|
5613
4494
|
];
|
|
5614
4495
|
try {
|
|
5615
|
-
const result = (0,
|
|
4496
|
+
const result = (0, import_node_child_process2.spawnSync)("sshfs", sshfsArgs, { stdio: "inherit" });
|
|
5616
4497
|
if (result.status !== 0) {
|
|
5617
4498
|
throw new Error(`sshfs exited with code ${result.status}`);
|
|
5618
4499
|
}
|
|
@@ -5636,9 +4517,9 @@ async function handleMount(api, options) {
|
|
|
5636
4517
|
}
|
|
5637
4518
|
async function handleUnmount(_api, options) {
|
|
5638
4519
|
const rawTaskIdPrefix = options.taskId.substring(0, 8);
|
|
5639
|
-
const safeTaskId =
|
|
5640
|
-
const mountDir =
|
|
5641
|
-
if (!
|
|
4520
|
+
const safeTaskId = path2.basename(rawTaskIdPrefix).replaceAll(/[^a-zA-Z0-9_-]/g, "") || "task";
|
|
4521
|
+
const mountDir = path2.join(os2.homedir(), "flightdesk-mounts", safeTaskId);
|
|
4522
|
+
if (!fs2.existsSync(mountDir)) {
|
|
5642
4523
|
console.log("Mount directory does not exist");
|
|
5643
4524
|
return;
|
|
5644
4525
|
}
|
|
@@ -5646,16 +4527,16 @@ async function handleUnmount(_api, options) {
|
|
|
5646
4527
|
try {
|
|
5647
4528
|
let result;
|
|
5648
4529
|
if (process.platform === "darwin") {
|
|
5649
|
-
result = (0,
|
|
4530
|
+
result = (0, import_node_child_process2.spawnSync)("umount", [mountDir], { stdio: "inherit" });
|
|
5650
4531
|
} else {
|
|
5651
|
-
result = (0,
|
|
4532
|
+
result = (0, import_node_child_process2.spawnSync)("fusermount", ["-u", mountDir], { stdio: "inherit" });
|
|
5652
4533
|
}
|
|
5653
4534
|
if (result.status !== 0) {
|
|
5654
4535
|
throw new Error(`Unmount exited with code ${result.status}`);
|
|
5655
4536
|
}
|
|
5656
4537
|
console.log("\u2705 Unmounted successfully");
|
|
5657
4538
|
try {
|
|
5658
|
-
|
|
4539
|
+
fs2.rmSync(mountDir, { recursive: true, force: true });
|
|
5659
4540
|
} catch {
|
|
5660
4541
|
}
|
|
5661
4542
|
} catch (error) {
|
|
@@ -5688,7 +4569,7 @@ async function handleTeardown(api, options) {
|
|
|
5688
4569
|
|
|
5689
4570
|
// apps/cli/src/main.ts
|
|
5690
4571
|
var program2 = new Command();
|
|
5691
|
-
program2.name("flightdesk").description("FlightDesk CLI - AI task management for Claude Code sessions").version("0.3.
|
|
4572
|
+
program2.name("flightdesk").description("FlightDesk CLI - AI task management for Claude Code sessions").version("0.3.2").option("--dev", "Use local development API (localhost:3000)").option("--api <url>", "Use custom API URL");
|
|
5692
4573
|
program2.hook("preAction", () => {
|
|
5693
4574
|
const opts = program2.opts();
|
|
5694
4575
|
if (opts.api) {
|
|
@@ -5701,7 +4582,6 @@ program2.hook("preAction", () => {
|
|
|
5701
4582
|
}
|
|
5702
4583
|
});
|
|
5703
4584
|
program2.command("init").description("Configure FlightDesk CLI with your API credentials").action(initCommand);
|
|
5704
|
-
program2.command("auth").description("Log in to Claude for session monitoring").action(authCommand);
|
|
5705
4585
|
program2.command("register [task-id]").description("Register a Claude Code session with a FlightDesk task (auto-detects project from git repo)").option("-p, --project <id>", "Project ID (auto-detected from git repo if not provided)").option("--view-url <url>", "Claude Code session view URL").option("--teleport-id <id>", "Claude Code teleport ID").option("--title <title>", "Task title (creates new task if task-id not provided)").option("--description <description>", "Task description").action(registerCommand);
|
|
5706
4586
|
var project = program2.command("project").description("Project management commands");
|
|
5707
4587
|
project.command("list").description("List projects in the active organization").action(() => projectCommand("list", {}));
|
|
@@ -5710,8 +4590,6 @@ task.command("create").description("Create a new task").requiredOption("-p, --pr
|
|
|
5710
4590
|
task.command("list").description("List tasks").option("-p, --project <id>", "Filter by project ID").option("--status <status>", "Filter by status").action((options) => taskCommand("list", options));
|
|
5711
4591
|
task.command("status <task-id>").description("Get task status").action((taskId) => taskCommand("status", { taskId }));
|
|
5712
4592
|
task.command("update <task-id>").description("Update task").option("-s, --status <status>", "New status").option("--branch <branch>", "Branch name").option("--pr-url <url>", "Pull request URL").option("--session <session>", "Claude Code session (URL or session ID)").action((taskId, options) => taskCommand("update", { taskId, ...options }));
|
|
5713
|
-
program2.command("watch").description("Start the Playwright daemon to monitor Claude Code sessions").option("--interval <minutes>", "Check interval in minutes", "5").option("--once", "Run once and exit").option("--auto-pr", 'Automatically click "Create PR" button when found').option("--no-headless", "Run browser in visible mode (for debugging)").action(watchCommand);
|
|
5714
|
-
program2.command("import").description("Scan Claude.ai for existing sessions and import them as tasks").option("--dry-run", "Show what would be imported without making changes").option("--limit <n>", "Maximum number of sessions to scan", "50").option("-p, --project <id>", "Import all sessions into a specific project").option("--no-headless", "Run browser in visible mode (for debugging)").option("-v, --verbose", "Show detailed progress").action(importCommand);
|
|
5715
4593
|
program2.command("status").description("Show status of all active tasks").option("-p, --project <id>", "Filter by project").action(statusCommand);
|
|
5716
4594
|
program2.command("prompt <task-id>").description("Get a prompt for a task (ready to paste into Claude)").option("--type <type>", "Prompt type: review, test_plan, summary, handoff", "review").action(promptCommand);
|
|
5717
4595
|
var org = program2.command("org").description("Organization management");
|