flightdesk 0.1.5 → 0.1.7

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 +236 -153
  2. package/main.js.map +4 -4
  3. package/package.json +1 -1
package/main.js CHANGED
@@ -3035,15 +3035,25 @@ var {
3035
3035
  Help
3036
3036
  } = import_index.default;
3037
3037
 
3038
- // apps/cli/src/commands/init.ts
3039
- var readline = __toESM(require("readline"));
3040
-
3041
3038
  // apps/cli/src/lib/config.ts
3042
3039
  var fs = __toESM(require("fs"));
3043
3040
  var path = __toESM(require("path"));
3044
3041
  var os = __toESM(require("os"));
3045
3042
  var CONFIG_FILE = path.join(os.homedir(), ".flightdeskrc");
3046
- var DEFAULT_API_URL = "https://flightdesk.dev/api";
3043
+ var DEFAULT_API_URL = "https://api.flightdesk.dev";
3044
+ var DEV_API_URL = "http://localhost:3000";
3045
+ var apiUrlOverride = null;
3046
+ function setDevMode(enabled) {
3047
+ if (enabled) {
3048
+ apiUrlOverride = DEV_API_URL;
3049
+ }
3050
+ }
3051
+ function setApiUrl(url) {
3052
+ apiUrlOverride = url;
3053
+ }
3054
+ function getApiUrl() {
3055
+ return apiUrlOverride ?? DEFAULT_API_URL;
3056
+ }
3047
3057
  function loadConfig() {
3048
3058
  try {
3049
3059
  if (fs.existsSync(CONFIG_FILE)) {
@@ -3111,6 +3121,7 @@ function isConfigured() {
3111
3121
  }
3112
3122
 
3113
3123
  // apps/cli/src/commands/init.ts
3124
+ var readline = __toESM(require("readline"));
3114
3125
  function question(rl, prompt) {
3115
3126
  return new Promise((resolve) => {
3116
3127
  rl.question(prompt, (answer) => {
@@ -3151,8 +3162,8 @@ async function initCommand() {
3151
3162
  rl.close();
3152
3163
  return;
3153
3164
  }
3154
- const apiUrlInput = await question(rl, `API URL (${DEFAULT_API_URL}): `);
3155
- const apiUrl = apiUrlInput || DEFAULT_API_URL;
3165
+ const apiUrl = getApiUrl();
3166
+ console.log(`Using API: ${apiUrl}`);
3156
3167
  let orgId;
3157
3168
  const keyMatch = apiKey.match(/^fd_key_([^_]+)_/);
3158
3169
  if (keyMatch) {
@@ -3211,9 +3222,10 @@ async function initCommand() {
3211
3222
  }
3212
3223
 
3213
3224
  // apps/cli/src/lib/session-monitor.ts
3214
- var path2 = __toESM(require("path"));
3215
- var os2 = __toESM(require("os"));
3216
- var fs2 = __toESM(require("fs"));
3225
+ var path2 = __toESM(require("node:path"));
3226
+ var os2 = __toESM(require("node:os"));
3227
+ var fs2 = __toESM(require("node:fs"));
3228
+ var readline2 = __toESM(require("node:readline"));
3217
3229
  var playwright = null;
3218
3230
  var USER_DATA_DIR = path2.join(os2.homedir(), ".flightdesk", "chromium-profile");
3219
3231
  async function isPlaywrightAvailable() {
@@ -3249,10 +3261,16 @@ async function checkAuth() {
3249
3261
  const { context } = await launchBrowser(true);
3250
3262
  try {
3251
3263
  const page = await context.newPage();
3252
- await page.goto("https://claude.ai/", { waitUntil: "domcontentloaded" });
3264
+ page.setDefaultTimeout(3e4);
3265
+ await page.goto("https://claude.ai/", { waitUntil: "domcontentloaded", timeout: 3e4 });
3266
+ await page.waitForTimeout(3e3);
3253
3267
  const url = page.url();
3254
- const isLoggedIn = !url.includes("/login") && !url.includes("/oauth");
3268
+ console.log(" Final URL:", url);
3269
+ const isLoggedIn = !url.includes("/login") && !url.includes("/oauth") && !url.includes("accounts.google") && url.includes("claude.ai");
3255
3270
  return isLoggedIn;
3271
+ } catch (error) {
3272
+ console.error("Auth check failed:", error.message);
3273
+ return false;
3256
3274
  } finally {
3257
3275
  await context.close();
3258
3276
  }
@@ -3262,15 +3280,20 @@ async function openForLogin() {
3262
3280
  throw new Error("Playwright not installed");
3263
3281
  }
3264
3282
  console.log("Opening browser for Claude login...");
3265
- console.log("Please log in, then close the browser window.");
3266
3283
  const { context } = await launchBrowser(false);
3267
3284
  try {
3268
3285
  const page = await context.newPage();
3269
- await page.goto("https://claude.ai/", { waitUntil: "domcontentloaded" });
3270
- await new Promise((resolve) => {
3271
- context.on("close", () => resolve());
3272
- });
3273
- console.log("Browser closed. Checking login status...");
3286
+ await page.goto("https://claude.ai/", { waitUntil: "domcontentloaded", timeout: 6e4 });
3287
+ console.log("\n\u{1F449} Press ENTER here when you have logged in...\n");
3288
+ await waitForEnter();
3289
+ console.log("Verifying login in current session...");
3290
+ await page.goto("https://claude.ai/", { waitUntil: "domcontentloaded", timeout: 3e4 });
3291
+ await page.waitForTimeout(2e3);
3292
+ const url = page.url();
3293
+ console.log(" Final URL:", url);
3294
+ const isLoggedIn = !url.includes("/login") && !url.includes("/oauth") && !url.includes("accounts.google") && url.includes("claude.ai");
3295
+ console.log("Closing browser...");
3296
+ return isLoggedIn;
3274
3297
  } finally {
3275
3298
  try {
3276
3299
  await context.close();
@@ -3278,6 +3301,18 @@ async function openForLogin() {
3278
3301
  }
3279
3302
  }
3280
3303
  }
3304
+ function waitForEnter() {
3305
+ return new Promise((resolve) => {
3306
+ const rl = readline2.createInterface({
3307
+ input: process.stdin,
3308
+ output: process.stdout
3309
+ });
3310
+ rl.question("", () => {
3311
+ rl.close();
3312
+ resolve();
3313
+ });
3314
+ });
3315
+ }
3281
3316
  async function monitorSession(sessionUrl, options = {}) {
3282
3317
  if (!await isPlaywrightAvailable()) {
3283
3318
  return { status: "error", error: "Playwright not installed" };
@@ -3305,7 +3340,7 @@ async function monitorSession(sessionUrl, options = {}) {
3305
3340
  if (branchName) {
3306
3341
  result.branchName = branchName;
3307
3342
  }
3308
- const prButton = await page.$('button:has-text("Create PR"), button:has-text("Create Pull Request"), button:has-text("Open PR")');
3343
+ const prButton = await page.$('button:has-text("Create PR"):not([aria-haspopup])');
3309
3344
  result.hasPrButton = !!prButton;
3310
3345
  if (autoPr && prButton) {
3311
3346
  console.log(' Clicking "Create PR" button...');
@@ -3327,21 +3362,18 @@ async function monitorSession(sessionUrl, options = {}) {
3327
3362
  }
3328
3363
  }
3329
3364
  async function extractBranchName(page) {
3330
- const selectors = [
3331
- // Look for git branch indicators
3332
- '[data-testid="branch-name"]',
3333
- ".branch-name",
3334
- // Look for text patterns
3335
- "text=/\\b(feature|fix|bugfix|hotfix|release|develop|main|master)\\/[\\w-]+/i"
3365
+ const primarySelectors = [
3366
+ String.raw`button.group\/copy span.truncate`
3367
+ // New branch name (verified)
3336
3368
  ];
3337
- for (const selector of selectors) {
3369
+ for (const selector of primarySelectors) {
3338
3370
  try {
3339
3371
  const element = await page.$(selector);
3340
3372
  if (element) {
3341
3373
  const text = await element.textContent();
3342
3374
  if (text) {
3343
3375
  const cleaned = text.trim();
3344
- if (cleaned && !cleaned.includes(" ")) {
3376
+ if (cleaned && cleaned.length > 2 && !cleaned.includes(" ")) {
3345
3377
  return cleaned;
3346
3378
  }
3347
3379
  }
@@ -3349,24 +3381,6 @@ async function extractBranchName(page) {
3349
3381
  } catch {
3350
3382
  }
3351
3383
  }
3352
- try {
3353
- const pageContent = await page.content();
3354
- const branchPatterns = [
3355
- /(?:branch|checkout|switched to)[:\s]+['"]?([a-zA-Z0-9/_-]+)['"]?/i,
3356
- /(?:On branch|HEAD -> )\s*([a-zA-Z0-9/_-]+)/i,
3357
- /\[([a-zA-Z0-9/_-]+)\]/
3358
- ];
3359
- for (const pattern of branchPatterns) {
3360
- const match = pageContent.match(pattern);
3361
- if (match && match[1]) {
3362
- const branch = match[1].trim();
3363
- if (branch.length > 2 && branch.length < 100 && !branch.includes("<")) {
3364
- return branch;
3365
- }
3366
- }
3367
- }
3368
- } catch {
3369
- }
3370
3384
  return null;
3371
3385
  }
3372
3386
  async function extractPrUrl(page) {
@@ -3424,15 +3438,13 @@ async function authCommand() {
3424
3438
  }
3425
3439
  console.log("\u274C Not logged in to Claude.\n");
3426
3440
  console.log("Opening browser for login...");
3427
- console.log("Please log in to your Claude account, then close the browser.\n");
3428
- await openForLogin();
3429
- console.log("\nVerifying login...");
3430
- const nowAuthenticated = await checkAuth();
3431
- if (nowAuthenticated) {
3432
- console.log("\u2705 Successfully logged in!");
3433
- console.log("\nThe watch daemon can now monitor your Claude Code sessions.");
3441
+ console.log("Please log in to your Claude account.\n");
3442
+ const loginSuccessful = await openForLogin();
3443
+ if (loginSuccessful) {
3444
+ console.log("\n\u2705 Successfully logged in!");
3445
+ console.log("The watch daemon can now monitor your Claude Code sessions.");
3434
3446
  } else {
3435
- console.log("\u274C Login was not detected.");
3447
+ console.log("\n\u274C Login was not detected.");
3436
3448
  console.log("Please try again with: flightdesk auth");
3437
3449
  }
3438
3450
  }
@@ -3442,6 +3454,7 @@ var FlightDeskAPI = class {
3442
3454
  constructor(org2) {
3443
3455
  this.apiUrl = org2.apiUrl;
3444
3456
  this.apiKey = org2.apiKey;
3457
+ this.organizationId = org2.id;
3445
3458
  }
3446
3459
  async graphql(query, variables) {
3447
3460
  const response = await fetch(`${this.apiUrl}/graphql`, {
@@ -3566,15 +3579,15 @@ var FlightDeskAPI = class {
3566
3579
  // ============================================================================
3567
3580
  async listProjects() {
3568
3581
  const query = `
3569
- query ListProjects {
3570
- userProjects {
3582
+ query ListProjects($organizationId: String!) {
3583
+ userProjects(organizationId: $organizationId) {
3571
3584
  id
3572
3585
  name
3573
3586
  githubRepo
3574
3587
  }
3575
3588
  }
3576
3589
  `;
3577
- const result = await this.graphql(query);
3590
+ const result = await this.graphql(query, { organizationId: this.organizationId });
3578
3591
  return result.userProjects;
3579
3592
  }
3580
3593
  async getProject(projectId) {
@@ -3902,7 +3915,7 @@ Watching... (Ctrl+C to stop)
3902
3915
  clearInterval(intervalId);
3903
3916
  process.exit(0);
3904
3917
  });
3905
- await new Promise(() => {
3918
+ await new Promise((_resolve) => {
3906
3919
  });
3907
3920
  }
3908
3921
  async function processSessionInfo(api, task2, info) {
@@ -4128,7 +4141,7 @@ async function promptCommand(taskId, options) {
4128
4141
  }
4129
4142
 
4130
4143
  // apps/cli/src/commands/org.ts
4131
- var readline2 = __toESM(require("readline"));
4144
+ var readline3 = __toESM(require("readline"));
4132
4145
  function question2(rl, prompt) {
4133
4146
  return new Promise((resolve) => {
4134
4147
  rl.question(prompt, (answer) => {
@@ -4163,7 +4176,7 @@ async function orgListCommand() {
4163
4176
  }
4164
4177
  }
4165
4178
  async function orgAddCommand() {
4166
- const rl = readline2.createInterface({
4179
+ const rl = readline3.createInterface({
4167
4180
  input: process.stdin,
4168
4181
  output: process.stdout
4169
4182
  });
@@ -4179,8 +4192,8 @@ async function orgAddCommand() {
4179
4192
  console.error("API Key is required");
4180
4193
  return;
4181
4194
  }
4182
- const apiUrlInput = await question2(rl, `API URL (${DEFAULT_API_URL}): `);
4183
- const apiUrl = apiUrlInput || DEFAULT_API_URL;
4195
+ const apiUrl = getApiUrl();
4196
+ console.log(`Using API: ${apiUrl}`);
4184
4197
  let orgId;
4185
4198
  const keyMatch = apiKey.match(/^fd_key_([^_]+)_/);
4186
4199
  if (keyMatch) {
@@ -4244,7 +4257,7 @@ async function orgRemoveCommand(orgId) {
4244
4257
  }
4245
4258
  process.exit(1);
4246
4259
  }
4247
- const rl = readline2.createInterface({
4260
+ const rl = readline3.createInterface({
4248
4261
  input: process.stdin,
4249
4262
  output: process.stdout
4250
4263
  });
@@ -4276,7 +4289,7 @@ async function orgSetDefaultCommand(orgId) {
4276
4289
  var import_child_process = require("child_process");
4277
4290
  async function contextCommand() {
4278
4291
  console.log("\n\u{1F50D} Current Context\n");
4279
- let repoInfo = {};
4292
+ const repoInfo = {};
4280
4293
  try {
4281
4294
  const remoteUrl = (0, import_child_process.execSync)("git remote get-url origin", {
4282
4295
  encoding: "utf-8",
@@ -4387,52 +4400,60 @@ async function syncCommand() {
4387
4400
  // apps/cli/src/lib/claude-selectors.ts
4388
4401
  var ClaudeSelectors = {
4389
4402
  // ============================================================================
4390
- // Sidebar - Session List
4403
+ // Sidebar - Session List (Verified 2026-02-23)
4391
4404
  // ============================================================================
4392
4405
  sidebar: {
4393
4406
  /** New task input (textarea in sidebar) */
4394
- newTaskInput: 'aside textarea[placeholder*="task"]',
4407
+ newTaskInput: 'textarea[placeholder="Ask Claude to write code..."]',
4395
4408
  /** Session list container */
4396
- sessionList: "aside nav",
4409
+ sessionList: String.raw`.flex.flex-col.gap-0\.5.px-1`,
4397
4410
  /** Individual session items - use :has-text() with title */
4398
- sessionItem: (title) => `aside a:has-text("${title}")`,
4399
- /** All session links */
4400
- allSessions: 'aside nav a[href*="/chat/"]',
4401
- /** Active/selected session */
4402
- activeSession: 'aside a[aria-current="page"]',
4411
+ sessionItem: (title) => `.cursor-pointer:has-text("${title}")`,
4412
+ /** All session items (div elements with cursor-pointer class) */
4413
+ allSessions: String.raw`.flex.flex-col.gap-0\.5.px-1 .cursor-pointer`,
4414
+ /** Session by index (0-indexed) */
4415
+ sessionByIndex: (index) => String.raw`.flex.flex-col.gap-0\.5.px-1` + ` > div:nth-child(${index + 1}) .cursor-pointer`,
4416
+ /** Active/selected session (has bg-bg-300 class) */
4417
+ activeSession: ".cursor-pointer.bg-bg-300",
4403
4418
  /** Session title within a session item */
4404
4419
  sessionTitle: "button.text-sm.font-medium.truncate",
4405
- /** Session menu button (three dots) */
4406
- sessionMenuButton: 'button[aria-label*="menu"], button:has(svg[class*="dots"])'
4420
+ /** Session archive button (icon-only, no aria-label) */
4421
+ sessionArchiveButton: (title) => `.cursor-pointer:has-text("${title}") button`,
4422
+ /** Search sessions button */
4423
+ searchButton: 'button[aria-label="Search \u2318K"]',
4424
+ /** User profile button */
4425
+ userProfileButton: '[data-testid="code-user-menu-button"]'
4407
4426
  },
4408
4427
  // ============================================================================
4409
- // Branch Bar (top of chat when branch exists)
4428
+ // Branch Bar (between chat messages and chat input) - Verified 2026-02-23
4410
4429
  // ============================================================================
4411
4430
  branchBar: {
4412
- /** Container for branch info */
4413
- container: 'div:has(button:has-text("Branch"))',
4431
+ /** Container for branch bar */
4432
+ container: ".flex.items-center.gap-2.w-full.p-2",
4414
4433
  /** Branch name display - the copyable span */
4415
4434
  branchName: "button.group\\/copy span.truncate",
4416
- /** Branch selector dropdown trigger */
4417
- branchSelector: 'button[aria-haspopup="listbox"]:has-text("Branch")',
4418
- /** Copy branch button */
4419
- copyBranchButton: "button.group\\/copy"
4435
+ /** Base/target branch dropdown (e.g., "develop") */
4436
+ baseBranchSelector: '.flex.items-center.gap-2.w-full.p-2 button[aria-haspopup="menu"]',
4437
+ /** Copy branch button (click to copy branch name) */
4438
+ copyBranchButton: "button.group\\/copy",
4439
+ /** Diff stats button (+N -M) */
4440
+ diffStatsButton: String.raw`.flex.items-center.gap-2.w-full.p-2 button.border-0\.5`
4420
4441
  },
4421
4442
  // ============================================================================
4422
- // Pull Request Controls
4443
+ // Pull Request Controls - Verified 2026-02-23
4423
4444
  // ============================================================================
4424
4445
  pullRequest: {
4425
4446
  /** Create PR button (left half - immediate action) - CAREFUL: clicks immediately! */
4426
4447
  createPrButton: 'button:has-text("Create PR"):not([aria-haspopup])',
4427
- /** PR dropdown trigger (right half - shows options) */
4428
- prDropdown: 'button[aria-haspopup="menu"]:has(svg[class*="chevron"])',
4448
+ /** PR dropdown trigger (right half - shows options) - SAFE to click */
4449
+ prDropdown: 'button.rounded-r-md[aria-haspopup="menu"]',
4429
4450
  /** PR status indicator when PR exists */
4430
4451
  prStatus: 'a[href*="/pull/"], a[href*="/merge_requests/"]',
4431
4452
  /** View PR link */
4432
4453
  viewPrLink: 'a:has-text("View PR"), a:has-text("View Pull Request")'
4433
4454
  },
4434
4455
  // ============================================================================
4435
- // Chat Area
4456
+ // Chat Area - Verified 2026-02-23
4436
4457
  // ============================================================================
4437
4458
  chat: {
4438
4459
  /** Main chat container */
@@ -4443,43 +4464,56 @@ var ClaudeSelectors = {
4443
4464
  userMessages: 'div[data-testid="user-message"]',
4444
4465
  /** Assistant messages */
4445
4466
  assistantMessages: 'div[data-testid="assistant-message"]',
4446
- /** Chat input (ProseMirror contenteditable) */
4447
- input: 'div.ProseMirror[contenteditable="true"]',
4467
+ /** Chat input (ProseMirror contenteditable) - NOT a textarea */
4468
+ input: '[aria-label="Enter your turn"]',
4469
+ /** Chat input fallback */
4470
+ inputFallback: 'div.tiptap.ProseMirror[contenteditable="true"]',
4471
+ /** Toggle menu button (+) */
4472
+ toggleMenuButton: 'button[aria-label="Toggle menu"]',
4448
4473
  /** Send button */
4449
- sendButton: 'button[aria-label*="Send"], button:has(svg[class*="arrow"])',
4474
+ sendButton: 'form.w-full button[aria-label="Submit"]',
4450
4475
  /** Stop button (during generation) */
4451
4476
  stopButton: 'button:has-text("Stop")'
4452
4477
  },
4453
4478
  // ============================================================================
4454
- // Model Selector
4479
+ // Model Selector - Verified 2026-02-23
4455
4480
  // ============================================================================
4456
4481
  model: {
4457
- /** Model selector dropdown */
4458
- selector: 'button[aria-haspopup="listbox"]:has-text("Claude")',
4459
- /** Model options in dropdown */
4460
- options: 'div[role="listbox"] div[role="option"]',
4482
+ /** Model selector dropdown (sidebar) */
4483
+ sidebarSelector: 'form:not(.w-full) [data-testid="model-selector-dropdown"]',
4484
+ /** Model selector dropdown (main chat) */
4485
+ chatSelector: 'form.w-full [data-testid="model-selector-dropdown"]',
4486
+ /** Model options in dropdown (role="menuitem") */
4487
+ options: '[role="menuitem"]',
4461
4488
  /** Specific model option */
4462
- option: (modelName) => `div[role="option"]:has-text("${modelName}")`
4489
+ option: (modelName) => `[role="menuitem"]:has-text("${modelName}")`,
4490
+ /** Quick model selectors */
4491
+ opus: '[role="menuitem"]:has-text("Opus")',
4492
+ sonnet: '[role="menuitem"]:has-text("Sonnet")',
4493
+ haiku: '[role="menuitem"]:has-text("Haiku")'
4463
4494
  },
4464
4495
  // ============================================================================
4465
- // Mode Toggle (Chat/Code)
4496
+ // Mode Toggle - Verified 2026-02-23
4497
+ // Text alternates between "Auto accept edits" and "Plan mode"
4466
4498
  // ============================================================================
4467
4499
  mode: {
4468
- /** Mode toggle button - simple button, not dropdown */
4469
- toggle: 'button:has-text("Code"), button:has-text("Chat")',
4470
- /** Current mode indicator */
4471
- currentMode: 'button[aria-pressed="true"]'
4500
+ /** Mode toggle button in sidebar - simple toggle, not dropdown */
4501
+ sidebarToggle: 'form:not(.w-full) button:has-text("Auto accept edits"), form:not(.w-full) button:has-text("Plan mode")',
4502
+ /** Mode toggle button in main chat */
4503
+ chatToggle: 'form.w-full button:has-text("Auto accept edits"), form.w-full button:has-text("Plan mode")'
4472
4504
  },
4473
4505
  // ============================================================================
4474
- // Repository Connection
4506
+ // Repository Connection - Verified 2026-02-23
4475
4507
  // ============================================================================
4476
4508
  repository: {
4477
- /** Connect repo button (when no repo connected) */
4478
- connectButton: 'button:has-text("Connect"), button:has-text("Repository")',
4479
- /** Repo name when connected */
4480
- repoName: 'a[href*="github.com"], span:has-text("/")',
4481
- /** Disconnect/change repo button */
4482
- changeRepoButton: 'button:has-text("Change"), button:has-text("Disconnect")'
4509
+ /** Repo button in sidebar (shows current repo name) */
4510
+ repoButton: "form:not(.w-full) button.flex.items-center.gap-1.min-w-0.rounded",
4511
+ /** Repo button by name */
4512
+ repoButtonByName: (repoName) => `form:not(.w-full) button:has-text("${repoName}")`,
4513
+ /** Add repository button (hidden until hover) */
4514
+ addRepoButton: 'button[aria-label="Add repository"]',
4515
+ /** Select repository dropdown */
4516
+ selectRepoDropdown: 'form:not(.w-full) button[aria-haspopup="menu"]:has-text("Select repository")'
4483
4517
  },
4484
4518
  // ============================================================================
4485
4519
  // Session Archive
@@ -4515,17 +4549,6 @@ var ClaudeSelectors = {
4515
4549
  listboxOptions: 'div[role="option"]'
4516
4550
  }
4517
4551
  };
4518
- function parseRepoName(text) {
4519
- const slashMatch = text.match(/^([a-zA-Z0-9_-]+)\/([a-zA-Z0-9_.-]+)$/);
4520
- if (slashMatch) {
4521
- return { owner: slashMatch[1], repo: slashMatch[2] };
4522
- }
4523
- const urlMatch = text.match(/github\.com\/([a-zA-Z0-9_-]+)\/([a-zA-Z0-9_.-]+)/);
4524
- if (urlMatch) {
4525
- return { owner: urlMatch[1], repo: urlMatch[2] };
4526
- }
4527
- return null;
4528
- }
4529
4552
 
4530
4553
  // apps/cli/src/commands/import.ts
4531
4554
  var fs3 = __toESM(require("fs"));
@@ -4587,7 +4610,7 @@ Found ${sessions.length} sessions:
4587
4610
  }
4588
4611
  for (const [repoName, repoSessions] of sessionsByRepo) {
4589
4612
  const matchingProject = existingProjects.find(
4590
- (p) => p.githubRepo?.includes(repoName) || p.name === repoName
4613
+ (p) => p.githubRepo?.endsWith(`/${repoName}`)
4591
4614
  );
4592
4615
  if (matchingProject) {
4593
4616
  console.log(`\u{1F4C1} ${repoName} \u2192 Project: ${matchingProject.name} (${matchingProject.id.slice(0, 8)})`);
@@ -4626,12 +4649,52 @@ Summary:`);
4626
4649
  console.log("\u{1F4A1} Run without --dry-run to create tasks in FlightDesk");
4627
4650
  return;
4628
4651
  }
4629
- console.log("To import these sessions:");
4630
- console.log("");
4631
- console.log("1. Create projects in FlightDesk for each repository");
4632
- console.log('2. Use `flightdesk register <project-id> --title "<session-title>" --view-url "<url>"` for each session');
4652
+ const importableSessions = [];
4653
+ for (const [repoName, repoSessions] of sessionsByRepo) {
4654
+ const matchingProject = existingProjects.find(
4655
+ (p) => p.githubRepo?.endsWith(`/${repoName}`)
4656
+ );
4657
+ if (matchingProject) {
4658
+ for (const session of repoSessions) {
4659
+ importableSessions.push({ session, project: matchingProject });
4660
+ }
4661
+ }
4662
+ }
4663
+ if (importableSessions.length === 0) {
4664
+ console.log("No sessions can be imported (no matching projects).");
4665
+ console.log("Create projects in FlightDesk first for the repositories you want to track.");
4666
+ return;
4667
+ }
4668
+ console.log(`
4669
+ \u{1F680} Importing ${importableSessions.length} sessions...
4670
+ `);
4671
+ let created = 0;
4672
+ let failed = 0;
4673
+ for (const { session, project } of importableSessions) {
4674
+ try {
4675
+ const task2 = await api.createTask({
4676
+ projectId: project.id,
4677
+ title: session.title,
4678
+ description: `Imported from Claude Code session`
4679
+ });
4680
+ await api.updateTask(task2.id, {
4681
+ branchName: session.branchName,
4682
+ sessionViewUrl: session.url
4683
+ });
4684
+ console.log(` \u2705 ${session.title.slice(0, 50)}`);
4685
+ created++;
4686
+ } catch (error) {
4687
+ console.error(` \u274C ${session.title.slice(0, 50)}: ${error.message}`);
4688
+ failed++;
4689
+ }
4690
+ }
4633
4691
  console.log("");
4634
- console.log("Or use the web interface at your FlightDesk dashboard.");
4692
+ console.log("\u2500".repeat(60));
4693
+ console.log(`
4694
+ Import complete:`);
4695
+ console.log(` Created: ${created}`);
4696
+ console.log(` Failed: ${failed}`);
4697
+ console.log(` Skipped (no matching project): ${sessions.length - importableSessions.length}`);
4635
4698
  }
4636
4699
  async function scanClaudeSessions(options) {
4637
4700
  if (!playwright2) {
@@ -4647,36 +4710,59 @@ async function scanClaudeSessions(options) {
4647
4710
  });
4648
4711
  try {
4649
4712
  const page = await context.newPage();
4650
- page.setDefaultTimeout(3e4);
4651
- console.log("Navigating to Claude.ai...");
4652
- await page.goto("https://claude.ai/", { waitUntil: "domcontentloaded" });
4653
- await page.waitForTimeout(3e3);
4713
+ page.setDefaultTimeout(6e4);
4714
+ console.log("Navigating to Claude Code...");
4715
+ await page.goto("https://claude.ai/code", { waitUntil: "domcontentloaded", timeout: 6e4 });
4716
+ if (options.verbose) {
4717
+ console.log("Waiting for page to load...");
4718
+ }
4719
+ await page.waitForTimeout(5e3);
4654
4720
  const url = page.url();
4721
+ if (options.verbose) {
4722
+ console.log("Current URL:", url);
4723
+ }
4655
4724
  if (url.includes("/login") || url.includes("/oauth")) {
4656
4725
  console.log("\n\u26A0\uFE0F Not logged into Claude. Run: flightdesk auth\n");
4657
4726
  return [];
4658
4727
  }
4659
4728
  console.log("Logged in. Scanning sessions...");
4729
+ await page.waitForTimeout(3e3);
4660
4730
  const sessions = [];
4661
- const sessionLinks = await page.$$(ClaudeSelectors.sidebar.allSessions);
4731
+ const sessionItems = await page.$$(ClaudeSelectors.sidebar.allSessions);
4662
4732
  if (options.verbose) {
4663
- console.log(`Found ${sessionLinks.length} session links in sidebar`);
4733
+ console.log(`Found ${sessionItems.length} session items in sidebar`);
4664
4734
  }
4665
- const limit = Math.min(sessionLinks.length, options.limit);
4735
+ const limit = Math.min(sessionItems.length, options.limit);
4666
4736
  for (let i = 0; i < limit; i++) {
4667
- const links = await page.$$(ClaudeSelectors.sidebar.allSessions);
4668
- if (i >= links.length) break;
4669
- const link = links[i];
4737
+ const items = await page.$$(ClaudeSelectors.sidebar.allSessions);
4738
+ if (i >= items.length) break;
4739
+ const item = items[i];
4670
4740
  try {
4671
- const href = await link.getAttribute("href");
4672
- const title = await link.textContent() || "Untitled";
4673
- if (!href) continue;
4674
- const sessionUrl = `https://claude.ai${href}`;
4741
+ const titleElement = await item.$("span.text-text-100");
4742
+ const title = titleElement ? (await titleElement.textContent())?.trim() || "Untitled" : "Untitled";
4743
+ let repoName;
4744
+ try {
4745
+ const repoElement = await item.$("span.text-text-500 span.truncate");
4746
+ if (repoElement) {
4747
+ const repoText = (await repoElement.textContent())?.trim();
4748
+ if (repoText && repoText.length > 0) {
4749
+ repoName = repoText;
4750
+ }
4751
+ }
4752
+ } catch {
4753
+ }
4675
4754
  if (options.verbose) {
4676
- process.stdout.write(`\r Scanning ${i + 1}/${limit}: ${title.slice(0, 40)}...`);
4755
+ const repoDisplay = repoName ? ` (${repoName})` : "";
4756
+ process.stdout.write(`\r Scanning ${i + 1}/${limit}: ${title.slice(0, 35)}${repoDisplay}...`);
4677
4757
  }
4678
- await link.click();
4679
- await page.waitForTimeout(1500);
4758
+ try {
4759
+ await page.keyboard.press("Escape");
4760
+ await page.waitForTimeout(300);
4761
+ } catch {
4762
+ }
4763
+ await item.click();
4764
+ await page.waitForTimeout(2e3);
4765
+ const sessionUrl = page.url();
4680
4766
  let branchName;
4681
4767
  try {
4682
4768
  const branchElement = await page.$(ClaudeSelectors.branchBar.branchName);
@@ -4685,20 +4771,6 @@ async function scanClaudeSessions(options) {
4685
4771
  }
4686
4772
  } catch {
4687
4773
  }
4688
- let repoName;
4689
- try {
4690
- const repoElement = await page.$(ClaudeSelectors.repository.repoName);
4691
- if (repoElement) {
4692
- const repoText = await repoElement.textContent();
4693
- if (repoText) {
4694
- const parsed = parseRepoName(repoText);
4695
- if (parsed) {
4696
- repoName = `${parsed.owner}/${parsed.repo}`;
4697
- }
4698
- }
4699
- }
4700
- } catch {
4701
- }
4702
4774
  const archived = !!await page.$(ClaudeSelectors.archive.indicator);
4703
4775
  sessions.push({
4704
4776
  url: sessionUrl,
@@ -4725,7 +4797,18 @@ async function scanClaudeSessions(options) {
4725
4797
 
4726
4798
  // apps/cli/src/main.ts
4727
4799
  var program2 = new Command();
4728
- program2.name("flightdesk").description("FlightDesk CLI - AI task management for Claude Code sessions").version("0.1.5");
4800
+ program2.name("flightdesk").description("FlightDesk CLI - AI task management for Claude Code sessions").version("0.1.7").option("--dev", "Use local development API (localhost:3000)").option("--api <url>", "Use custom API URL");
4801
+ program2.hook("preAction", () => {
4802
+ const opts = program2.opts();
4803
+ if (opts.api) {
4804
+ setApiUrl(opts.api);
4805
+ console.log(`\u{1F527} Using custom API: ${opts.api}
4806
+ `);
4807
+ } else if (opts.dev) {
4808
+ setDevMode(true);
4809
+ console.log("\u{1F527} Development mode: using http://localhost:3000\n");
4810
+ }
4811
+ });
4729
4812
  program2.command("init").description("Configure FlightDesk CLI with your API credentials").action(initCommand);
4730
4813
  program2.command("auth").description("Log in to Claude for session monitoring").action(authCommand);
4731
4814
  program2.command("register <project-id> [task-id]").description("Register a Claude Code session with a FlightDesk task").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);