package main import ( "bufio" "context" "database/sql" "fmt" "net/url" "os" "path/filepath" "strconv" "strings" "time" "encoding/json" "github.com/cobr-ai/apex/internal/cv" "github.com/cobr-ai/apex/internal/docx" "github.com/cobr-ai/apex/internal/eval" "github.com/cobr-ai/apex/internal/followup" "github.com/cobr-ai/apex/internal/intake" "github.com/cobr-ai/apex/internal/linkedin" "github.com/cobr-ai/apex/internal/patterns" "github.com/cobr-ai/apex/internal/scan" "github.com/cobr-ai/apex/internal/store" ) // runScan headless: load all enabled adapters, run them, write jobs to DB. func runScan(args []string) int { home, err := os.UserHomeDir() if err != nil { fmt.Fprintf(os.Stderr, "scan: resolve home: %v\n", err) return 1 } dbPath := filepath.Join(home, ".apex", "apex.db") os.MkdirAll(filepath.Dir(dbPath), 0755) sdb, err := store.NewDB(dbPath) if err != nil { fmt.Fprintf(os.Stderr, "scan: db init: %v\n", err) return 1 } defer sdb.Close() db := sdb.Conn() // Load config from repo root root := findRepoRoot() cfgPath := filepath.Join(root, "internal", "scan", "adapters.yaml") cfg, err := scan.LoadConfig(cfgPath) if err != nil { fmt.Fprintf(os.Stderr, "scan: config load: %v\n", err) return 1 } runner := scan.NewRunner(db, cfg) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) defer cancel() for result := range runner.RunAll(ctx, cfg.TrackedCompanies, cfg.Aggregators) { if result.Err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", result.Company, result.Err) } else { fmt.Printf("%s: %d found, %d filtered, %d new (%dms)\n", result.Company, result.Found, result.Filtered, result.New, result.DurationMs) } } return 0 } // runEval headless: load pending jobs, evaluate via Claude, write reports. func runEval(args []string) int { // Parse --limit, --min-score, and --workers flags limit := 0 minScore := 0.0 workers := eval.DefaultConcurrency for i := 0; i < len(args); i++ { switch args[i] { case "--limit": if i+1 < len(args) { v, err := strconv.Atoi(args[i+1]) if err != nil { fmt.Fprintf(os.Stderr, "eval: invalid --limit: %v\n", err) return 2 } limit = v i++ } case "--min-score": if i+1 < len(args) { v, err := strconv.ParseFloat(args[i+1], 64) if err != nil { fmt.Fprintf(os.Stderr, "eval: invalid --min-score: %v\n", err) return 2 } minScore = v i++ } case "--workers": if i+1 < len(args) { v, err := strconv.Atoi(args[i+1]) if err != nil { fmt.Fprintf(os.Stderr, "eval: invalid --workers: %v\n", err) return 2 } if v < 1 { v = 1 } if v > 10 { v = 10 } workers = v i++ } } } // Check Claude CLI availability client := eval.NewClient() if err := client.Available(); err != nil { fmt.Fprintf(os.Stderr, "eval: claude not available: %v\n", err) return 1 } home, err := os.UserHomeDir() if err != nil { fmt.Fprintf(os.Stderr, "eval: resolve home: %v\n", err) return 1 } dbPath := filepath.Join(home, ".apex", "apex.db") sdb, err := store.NewDB(dbPath) if err != nil { fmt.Fprintf(os.Stderr, "eval: db init: %v\n", err) return 1 } defer sdb.Close() db := sdb.Conn() reportsDir := filepath.Join(home, ".apex", "reports") cvPath := filepath.Join(home, ".apex", "cv.md") writer := &eval.ReportWriter{ DB: db, ReportsDir: reportsDir, } // Load pending jobs jobs, err := loadPendingJobs(db, limit) if err != nil { fmt.Fprintf(os.Stderr, "eval: load pending: %v\n", err) return 1 } if len(jobs) == 0 { fmt.Println("eval: no pending jobs") return 0 } batch := &eval.Batch{ Client: client, Writer: writer, DB: db, CVPath: cvPath, Concurrency: workers, MinScore: minScore, } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) defer cancel() // ponytail: ANSI progress display; no color library statsEval := 0 statsSkipped := 0 statsErrors := 0 // Stream events and emit progress display for ev := range batch.Run(ctx, jobs) { switch ev.Kind { case eval.EventStart: fmt.Printf("[ ... ] evaluating: %s — %s\n", ev.Job.Company, ev.Job.Title) case eval.EventDone: statsEval++ fmt.Printf("[ OK ] %.1f/5 — %s — %s\n", ev.Eval.Score, ev.Job.Company, ev.Job.Title) case eval.EventSkipped: statsSkipped++ fmt.Printf("[SKIP ] score below threshold — %s — %s\n", ev.Job.Company, ev.Job.Title) case eval.EventError: statsErrors++ fmt.Printf("[FAIL ] %s — %s: %v\n", ev.Job.Company, ev.Job.Title, ev.Err) } } fmt.Printf("\nEvaluated: %d Skipped: %d Errors: %d\n", statsEval, statsSkipped, statsErrors) return 0 } // loadPendingJobs returns jobs that do not yet have an application row, up // to limit. Mirrors internal/tui/eval_views.go:62 so headless eval and the // TUI eval tab share the same notion of "pending". func loadPendingJobs(db *sql.DB, limit int) ([]eval.JobContext, error) { if limit <= 0 { limit = 10 } rows, err := db.Query(` SELECT j.url, j.company, j.title, COALESCE(j.description, ''), COALESCE(j.source, '') FROM jobs j LEFT JOIN applications a ON a.job_id = j.id WHERE a.id IS NULL AND j.archived = 0 ORDER BY j.discovered_at DESC LIMIT ? `, limit) if err != nil { return nil, err } defer rows.Close() var jobs []eval.JobContext for rows.Next() { var j eval.JobContext if err := rows.Scan(&j.URL, &j.Company, &j.Title, &j.JDText, &j.Source); err != nil { return nil, err } jobs = append(jobs, j) } return jobs, rows.Err() } // withLinkedInClient creates a client, runs fn, and always calls Close. func withLinkedInClient(ctx context.Context, fn func(*linkedin.Client) error) error { cli, err := linkedin.NewClient(ctx) if err != nil { return err } defer cli.Close() return fn(cli) } // validateLinkedInURL rejects non-https or non-linkedin.com URLs to prevent // browser navigation to attacker-controlled destinations. func validateLinkedInURL(raw string) error { u, err := url.Parse(raw) if err != nil { return fmt.Errorf("invalid URL: %w", err) } if u.Scheme != "https" { return fmt.Errorf("URL must use https scheme, got %q", u.Scheme) } host := u.Hostname() if host != "www.linkedin.com" && !strings.HasSuffix(host, ".linkedin.com") { return fmt.Errorf("URL must be on linkedin.com, got %q", host) } return nil } // runOffers compares multiple job offers side-by-side with Claude ranking. func runOffers(args []string) int { if len(args) < 2 { fmt.Fprintln(os.Stderr, "usage: apex offers [url...]") return 2 } home, err := os.UserHomeDir() if err != nil { fmt.Fprintf(os.Stderr, "offers: resolve home: %v\n", err) return 1 } dbPath := filepath.Join(home, ".apex", "apex.db") sdb, err := store.NewDB(dbPath) if err != nil { fmt.Fprintf(os.Stderr, "offers: db init: %v\n", err) return 1 } defer sdb.Close() db := sdb.Conn() type jobOffer struct { Company string Title string Description string Report string } var offers []jobOffer // Load each job and its most recent report for _, jobURL := range args { var jobID int64 err := db.QueryRow(`SELECT id FROM jobs WHERE url = ?`, jobURL).Scan(&jobID) if err == sql.ErrNoRows { fmt.Fprintf(os.Stderr, "warning: no job found for %s — skipping\n", jobURL) continue } if err != nil { fmt.Fprintf(os.Stderr, "warning: query job %s: %v — skipping\n", jobURL, err) continue } var company, title, description, reportMD string err = db.QueryRow(` SELECT j.company, j.title, COALESCE(j.description,'') FROM jobs j WHERE j.id = ? `, jobID).Scan(&company, &title, &description) if err != nil { fmt.Fprintf(os.Stderr, "warning: query job details for %s: %v — skipping\n", jobURL, err) continue } // Query most recent report for this job err = db.QueryRow(` SELECT COALESCE(r.report_md, '') FROM reports r JOIN applications a ON a.id = r.application_id WHERE a.job_id = ? ORDER BY r.id DESC LIMIT 1 `, jobID).Scan(&reportMD) if err == sql.ErrNoRows { fmt.Fprintf(os.Stderr, "warning: no eval report for %s — run 'apex eval' first, skipping\n", jobURL) continue } if err != nil { fmt.Fprintf(os.Stderr, "warning: query report for %s: %v — skipping\n", jobURL, err) continue } if reportMD == "" { fmt.Fprintf(os.Stderr, "warning: no eval report for %s — run 'apex eval' first, skipping\n", jobURL) continue } offers = append(offers, jobOffer{ Company: company, Title: title, Description: description, Report: reportMD, }) } if len(offers) < 2 { fmt.Fprintln(os.Stderr, "need at least 2 evaluated jobs to compare") return 1 } // Build comparison prompt prompt := "Compare these job opportunities side-by-side and provide a ranked recommendation.\n\n" + "For each job, an evaluation report is provided below.\n\n" + "Output format:\n" + "1. Ranked table: Rank | Company | Role | Fit Score | Comp Potential | Culture Risk | Verdict\n" + "2. For each job: one paragraph rationale (3-5 sentences)\n" + "3. Final recommendation: which to prioritize and why\n\n" + "Jobs to compare:\n" for _, offer := range offers { prompt += fmt.Sprintf("## %s — %s\n\n%s\n\n", offer.Company, offer.Title, offer.Report) } // Call Claude client := eval.NewClient() if err := client.Available(); err != nil { fmt.Fprintf(os.Stderr, "offers: claude not available: %v\n", err) return 1 } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() result, err := client.Evaluate(ctx, prompt, nil) if err != nil { fmt.Fprintf(os.Stderr, "offers: %v\n", err) return 1 } fmt.Println(result.Raw) return 0 } // runLinkedIn dispatches apex linkedin subcommands. func runLinkedIn(args []string) int { if len(args) == 0 { fmt.Fprintln(os.Stderr, "usage: apex linkedin ") fmt.Fprintln(os.Stderr, " messages list recruiter message threads") fmt.Fprintln(os.Stderr, " find scroll inbox to a sender, return thread URL") fmt.Fprintln(os.Stderr, " thread read a message thread") fmt.Fprintln(os.Stderr, " reply send a reply (requires confirmation)") fmt.Fprintln(os.Stderr, " invite [--yes] send a connection request with a custom note (≤200 chars)") fmt.Fprintln(os.Stderr, " jobs [--location ] [--remote] search jobs") fmt.Fprintln(os.Stderr, " contacts find recruiters at a company") fmt.Fprintln(os.Stderr, " test test selectors and auth state") fmt.Fprintln(os.Stderr, " probe-thread dump DOM selectors from a thread (use when thread is broken)") fmt.Fprintln(os.Stderr, " login open browser to complete LinkedIn login (requires display)") return 2 } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() sub := args[0] rest := args[1:] var runErr error switch sub { case "test": runErr = withLinkedInClient(ctx, func(cli *linkedin.Client) error { if err := cli.TestSelectors(ctx); err != nil { return err } fmt.Println("OK: selectors pass") return nil }) case "find": if len(rest) == 0 { fmt.Fprintln(os.Stderr, "usage: apex linkedin find ") return 2 } runErr = withLinkedInClient(ctx, func(cli *linkedin.Client) error { url, err := cli.FindThread(ctx, strings.Join(rest, " ")) if err != nil { return err } fmt.Println(url) return nil }) case "probe-buttons": if len(rest) == 0 { fmt.Fprintln(os.Stderr, "usage: apex linkedin probe-buttons ") return 2 } runErr = withLinkedInClient(ctx, func(cli *linkedin.Client) error { info, err := cli.ProbeButtons(ctx, rest[0]) if err != nil { return err } fmt.Println(info) return nil }) case "probe-thread": if len(rest) == 0 { fmt.Fprintln(os.Stderr, "usage: apex linkedin probe-thread ") return 2 } if err := validateLinkedInURL(rest[0]); err != nil { fmt.Fprintf(os.Stderr, "linkedin probe-thread: %v\n", err) return 1 } runErr = withLinkedInClient(ctx, func(cli *linkedin.Client) error { info, err := cli.ProbeThread(ctx, rest[0]) if err != nil { return err } fmt.Println(info) return nil }) case "messages": runErr = withLinkedInClient(ctx, func(cli *linkedin.Client) error { threads, err := cli.ListMessages(ctx) if err != nil { return err } for i, t := range threads { fmt.Printf("%d. %s\n %s\n %s\n\n", i+1, t.Sender, t.Preview, t.URL) } return nil }) case "thread": if len(rest) == 0 { fmt.Fprintln(os.Stderr, "usage: apex linkedin thread ") return 2 } if err := validateLinkedInURL(rest[0]); err != nil { fmt.Fprintf(os.Stderr, "linkedin thread: %v\n", err) return 2 } runErr = withLinkedInClient(ctx, func(cli *linkedin.Client) error { thread, err := cli.ReadThread(ctx, rest[0]) if err != nil { return err } fmt.Printf("Thread with: %s\n\n", thread.Sender) for _, m := range thread.Messages { fmt.Printf("[%s] %s\n%s\n\n", m.Timestamp, m.Author, m.Text) } msgType := linkedin.ClassifyMessage(*thread) missing := linkedin.MissingFields(*thread) fmt.Printf("--- classification: %s", msgTypeLabel(msgType)) if len(missing) > 0 { fmt.Printf(" | missing: %s", strings.Join(missing, ", ")) draft := linkedin.DraftInfoRequest(*thread, missing) fmt.Printf("\n--- suggested reply:\n%s", draft) } fmt.Println() return nil }) case "reply": if len(rest) < 2 { fmt.Fprintln(os.Stderr, "usage: apex linkedin reply ") return 2 } threadURL := rest[0] if err := validateLinkedInURL(threadURL); err != nil { fmt.Fprintf(os.Stderr, "linkedin reply: %v\n", err) return 2 } message := strings.Join(rest[1:], " ") if !promptConfirm(threadURL, message, false) { fmt.Println("cancelled") return 0 } runErr = withLinkedInClient(ctx, func(cli *linkedin.Client) error { if err := cli.Reply(ctx, threadURL, message); err != nil { return err } fmt.Println("sent") return nil }) case "invite": if len(rest) < 2 { fmt.Fprintln(os.Stderr, "usage: apex linkedin invite [--yes]") return 2 } profileURL := rest[0] if err := validateLinkedInURL(profileURL); err != nil { fmt.Fprintf(os.Stderr, "linkedin invite: %v\n", err) return 2 } note := strings.Join(rest[1:], " ") // Strip --yes flag if present note = strings.TrimSpace(strings.TrimSuffix(note, "--yes")) if len(note) > 200 { fmt.Fprintf(os.Stderr, "linkedin invite: note exceeds 200 chars (got %d)\n", len(note)) return 1 } // Check for --yes flag skipConfirm := false for _, arg := range rest[1:] { if arg == "--yes" { skipConfirm = true break } } if !promptConfirm(profileURL, note, skipConfirm) { fmt.Println("cancelled") return 0 } runErr = withLinkedInClient(ctx, func(cli *linkedin.Client) error { if err := cli.SendInvite(ctx, profileURL, note); err != nil { return err } fmt.Println("sent") return nil }) case "jobs": if len(rest) == 0 { fmt.Fprintln(os.Stderr, "usage: apex linkedin jobs [--location ] [--remote]") return 2 } var location string remoteOnly := false var keywords []string for i := 0; i < len(rest); i++ { switch rest[i] { case "--location": if i+1 < len(rest) { location = rest[i+1] i++ } case "--remote": remoteOnly = true default: keywords = append(keywords, rest[i]) } } runErr = withLinkedInClient(ctx, func(cli *linkedin.Client) error { results, err := cli.SearchJobs(ctx, strings.Join(keywords, " "), location, remoteOnly) if err != nil { return err } for i, j := range results { fmt.Printf("%d. %s @ %s\n %s | %s\n\n", i+1, j.Title, j.Company, j.Location, j.URL) } return nil }) case "contacts": if len(rest) == 0 { fmt.Fprintln(os.Stderr, "usage: apex linkedin contacts ") return 2 } runErr = withLinkedInClient(ctx, func(cli *linkedin.Client) error { contacts, err := cli.FindContacts(ctx, strings.Join(rest, " ")) if err != nil { return err } for i, c := range contacts { fmt.Printf("%d. %s — %s\n %s\n\n", i+1, c.Name, c.Title, c.ProfileURL) } return nil }) case "login": runErr = withLinkedInClient(ctx, func(cli *linkedin.Client) error { if err := cli.Login(ctx); err != nil { return err } fmt.Println("session established — headless commands will work now") return nil }) default: fmt.Fprintf(os.Stderr, "unknown subcommand: %s\n", sub) return 2 } if runErr != nil { fmt.Fprintf(os.Stderr, "linkedin %s: %v\n", sub, runErr) return 1 } return 0 } func msgTypeLabel(t linkedin.MessageType) string { switch t { case linkedin.MsgTypeVague: return "vague" case linkedin.MsgTypeSpecific: return "specific" case linkedin.MsgTypeFollowUp: return "follow-up" case linkedin.MsgTypeSpam: return "spam" default: return "unknown" } } // findRepoRoot walks up from cwd looking for internal/scan/adapters.yaml. func findRepoRoot() string { wd, err := os.Getwd() if err != nil { return "." } d := wd for i := 0; i < 6; i++ { if _, err := os.Stat(filepath.Join(d, "internal", "scan", "adapters.yaml")); err == nil { return d } parent := filepath.Dir(d) if parent == d { break } d = parent } return "." } // runCV generates a tailored HTML CV + PDF for a job URL, or a generic CV // if no --job-url is given. func runCV(args []string) int { var jobURL string for i := 0; i < len(args); i++ { if args[i] == "--job-url" && i+1 < len(args) { jobURL = args[i+1] i++ } } home, herr := os.UserHomeDir() if herr != nil { fmt.Fprintf(os.Stderr, "cv: resolve home: %v\n", herr) return 1 } dbPath := filepath.Join(home, ".apex", "apex.db") sdb, err := store.NewDB(dbPath) if err != nil { fmt.Fprintf(os.Stderr, "cv: db init: %v\n", err) return 1 } defer sdb.Close() exporter := &cv.HTMLExporter{ DB: sdb.Conn(), CVPath: filepath.Join(home, ".apex", "cv.md"), OutputDir: filepath.Join(home, ".apex", "cv"), } ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) defer cancel() result, err := exporter.Generate(ctx, jobURL) if err != nil { fmt.Fprintf(os.Stderr, "cv: %v\n", err) return 1 } fmt.Printf("HTML: %s\n", result.HTMLPath) if result.PDFPath != "" { fmt.Printf("PDF: %s\n", result.PDFPath) } return 0 } // runPatterns prints application pattern analysis. func runPatterns(args []string) int { home, err := os.UserHomeDir() if err != nil { fmt.Fprintf(os.Stderr, "patterns: resolve home: %v\n", err) return 1 } dbPath := filepath.Join(home, ".apex", "apex.db") sdb, err := store.NewDB(dbPath) if err != nil { fmt.Fprintf(os.Stderr, "patterns: db init: %v\n", err) return 1 } defer sdb.Close() stats, err := patterns.Analyze(sdb.Conn()) if err != nil { fmt.Fprintf(os.Stderr, "patterns: %v\n", err) return 1 } fmt.Print(patterns.Format(stats)) return 0 } // runFollowup prints pending follow-ups based on status cadence. func runFollowup(args []string) int { home, err := os.UserHomeDir() if err != nil { fmt.Fprintf(os.Stderr, "followup: resolve home: %v\n", err) return 1 } dbPath := filepath.Join(home, ".apex", "apex.db") sdb, err := store.NewDB(dbPath) if err != nil { fmt.Fprintf(os.Stderr, "followup: db init: %v\n", err) return 1 } defer sdb.Close() items, err := followup.Pending(sdb.Conn()) if err != nil { fmt.Fprintf(os.Stderr, "followup: %v\n", err) return 1 } fmt.Print(followup.Format(items)) return 0 } // runStories prints STAR stories extracted from recent evaluation reports. func runStories(args []string) int { limit := 20 for i := 0; i < len(args); i++ { if args[i] == "--limit" && i+1 < len(args) { if v, err := strconv.Atoi(args[i+1]); err == nil { limit = v i++ } } } home, err := os.UserHomeDir() if err != nil { fmt.Fprintf(os.Stderr, "stories: resolve home: %v\n", err) return 1 } dbPath := filepath.Join(home, ".apex", "apex.db") sdb, err := store.NewDB(dbPath) if err != nil { fmt.Fprintf(os.Stderr, "stories: db init: %v\n", err) return 1 } defer sdb.Close() db := sdb.Conn() rows, err := db.Query(` SELECT j.company, j.title, r.stories FROM reports r JOIN applications a ON a.id = r.application_id JOIN jobs j ON j.id = a.job_id WHERE r.stories IS NOT NULL AND r.stories != '' AND r.stories != '[]' ORDER BY r.generated_at DESC LIMIT ? `, limit) if err != nil { fmt.Fprintf(os.Stderr, "stories: query: %v\n", err) return 1 } defer rows.Close() type story struct { Title string `json:"title"` Situation string `json:"situation"` Action string `json:"action"` Result string `json:"result"` } found := 0 for rows.Next() { var company, title, storiesJSON string if err := rows.Scan(&company, &title, &storiesJSON); err != nil { continue } var stories []story if err := json.Unmarshal([]byte(storiesJSON), &stories); err != nil || len(stories) == 0 { continue } fmt.Printf("=== %s — %s ===\n\n", company, title) for i, s := range stories { fmt.Printf("Story %d: %s\n", i+1, s.Title) if s.Situation != "" { fmt.Printf(" Situation: %s\n", s.Situation) } if s.Action != "" { fmt.Printf(" Action: %s\n", s.Action) } if s.Result != "" { fmt.Printf(" Result: %s\n", s.Result) } fmt.Println() } found++ } if found == 0 { fmt.Println("No stories found. Run eval on some jobs first.") } return 0 } // runPipelineAdd queues a job URL for batch evaluation. func runPipelineAdd(args []string) int { if len(args) == 0 { fmt.Fprintln(os.Stderr, "usage: apex pipeline add ") return 2 } jobURL := args[0] home, err := os.UserHomeDir() if err != nil { fmt.Fprintf(os.Stderr, "pipeline add: %v\n", err) return 1 } sdb, err := store.NewDB(filepath.Join(home, ".apex", "apex.db")) if err != nil { fmt.Fprintf(os.Stderr, "pipeline add: db: %v\n", err) return 1 } defer sdb.Close() db := sdb.Conn() // Upsert job row (minimal — URL only; company/title will be filled on eval). _, err = db.Exec(` INSERT INTO jobs (url, company, title, source) VALUES (?, '', '', 'manual') ON CONFLICT(url) DO NOTHING `, jobURL) if err != nil { fmt.Fprintf(os.Stderr, "pipeline add: upsert job: %v\n", err) return 1 } var jobID int64 if err := db.QueryRow(`SELECT id FROM jobs WHERE url = ?`, jobURL).Scan(&jobID); err != nil { fmt.Fprintf(os.Stderr, "pipeline add: lookup job: %v\n", err) return 1 } // Create application row at Queued status if not already tracked. _, err = db.Exec(` INSERT INTO applications (job_id, status) VALUES (?, 'Queued') ON CONFLICT(job_id) DO NOTHING `, jobID) if err != nil { fmt.Fprintf(os.Stderr, "pipeline add: upsert application: %v\n", err) return 1 } // Insert into pipeline table. _, err = db.Exec(`INSERT INTO pipeline (job_id) VALUES (?)`, jobID) if err != nil { fmt.Fprintf(os.Stderr, "pipeline add: insert pipeline: %v\n", err) return 1 } fmt.Printf("Queued: %s\n", jobURL) return 0 } // runPipelineList prints all jobs currently in the pipeline queue. func runPipelineList(args []string) int { home, err := os.UserHomeDir() if err != nil { fmt.Fprintf(os.Stderr, "pipeline list: %v\n", err) return 1 } sdb, err := store.NewDB(filepath.Join(home, ".apex", "apex.db")) if err != nil { fmt.Fprintf(os.Stderr, "pipeline list: db: %v\n", err) return 1 } defer sdb.Close() db := sdb.Conn() rows, err := db.Query(` SELECT p.id, COALESCE(j.company,''), COALESCE(j.title,''), j.url, p.queued_at, p.completed_at FROM pipeline p JOIN jobs j ON p.job_id = j.id ORDER BY p.queued_at `) if err != nil { fmt.Fprintf(os.Stderr, "pipeline list: query: %v\n", err) return 1 } defer rows.Close() fmt.Printf("%-4s %-20s %-30s %-10s %s\n", "ID", "Company", "Title", "Queued", "Status") fmt.Println(strings.Repeat("-", 80)) count := 0 for rows.Next() { var id int64 var company, title, jobURL string var queuedAt time.Time var completedAt sql.NullTime if err := rows.Scan(&id, &company, &title, &jobURL, &queuedAt, &completedAt); err != nil { continue } status := "pending" if completedAt.Valid { status = "done" } if company == "" { company = "(manual)" } if title == "" { title = jobURL } fmt.Printf("%-4d %-20s %-30s %-10s %s\n", id, truncDisplay(company, 20), truncDisplay(title, 30), queuedAt.Format("2006-01-02"), status) count++ } if count == 0 { fmt.Println("Pipeline is empty. Use: apex pipeline add ") } return 0 } // runPipelineRun evaluates all pending pipeline jobs via eval.Batch. func runPipelineRun(args []string) int { minScore := 0.0 for i := 0; i < len(args)-1; i++ { if args[i] == "--min-score" { v, err := strconv.ParseFloat(args[i+1], 64) if err == nil { minScore = v } } } client := eval.NewClient() if err := client.Available(); err != nil { fmt.Fprintf(os.Stderr, "pipeline run: claude not available: %v\n", err) return 1 } home, err := os.UserHomeDir() if err != nil { fmt.Fprintf(os.Stderr, "pipeline run: %v\n", err) return 1 } sdb, err := store.NewDB(filepath.Join(home, ".apex", "apex.db")) if err != nil { fmt.Fprintf(os.Stderr, "pipeline run: db: %v\n", err) return 1 } defer sdb.Close() db := sdb.Conn() // Load pending pipeline jobs. rows, err := db.Query(` SELECT p.id, j.url, COALESCE(j.company,''), COALESCE(j.title,''), COALESCE(j.description,''), COALESCE(j.source,'manual') FROM pipeline p JOIN jobs j ON p.job_id = j.id WHERE p.completed_at IS NULL ORDER BY p.queued_at `) if err != nil { fmt.Fprintf(os.Stderr, "pipeline run: query: %v\n", err) return 1 } defer rows.Close() type pipelineRow struct { pipelineID int64 job eval.JobContext } var pending []pipelineRow for rows.Next() { var pr pipelineRow if err := rows.Scan(&pr.pipelineID, &pr.job.URL, &pr.job.Company, &pr.job.Title, &pr.job.JDText, &pr.job.Source); err != nil { continue } pending = append(pending, pr) } if len(pending) == 0 { fmt.Println("No pending pipeline jobs.") return 0 } fmt.Printf("Running pipeline: %d job(s)\n", len(pending)) jobs := make([]eval.JobContext, len(pending)) for i, pr := range pending { jobs[i] = pr.job } batch := &eval.Batch{ Client: client, Writer: &eval.ReportWriter{DB: db, ReportsDir: filepath.Join(home, ".apex", "reports")}, DB: db, CVPath: filepath.Join(home, ".apex", "cv.md"), Concurrency: eval.DefaultConcurrency, MinScore: minScore, } // Build URL→pipelineID map for completion tracking. pipelineIDs := make(map[string]int64, len(pending)) for _, pr := range pending { pipelineIDs[pr.job.URL] = pr.pipelineID } ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute) defer cancel() for ev := range batch.Run(ctx, jobs) { pid := pipelineIDs[ev.Job.URL] switch ev.Kind { case eval.EventStart: fmt.Printf("START\t%s\t%s\n", ev.Job.Company, ev.Job.Title) if pid != 0 { _, _ = db.Exec(`UPDATE pipeline SET started_at = CURRENT_TIMESTAMP WHERE id = ? AND started_at IS NULL`, pid) } case eval.EventDone: fmt.Printf("DONE\t%s\t%.1f\t%s\n", ev.Job.Company, ev.Eval.Score, ev.Saved.FilePath) if pid != 0 { _, _ = db.Exec(`UPDATE pipeline SET completed_at = CURRENT_TIMESTAMP WHERE id = ?`, pid) } case eval.EventSkipped: fmt.Printf("SKIP\t%s\t%.1f (below min-score)\n", ev.Job.Company, ev.Eval.Score) if pid != 0 { _, _ = db.Exec(`UPDATE pipeline SET completed_at = CURRENT_TIMESTAMP WHERE id = ?`, pid) } case eval.EventError: fmt.Printf("ERR\t%s\t%v\n", ev.Job.Company, ev.Err) } } return 0 } // runApply marks a job as Applied after interactive user confirmation. // Never auto-submits — always requires explicit 'y' from the user. func runApply(args []string) int { if len(args) == 0 { fmt.Fprintln(os.Stderr, "usage: apex apply ") return 2 } jobURL := args[0] home, err := os.UserHomeDir() if err != nil { fmt.Fprintf(os.Stderr, "apply: %v\n", err) return 1 } sdb, err := store.NewDB(filepath.Join(home, ".apex", "apex.db")) if err != nil { fmt.Fprintf(os.Stderr, "apply: db: %v\n", err) return 1 } defer sdb.Close() db := sdb.Conn() var company, title, status string var score sql.NullFloat64 err = db.QueryRow(` SELECT j.company, j.title, a.status, a.score FROM jobs j LEFT JOIN applications a ON a.job_id = j.id WHERE j.url = ? `, jobURL).Scan(&company, &title, &status, &score) if err == sql.ErrNoRows { fmt.Fprintf(os.Stderr, "apply: job not found: %s\n", jobURL) return 1 } if err != nil { fmt.Fprintf(os.Stderr, "apply: query: %v\n", err) return 1 } scoreStr := "unscored" if score.Valid { scoreStr = fmt.Sprintf("%.1f/5", score.Float64) } fmt.Printf("%s — %s [%s | score: %s]\n", company, title, status, scoreStr) fmt.Fprint(os.Stderr, "Mark as Applied? [y/N]: ") scanner := bufio.NewScanner(os.Stdin) if !scanner.Scan() || strings.ToLower(strings.TrimSpace(scanner.Text())) != "y" { fmt.Println("Cancelled.") return 0 } _, err = db.Exec(` UPDATE applications SET status = 'Applied', updated_at = CURRENT_TIMESTAMP WHERE job_id = (SELECT id FROM jobs WHERE url = ?) `, jobURL) if err != nil { fmt.Fprintf(os.Stderr, "apply: update: %v\n", err) return 1 } fmt.Printf("Marked as Applied: %s — %s\n", company, title) return 0 } // runCompare loads a job eval and provides gap analysis against senior benchmarks. // runEvalMode is the shared implementation for all focused eval modes. // It resolves the job, loads its stored report, builds a mode-specific prompt, // and streams the Claude response to stdout. func runEvalMode(name, promptTemplate string, args []string) int { jobURL, err := resolveJobURL(args) if err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) return 1 } home, err := os.UserHomeDir() if err != nil { fmt.Fprintf(os.Stderr, "%s: resolve home: %v\n", name, err) return 1 } sdb, err := store.NewDB(filepath.Join(home, ".apex", "apex.db")) if err != nil { fmt.Fprintf(os.Stderr, "%s: db init: %v\n", name, err) return 1 } defer sdb.Close() job, report, err := loadJobAndReport(sdb.Conn(), jobURL) if err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) return 1 } client := eval.NewClient() if err := client.Available(); err != nil { fmt.Fprintf(os.Stderr, "%s: claude not available: %v\n", name, err) return 1 } prompt := fmt.Sprintf("%s\n\nJob: %s at %s\n%s", promptTemplate, job.Title, job.Company, report) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() result, err := client.Evaluate(ctx, prompt, nil) if err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) return 1 } fmt.Println(result.Raw) return 0 } func runCompare(args []string) int { return runEvalMode("compare", "Given this job evaluation, compare it against a senior security engineer benchmark. What gaps exist? What strengths align? Format: Gap Analysis, Strength Map, Net Assessment.", args) } func runDeep(args []string) int { return runEvalMode("deep", "Perform a deep dive on this job: company financial health signals, team culture indicators from job language, red flags, growth trajectory. Be specific and cite evidence from the JD.", args) } func runTraining(args []string) int { return runEvalMode("training", "From this job evaluation, extract a personal development roadmap. List: skills to acquire (with timelines), certifications worth pursuing, projects to build, and learning resources.", args) } func runProject(args []string) int { return runEvalMode("project", "Design 2-3 portfolio projects that would directly address gaps in this job evaluation. For each: title, 1-sentence pitch, tech stack, estimated build time, and why it targets this role.", args) } func runNegotiate(args []string) int { return runEvalMode("negotiate", "Using this job evaluation score and analysis, build a salary negotiation strategy. Include: target range reasoning, leverage points, BATNA, timing recommendations, and exact opening line.", args) } func runContacto(args []string) int { return runEvalMode("contacto", "Draft 3 LinkedIn outreach messages to people at this company: one to a hiring manager, one to a peer engineer, one to an internal recruiter. Keep each under 150 words. Be specific to the role.", args) } // resolveJobURL returns the job URL from args, or queries the most recent job if empty. func resolveJobURL(args []string) (string, error) { if len(args) > 0 && args[0] != "" { return args[0], nil } home, err := os.UserHomeDir() if err != nil { return "", err } dbPath := filepath.Join(home, ".apex", "apex.db") sdb, err := store.NewDB(dbPath) if err != nil { return "", err } defer sdb.Close() db := sdb.Conn() var url string err = db.QueryRow(`SELECT url FROM jobs ORDER BY id DESC LIMIT 1`).Scan(&url) if err != nil { return "", fmt.Errorf("no recent job found") } return url, nil } // loadJobAndReport queries a job by URL and reads its most recent report markdown. func loadJobAndReport(db *sql.DB, jobURL string) (eval.JobContext, string, error) { var job eval.JobContext err := db.QueryRow(` SELECT url, company, title, COALESCE(location, ''), COALESCE(source, ''), COALESCE(description, '') FROM jobs WHERE url = ? `, jobURL).Scan(&job.URL, &job.Company, &job.Title, &job.Location, &job.Source, &job.JDText) if err != nil { if err == sql.ErrNoRows { return eval.JobContext{}, "", fmt.Errorf("job not found: %s", jobURL) } return eval.JobContext{}, "", fmt.Errorf("query job: %w", err) } var jobID int64 err = db.QueryRow(`SELECT id FROM jobs WHERE url = ?`, jobURL).Scan(&jobID) if err != nil { return eval.JobContext{}, "", fmt.Errorf("lookup job id: %w", err) } // Query most recent report for this job var filePath string err = db.QueryRow(` SELECT r.file_path FROM reports r JOIN applications a ON a.id = r.application_id WHERE a.job_id = ? ORDER BY r.id DESC LIMIT 1 `, jobID).Scan(&filePath) if err != nil { if err == sql.ErrNoRows { return job, "", fmt.Errorf("no evaluation report found for job") } return eval.JobContext{}, "", fmt.Errorf("query report: %w", err) } reportBytes, err := os.ReadFile(filePath) if err != nil { return eval.JobContext{}, "", fmt.Errorf("read report: %w", err) } return job, string(reportBytes), nil } // runIngest parses a DOCX resume file and generates a cv.md with writing style extraction. func runIngest(args []string) int { if len(args) != 1 { fmt.Fprintln(os.Stderr, "usage: apex ingest ") return 2 } filePath := args[0] // Validate extension is .docx if !strings.HasSuffix(strings.ToLower(filePath), ".docx") { fmt.Fprintf(os.Stderr, "ingest: file must have .docx extension, got %q\n", filePath) return 1 } // Parse DOCX md, styleProfile, err := docx.Ingest(filePath) if err != nil { fmt.Fprintf(os.Stderr, "ingest: %v\n", err) return 1 } // Determine cv.md path home, err := os.UserHomeDir() if err != nil { fmt.Fprintf(os.Stderr, "ingest: resolve home: %v\n", err) return 1 } cvPath := filepath.Join(home, ".apex", "cv.md") // Check if cv.md already exists if _, err := os.Stat(cvPath); err == nil { // File exists, ask for confirmation fmt.Printf("cv.md already exists at %s. Overwrite? [y/N]: ", cvPath) scanner := bufio.NewScanner(os.Stdin) if !scanner.Scan() || strings.ToLower(strings.TrimSpace(scanner.Text())) != "y" { fmt.Println("aborted") return 0 } } // Check Claude CLI availability client := eval.NewClient() if err := client.Available(); err != nil { fmt.Fprintf(os.Stderr, "ingest: claude not available: %v\n", err) return 1 } // Build Claude prompt actionVerbsStr := "" if len(styleProfile.ActionVerbs) > 0 { verbs := styleProfile.ActionVerbs if len(verbs) > 10 { verbs = verbs[:10] } actionVerbsStr = strings.Join(verbs, ", ") } prompt := fmt.Sprintf(`Given this resume in plain text, produce a clean, well-structured CV in Markdown format. Preserve all dates, company names, job titles, and metrics exactly as written. Use ## headings for sections, ### for role titles, bullet points for achievements. Style patterns detected: Action verbs: %s Summary style: %s Resume text: %s`, actionVerbsStr, styleProfile.SummaryStyle, md) // Call Claude ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() result, err := client.Evaluate(ctx, prompt, nil) if err != nil { fmt.Fprintf(os.Stderr, "ingest: claude: %v\n", err) return 1 } // Create directory if needed if err := os.MkdirAll(filepath.Dir(cvPath), 0755); err != nil { fmt.Fprintf(os.Stderr, "ingest: mkdir: %v\n", err) return 1 } // Write CV if err := os.WriteFile(cvPath, []byte(result.Raw), 0600); err != nil { fmt.Fprintf(os.Stderr, "ingest: write cv: %v\n", err) return 1 } fmt.Printf("CV written to %s\n", cvPath) if len(styleProfile.ActionVerbs) > 0 { topVerbs := styleProfile.ActionVerbs if len(topVerbs) > 5 { topVerbs = topVerbs[:5] } fmt.Printf("Detected writing style: %s\n", strings.Join(topVerbs, ", ")) } return 0 } func truncDisplay(s string, max int) string { if len(s) <= max { return s } return s[:max-1] + "…" } // rawWrapper wraps the raw markdown string with a GetRaw() method type rawWrapper struct { raw string } func (w rawWrapper) GetRaw() string { return w.raw } // evalClientAdapter wraps eval.Client to implement intake.EvalClient type evalClientAdapter struct { client *eval.Client } func (a *evalClientAdapter) Evaluate(ctx context.Context, prompt string, onChunk interface{}) (intake.EvalResult, error) { result, err := a.client.Evaluate(ctx, prompt, nil) if err != nil { return nil, err } return rawWrapper{raw: result.Raw}, nil } // runIntake launches an interactive 8-phase intake interview func runIntake(args []string) int { // Parse flags synthesizeOnly := false for _, arg := range args { if arg == "--synthesize" { synthesizeOnly = true } } home, err := os.UserHomeDir() if err != nil { fmt.Fprintf(os.Stderr, "intake: resolve home: %v\n", err) return 1 } dbPath := filepath.Join(home, ".apex", "apex.db") os.MkdirAll(filepath.Dir(dbPath), 0755) sdb, err := store.NewDB(dbPath) if err != nil { fmt.Fprintf(os.Stderr, "intake: db init: %v\n", err) return 1 } defer sdb.Close() db := sdb.Conn() manager := intake.NewManager(db) session, err := manager.GetCurrentSession() if err != nil { fmt.Fprintf(os.Stderr, "intake: load session: %v\n", err) return 1 } // If --synthesize flag is set, skip to synthesis if synthesizeOnly { if session.IsComplete { fmt.Println("Intake already complete. Regenerating cv.md and profile.yml...") } client := eval.NewClient() if err := client.Available(); err != nil { fmt.Fprintf(os.Stderr, "intake: claude not available: %v\n", err) return 1 } sess := intake.NewSession(db) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() adapter := &evalClientAdapter{client: client} if err := sess.Synthesize(ctx, session, adapter); err != nil { fmt.Fprintf(os.Stderr, "intake: synthesize failed: %v\n", err) fmt.Fprintln(os.Stderr, "Run `apex intake --synthesize` to retry without re-answering.") return 1 } fmt.Printf("CV written to %s/.apex/cv.md\n", home) fmt.Printf("Profile written to %s/.apex/config/profile.yml\n", home) return 0 } // If already complete, offer to regenerate if session.IsComplete { fmt.Println("Intake already complete. Use --synthesize to regenerate cv.md and profile.yml.") return 0 } // Interactive flow phaseQuestions := map[intake.Phase]string{ intake.PhaseIdentity: "Full name, email, location (city/country), LinkedIn URL, timezone", intake.PhaseEducation: "Degrees, institutions, graduation years, relevant coursework", intake.PhaseRoles: "For each job: company, title, dates, key achievements with metrics", intake.PhaseCertifications: "Certifications, issuing body, year obtained", intake.PhaseSkills: "Technical skills, tools, languages, frameworks", intake.PhaseProjects: "Portfolio projects: name, description, tech stack, impact", intake.PhasePreferences: "Target roles, salary range (min/max), preferred locations, remote preference", intake.PhaseVoice: "Describe your professional philosophy or what makes your work distinctive", } reader := bufio.NewReader(os.Stdin) for session.CurrentPhase <= intake.PhaseVoice { currentPhase := session.CurrentPhase question, ok := phaseQuestions[currentPhase] if !ok { fmt.Fprintf(os.Stderr, "intake: unknown phase %d\n", currentPhase) return 1 } fmt.Printf("\n[Phase %d of 8] %s\n", int(currentPhase)+1, currentPhase.String()) fmt.Printf("Question: %s\n", question) fmt.Println("(Enter your response, then a blank line to confirm)") // Read multiline input var lines []string for { line, err := reader.ReadString('\n') if err != nil && err.Error() != "EOF" { fmt.Fprintf(os.Stderr, "intake: read input: %v\n", err) return 1 } line = strings.TrimSuffix(line, "\n") if line == "" { break } lines = append(lines, line) if err != nil && err.Error() == "EOF" { break } } if len(lines) == 0 { fmt.Println("Skipping this phase (no input).") // Still advance to next phase manager.AdvanceToNextPhase(session) continue } // For now, store as a single fields entry with key "response" answer := intake.FormAnswer{ Phase: currentPhase, Fields: map[string]string{"response": strings.Join(lines, "\n")}, SubmittedAt: time.Now(), } if err := manager.SubmitPhaseAnswer(session, answer); err != nil { fmt.Fprintf(os.Stderr, "intake: save answer: %v\n", err) return 1 } // Advance to next phase manager.AdvanceToNextPhase(session) } // Mark complete and synthesize fmt.Println("\nSynthesizing CV and profile via Claude...") if err := manager.CompleteIntake(session); err != nil { fmt.Fprintf(os.Stderr, "intake: mark complete: %v\n", err) return 1 } client := eval.NewClient() if err := client.Available(); err != nil { fmt.Fprintf(os.Stderr, "intake: claude not available: %v\n", err) return 1 } sess := intake.NewSession(db) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() // Reload session to ensure we have all answers session, err = manager.GetCurrentSession() if err != nil { fmt.Fprintf(os.Stderr, "intake: reload session: %v\n", err) return 1 } adapter := &evalClientAdapter{client: client} if err := sess.Synthesize(ctx, session, adapter); err != nil { fmt.Fprintf(os.Stderr, "intake: synthesize failed: %v\n", err) fmt.Fprintln(os.Stderr, "Run `apex intake --synthesize` to retry without re-answering.") return 1 } fmt.Printf("CV written to %s/.apex/cv.md\n", home) fmt.Printf("Profile written to %s/.apex/config/profile.yml\n", home) return 0 } // promptConfirm prints a confirmation prompt and returns true only if the user enters 'y' (case-insensitive). // If yesFlag is true, returns true silently without prompting. func promptConfirm(target, body string, yesFlag bool) bool { if yesFlag { return true } fmt.Printf("Send to %s:\n\n%s\n\nConfirm? [y/N] ", target, body) var confirm string fmt.Scanln(&confirm) return strings.ToLower(strings.TrimSpace(confirm)) == "y" }