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.
Files changed (4) hide show
  1. package/README.md +8 -30
  2. package/main.js +45 -1167
  3. package/main.js.map +4 -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 path4 = require("node:path");
962
- var fs5 = require("node:fs");
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 = path4.resolve(baseDir, baseName);
1895
- if (fs5.existsSync(localBin)) return localBin;
1896
- if (sourceExt.includes(path4.extname(baseName))) return void 0;
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) => fs5.existsSync(`${localBin}${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 = fs5.realpathSync(this._scriptPath);
1910
+ resolvedScriptPath = fs3.realpathSync(this._scriptPath);
1911
1911
  } catch (err) {
1912
1912
  resolvedScriptPath = this._scriptPath;
1913
1913
  }
1914
- executableDir = path4.resolve(
1915
- path4.dirname(resolvedScriptPath),
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 = path4.basename(
1922
+ const legacyName = path3.basename(
1923
1923
  this._scriptPath,
1924
- path4.extname(this._scriptPath)
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(path4.extname(executableFile));
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 = path4.basename(filename, path4.extname(filename));
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(path5) {
2790
- if (path5 === void 0) return this._executableDir;
2791
- this._executableDir = path5;
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 import_node_child_process2 = require("node:child_process");
3586
+ var import_node_child_process = require("node:child_process");
4102
3587
  function detectGitRepo() {
4103
3588
  try {
4104
- const remoteUrl = (0, import_node_child_process2.execSync)("git remote get-url origin", {
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, import_node_child_process2.execSync)("git rev-parse --abbrev-ref HEAD", {
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) input.prUrl = 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 import_node_child_process3 = require("node:child_process");
5415
- var path3 = __toESM(require("node:path"));
5416
- var os3 = __toESM(require("node:os"));
5417
- var fs4 = __toESM(require("node:fs"));
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, import_node_child_process3.spawn)("ssh", [
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, import_node_child_process3.execSync)("which sshfs", { stdio: "ignore" });
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 = path3.basename(rawTaskIdPrefix).replaceAll(/[^a-zA-Z0-9_-]/g, "") || "task";
5584
- const mountDir = options.directory || path3.join(os3.homedir(), "flightdesk-mounts", safeTaskId);
5585
- if (!fs4.existsSync(mountDir)) {
5586
- fs4.mkdirSync(mountDir, { recursive: true });
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, import_node_child_process3.execSync)("mount", { encoding: "utf8" });
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, import_node_child_process3.spawnSync)("sshfs", sshfsArgs, { stdio: "inherit" });
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 = path3.basename(rawTaskIdPrefix).replaceAll(/[^a-zA-Z0-9_-]/g, "") || "task";
5640
- const mountDir = path3.join(os3.homedir(), "flightdesk-mounts", safeTaskId);
5641
- if (!fs4.existsSync(mountDir)) {
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, import_node_child_process3.spawnSync)("umount", [mountDir], { stdio: "inherit" });
4530
+ result = (0, import_node_child_process2.spawnSync)("umount", [mountDir], { stdio: "inherit" });
5650
4531
  } else {
5651
- result = (0, import_node_child_process3.spawnSync)("fusermount", ["-u", mountDir], { stdio: "inherit" });
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
- fs4.rmSync(mountDir, { recursive: true, force: true });
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.0").option("--dev", "Use local development API (localhost:3000)").option("--api <url>", "Use custom API URL");
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");