flightdesk 0.2.5 → 0.3.0

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 (3) hide show
  1. package/main.js +110 -80
  2. package/main.js.map +3 -3
  3. package/package.json +1 -1
package/main.js CHANGED
@@ -3216,6 +3216,8 @@ var FlightDeskAPI = class _FlightDeskAPI {
3216
3216
  status
3217
3217
  branchName
3218
3218
  prUrl
3219
+ sessionViewUrl
3220
+ sessionTeleportId
3219
3221
  }
3220
3222
  }
3221
3223
  `;
@@ -3286,7 +3288,10 @@ var FlightDeskAPI = class _FlightDeskAPI {
3286
3288
  input.projectId = options.projectId;
3287
3289
  }
3288
3290
  if (options?.status) {
3289
- input.status = [options.status];
3291
+ input.status = Array.isArray(options.status) ? options.status : [options.status];
3292
+ }
3293
+ if (options?.limit) {
3294
+ input.limit = options.limit;
3290
3295
  }
3291
3296
  const result = await this.graphql(query, { input });
3292
3297
  return result.userTasks;
@@ -3389,6 +3394,11 @@ var FlightDeskAPI = class _FlightDeskAPI {
3389
3394
  createdAt
3390
3395
  updatedAt
3391
3396
  sshConnectionString
3397
+ processUrls {
3398
+ name
3399
+ url
3400
+ primary
3401
+ }
3392
3402
  }
3393
3403
  }
3394
3404
  `;
@@ -3864,44 +3874,63 @@ function waitForEnter() {
3864
3874
  });
3865
3875
  });
3866
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
+ }
3867
3912
  async function scrapeSession(page, sessionUrl, options = {}) {
3868
3913
  const { timeout = 3e4, autoPr = false } = options;
3869
3914
  try {
3870
3915
  page.setDefaultTimeout(timeout);
3871
- await page.goto(sessionUrl, { waitUntil: "domcontentloaded", timeout });
3872
- await page.waitForSelector('[data-testid="conversation-turn"], .code-spinner-animate, button:has-text("Create PR")', {
3873
- timeout: 1e4
3874
- }).catch(() => {
3875
- });
3876
- const result = { status: "active" };
3916
+ const debug = process.env.FLIGHTDESK_DEBUG === "1";
3917
+ await navigateAndPrepare(page, sessionUrl, timeout, debug);
3918
+ if (debug) await captureDebugInfo(page);
3877
3919
  const archiveIndicator = await page.$("text=This session has been archived");
3878
- if (archiveIndicator) {
3879
- return { status: "archived" };
3880
- }
3920
+ if (archiveIndicator) return { status: "archived" };
3881
3921
  const url = page.url();
3882
3922
  if (url.includes("/login") || url.includes("/oauth")) {
3883
- return {
3884
- status: "error",
3885
- error: "Not logged in to Claude. Run: flightdesk auth"
3886
- };
3923
+ return { status: "error", error: "Not logged in to Claude. Run: flightdesk auth" };
3887
3924
  }
3888
- const isActive = await detectActiveSpinner(page);
3889
- result.isActive = isActive;
3925
+ const result = { status: "active" };
3926
+ result.isActive = await detectActiveSpinner(page, debug);
3890
3927
  const branchName = await extractBranchName(page);
3891
- if (branchName) {
3892
- result.branchName = branchName;
3893
- }
3928
+ if (branchName) result.branchName = branchName;
3894
3929
  const createPrButton = await page.$('button:has-text("Create PR"):not([aria-haspopup])');
3895
3930
  const viewPrButton = await page.$('button:has-text("View PR")');
3896
3931
  result.hasPrButton = !!createPrButton && !viewPrButton;
3897
3932
  if (autoPr && createPrButton && !viewPrButton) {
3898
- console.log(' Clicking "Create PR" button...');
3899
- await createPrButton.click();
3900
- await page.waitForTimeout(5e3);
3901
- const prUrl = await extractPrUrl(page);
3902
- if (prUrl) {
3903
- result.prUrl = prUrl;
3904
- }
3933
+ result.prUrl = await autoCreatePr(page, createPrButton);
3905
3934
  }
3906
3935
  return result;
3907
3936
  } catch (error) {
@@ -3994,58 +4023,27 @@ async function extractPrUrl(page) {
3994
4023
  }
3995
4024
  return null;
3996
4025
  }
3997
- async function detectActiveSpinner(page) {
4026
+ async function detectActiveSpinner(page, debug = false) {
3998
4027
  try {
3999
- const spinner = await page.$(".code-spinner-animate");
4000
- if (spinner) {
4001
- const isVisibleAndAnimating = await spinner.evaluate((el) => {
4002
- const style = globalThis.getComputedStyle(el);
4003
- if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") {
4004
- return false;
4005
- }
4006
- if (style.animationName && style.animationName !== "none") {
4007
- return true;
4008
- }
4009
- if (style.animationPlayState === "paused") {
4010
- return false;
4011
- }
4012
- const rect = el.getBoundingClientRect();
4013
- return rect.width > 0 && rect.height > 0;
4014
- });
4015
- if (isVisibleAndAnimating) {
4016
- return true;
4017
- }
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}`);
4018
4032
  }
4019
- const spinnerByContent = await page.$('span:has-text("\u273D")');
4020
- if (spinnerByContent) {
4021
- const hasAnimation = await spinnerByContent.evaluate((el) => {
4022
- let current = el;
4023
- while (current) {
4024
- const classList = current.classList;
4025
- if (classList?.contains("code-spinner-animate")) {
4026
- const style2 = globalThis.getComputedStyle(current);
4027
- if (style2.display === "none" || style2.visibility === "hidden" || style2.opacity === "0") {
4028
- current = current.parentElement;
4029
- continue;
4030
- }
4031
- if (style2.animationPlayState === "paused") {
4032
- current = current.parentElement;
4033
- continue;
4034
- }
4035
- return style2.animationName !== "none";
4036
- }
4037
- const style = globalThis.getComputedStyle(current);
4038
- if (style.animationName && style.animationName !== "none" && style.animationPlayState !== "paused") {
4039
- return true;
4040
- }
4041
- current = current.parentElement;
4042
- }
4043
- return false;
4044
- });
4045
- return hasAnimation;
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;
4046
4042
  }
4043
+ if (debug) console.log(" [DEBUG] No activity indicators: IDLE");
4047
4044
  return false;
4048
- } catch {
4045
+ } catch (error) {
4046
+ if (debug) console.log(` [DEBUG] Spinner detection error: ${error}`);
4049
4047
  return false;
4050
4048
  }
4051
4049
  }
@@ -4415,7 +4413,11 @@ async function watchCommand(options) {
4415
4413
  const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
4416
4414
  console.log(`[${timestamp}] Checking tasks...`);
4417
4415
  try {
4418
- const tasks = await api.listTasks();
4416
+ const ACTIVE_STATUSES = ["PENDING", "DISPATCHED", "IN_PROGRESS", "BRANCH_CREATED"];
4417
+ const tasks = await api.listTasks({
4418
+ status: ACTIVE_STATUSES,
4419
+ limit: 200
4420
+ });
4419
4421
  const activeTasks = tasks.filter(
4420
4422
  (t) => ["DISPATCHED", "IN_PROGRESS", "BRANCH_CREATED"].includes(t.status) || t.status === "PENDING" && t.sessionViewUrl
4421
4423
  );
@@ -4663,15 +4665,34 @@ ${task2.description}`);
4663
4665
  Created: ${new Date(task2.createdAt).toLocaleString()}`);
4664
4666
  console.log(`Updated: ${new Date(task2.updatedAt).toLocaleString()}`);
4665
4667
  }
4668
+ function parseSessionId(value) {
4669
+ const urlPrefix = "https://claude.ai/code/";
4670
+ if (value.startsWith(urlPrefix)) {
4671
+ return value.slice(urlPrefix.length);
4672
+ }
4673
+ return value;
4674
+ }
4666
4675
  async function handleUpdate(api, options) {
4667
4676
  const input = {};
4668
4677
  if (options.status) input.status = options.status;
4669
4678
  if (options.branch) input.branchName = options.branch;
4670
4679
  if (options.prUrl) input.prUrl = options.prUrl;
4680
+ if (options.session) {
4681
+ const sessionId = parseSessionId(options.session);
4682
+ input.sessionViewUrl = `https://claude.ai/code/${sessionId}`;
4683
+ input.sessionTeleportId = sessionId;
4684
+ }
4671
4685
  if (Object.keys(input).length === 0) {
4672
- console.error("No updates specified. Use --status, --branch, or --pr-url");
4686
+ console.error("No updates specified. Use --status, --branch, --pr-url, or --session");
4673
4687
  process.exit(1);
4674
4688
  }
4689
+ if (options.branch && !options.status) {
4690
+ const current = await api.getTask(options.taskId);
4691
+ if (current?.status === "DISPATCHED") {
4692
+ input.status = "IN_PROGRESS";
4693
+ console.log(` Auto-transitioning DISPATCHED \u2192 IN_PROGRESS`);
4694
+ }
4695
+ }
4675
4696
  console.log(`Updating task ${options.taskId}...`);
4676
4697
  const task2 = await api.updateTask(options.taskId, input);
4677
4698
  console.log(`
@@ -4679,6 +4700,7 @@ async function handleUpdate(api, options) {
4679
4700
  console.log(` Status: ${task2.status}`);
4680
4701
  if (task2.branchName) console.log(` Branch: ${task2.branchName}`);
4681
4702
  if (task2.prUrl) console.log(` PR: ${task2.prUrl}`);
4703
+ if (task2.sessionViewUrl) console.log(` Session: ${task2.sessionViewUrl}`);
4682
4704
  }
4683
4705
  function getStatusEmoji2(status) {
4684
4706
  switch (status) {
@@ -5466,7 +5488,15 @@ async function handleStatus2(api, options) {
5466
5488
  console.log("\u2500".repeat(50));
5467
5489
  console.log(`${emoji} Status: ${instance.status}`);
5468
5490
  console.log(` ID: ${instance.id.substring(0, 8)}`);
5469
- console.log(` Preview URL: ${instance.previewUrl}`);
5491
+ if (instance.processUrls && instance.processUrls.length > 0) {
5492
+ console.log(" URLs:");
5493
+ for (const pu of instance.processUrls) {
5494
+ const label = pu.primary ? `${pu.name} (primary)` : pu.name;
5495
+ console.log(` ${label}: ${pu.url}`);
5496
+ }
5497
+ } else {
5498
+ console.log(` Preview URL: ${instance.previewUrl}`);
5499
+ }
5470
5500
  console.log(` SSH: ${instance.sshConnectionString}`);
5471
5501
  console.log(` Container: ${instance.containerId.substring(0, 12)}`);
5472
5502
  if (instance.lastActivityAt) {
@@ -5541,7 +5571,7 @@ async function handleMount(api, options) {
5541
5571
  console.error("");
5542
5572
  console.error("Install it with:");
5543
5573
  if (process.platform === "darwin") {
5544
- console.error(" brew install macfuse sshfs");
5574
+ console.error(" brew install macfuse gromgit/fuse/sshfs-mac");
5545
5575
  } else if (process.platform === "linux") {
5546
5576
  console.error(" sudo apt install sshfs");
5547
5577
  } else {
@@ -5658,7 +5688,7 @@ async function handleTeardown(api, options) {
5658
5688
 
5659
5689
  // apps/cli/src/main.ts
5660
5690
  var program2 = new Command();
5661
- program2.name("flightdesk").description("FlightDesk CLI - AI task management for Claude Code sessions").version("0.2.5").option("--dev", "Use local development API (localhost:3000)").option("--api <url>", "Use custom API URL");
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");
5662
5692
  program2.hook("preAction", () => {
5663
5693
  const opts = program2.opts();
5664
5694
  if (opts.api) {
@@ -5679,7 +5709,7 @@ var task = program2.command("task").description("Task management commands");
5679
5709
  task.command("create").description("Create a new task").requiredOption("-p, --project <id>", "Project ID").requiredOption("-t, --title <title>", "Task title").option("-d, --description <description>", "Task description").action((options) => taskCommand("create", options));
5680
5710
  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));
5681
5711
  task.command("status <task-id>").description("Get task status").action((taskId) => taskCommand("status", { taskId }));
5682
- 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").action((taskId, options) => taskCommand("update", { taskId, ...options }));
5712
+ 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 }));
5683
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);
5684
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);
5685
5715
  program2.command("status").description("Show status of all active tasks").option("-p, --project <id>", "Filter by project").action(statusCommand);