perchai-cli 2.4.47 → 2.4.49

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 (2) hide show
  1. package/dist/perch.mjs +1422 -673
  2. package/package.json +1 -1
package/dist/perch.mjs CHANGED
@@ -83484,6 +83484,8 @@ Browser Operator skill contract:
83484
83484
  - Prefer browser_execute_task for short multi-step jobs: include the goal, requiredValues, successCriteria, allowSharingChange only when the user explicitly approved access changes, and a bounded steps array.
83485
83485
  - Use semantic browser tools for ordinary controls with text, role, or aria labels.
83486
83486
  - Use browser_visual_click or browser_focus_type for canvas, iframe, document-body, drag/drop, or targets that are visible but not exposed as DOM elements.
83487
+ - When a semantic click reports no DOM match, the runner auto-escalates to a coordinate/visual (CUA) click on the same target before failing. Do not re-issue the identical semantic click; if it still fails, escalate yourself with browser_visual_click on the target you can see, then report a blocker only if the coordinate click also fails.
83488
+ - For targets behind a menu, dropdown, or kebab/overflow (e.g. Reddit "Mod Tools", a "More" menu): observe \u2192 click the parent menu/trigger to open it \u2192 observe \u2192 then click the now-visible item. Do not click a hidden menu item directly; open its parent first.
83487
83489
  - If coordinate tools are unavailable and the target is only visually reachable, do not loop empty semantic clicks; report the missing visual-action capability.
83488
83490
  - For Google Docs: never inspect, open, or change the Viewing / Editing / Suggesting mode dropdown during normal writing or recovery. New Docs should already start editable. If the visible toolbar is already read-only, Viewing, or Suggesting, stop with a permission/mode blocker instead of trying to switch modes yourself. Collapse Document tabs/outlines if they cover the page, click/type into the visible paper area, and verify with URL/title/save status/screenshot. Do not trust DOM body readback as required proof because Kix can be visually written while DOM snapshots stay blank.
83489
83491
  - Never claim completion from intent alone; require visible proof, save/sent status, URL, receipt, or screenshot evidence.
@@ -83589,17 +83591,20 @@ Rules:
83589
83591
  name: "Browser Operator",
83590
83592
  description: "Operates a logged-in browser turn-by-turn for Quill delivery and web tasks.",
83591
83593
  // Browser work runs on the selected main model. Native vision models see
83592
- // screenshots directly; text-only models can call the plain vision observer
83593
- // with visionInspect({ currentScreen: true }) when visual confirmation matters.
83594
+ // screenshots directly; for text-only models the loop automatically runs each
83595
+ // post-action screenshot through the vision sidecar and injects the readout,
83596
+ // so the operator is always sighted. visionInspect({ currentScreen: true })
83597
+ // remains available for targeted follow-up questions.
83594
83598
  lane: "chat",
83595
83599
  systemPrompt: `You are Quill's Browser Operator. You operate the user's logged-in browser like a careful human.
83596
83600
 
83597
83601
  Rules:
83598
83602
  - Drive the browser by looking and acting on what you see by name. Never use remembered IDs, refs, CSS selectors, or raw Playwright tools.
83599
83603
  - Before tool calls, send short live preambles so the user sees steady progress: 1-2 friendly sentences, usually 8-12 words. Group related actions. Examples: "I've created the doc; now opening it to verify the title rendered." / "Email's filled; sending and watching for the confirmation." Final summary stays concise, 10 lines or fewer.
83600
- - Use browser_execute_task for short verified sequences, or browser_navigate, browser_observe, browser_click, browser_type, browser_visual_click, browser_focus_type, browser_press_key, browser_read, browser_wait, and browser_handle_dialog for one-off recovery steps. For browser_click/browser_type, pass a semantic target with text, role, ariaLabel, and optional nth. For browser_visual_click/browser_focus_type, pass screenshot-relative x/y coordinates, usually 0-1 viewport fractions.
83604
+ - Use browser_execute_task for short verified sequences, or browser_navigate, browser_observe, browser_click, browser_type, browser_visual_click, browser_focus_type, browser_press_key, browser_read, browser_wait, and browser_handle_dialog for one-off recovery steps. For browser_click/browser_type, pass a semantic target with text, role, ariaLabel, and optional nth. For browser_visual_click/browser_focus_type, ALWAYS pass a label naming the target \u2014 the runner grounds that label against the live DOM (including shadow-DOM web components) and clicks the real element; the x/y you pass is only a last-resort fallback for true canvas/non-DOM targets (e.g. the Google Docs paper). Prefer naming the target over guessing coordinates.
83605
+ - To fill a field on a web-component/shadow-DOM site (e.g. Reddit's title/body): browser_visual_click the field to focus it, then browser_type the text \u2014 typing goes into the focused field, so you do not need a semantic target. If a type reports the field is still empty, visual_click the field again to focus it, then retype; do not repeat a type into an unfocused field.
83601
83606
  - One action, then look at the result, then choose the next action. If a click does nothing, observe again and try a different visible target; do not repeat the same failed click more than once.
83602
- - After each action, inspect the fresh browser result before deciding the next action. If you can see an attached screenshot, use it as the source of truth. If you cannot see images and visual details matter, call visionInspect with {"currentScreen": true, "prompt": "what to verify"}.
83607
+ - After each action, inspect the fresh browser result before deciding the next action. If you can see an attached screenshot, use it as the source of truth. If the result instead carries a "Vision readout of the current screen" message, a vision sidecar already looked for you \u2014 trust it as the source of truth and use the x/y viewport fractions it names for any browser_visual_click. Only call visionInspect with {"currentScreen": true, "prompt": "..."} when you need a more specific visual question answered.
83603
83608
  - Safety: never click anything that grants access or changes sharing/permissions, such as "Share & send", "Share", or "Grant access". If a share/permission dialog appears, choose the option that sends without changing permissions, such as "Send without sharing" or "Send anyway".
83604
83609
  - Do not claim completion unless the browser shows a confirmation, URL, saved state, sent state, or other visible receipt.
83605
83610
 
@@ -86234,7 +86239,8 @@ var init_desktopContextSection = __esm({
86234
86239
  BROWSER_EXECUTION_GUIDANCE = `## Browser execution \u2014 logged-in web surface
86235
86240
  Use the embedded browser as the logged-in execution surface for web apps, account-bound pages, SPAs, and recovery paths. Google delivery uses verified shortcuts first; non-Google apps and pages without a verified surface tool are browser-first.
86236
86241
  Prefer browser_execute_task for short multi-step browser jobs because it carries the goal, requiredValues, successCriteria, sharing/mode flags, and enforced policy in one guarded runner. Use browser_navigate, browser_observe, browser_click, browser_type, browser_visual_click, browser_focus_type, browser_press_key, and browser_wait for single-step recovery. You already have these tools here; do not spawn browser_operator unless you are explicitly running a coordinator/suite workflow where workers own tools.
86237
- Every browser step returns fresh snapshot text and perception fields. Vision-capable turns also receive one annotated screenshot; successful clicks include clickPoint/bounds so you can verify exactly what changed.
86242
+ Every browser step returns fresh snapshot text and perception fields. Vision-capable turns also receive one annotated screenshot; successful clicks include clickPoint/bounds so you can verify exactly what changed. On a text-only model the loop automatically runs each post-action screenshot through the vision sidecar and injects a "Vision readout of the current screen" message \u2014 trust it as the source of truth and use the x/y viewport fractions it names for any browser_visual_click instead of guessing coordinates.
86243
+ When a semantic browser_click finds no DOM match, the runner auto-escalates to a coordinate/visual (CUA) click on the same target before returning a failure, so the cursor still moves. Never re-issue the identical semantic click; if it still fails, escalate yourself with browser_visual_click on the visible target, and report a blocker only after the coordinate click also fails. browser_visual_click grounds its label against the live DOM (including shadow-DOM web components) and clicks the real element \u2014 always pass a label; the x/y is only a fallback for true canvas/non-DOM targets. To fill a field on a web-component/shadow-DOM site (e.g. Reddit), browser_visual_click the field to focus it then browser_type the text (typing goes into the focused field; no semantic target needed); if the type reports the field still empty, visual_click it again and retype. For a target behind a menu/dropdown/overflow (e.g. Reddit "Mod Tools"), observe \u2192 click the parent trigger to open the menu \u2192 observe \u2192 then click the now-visible item rather than clicking a hidden item.
86238
86244
  If a verified tool returns needs_operator with a live URL/snapshot/screenshot, continue from that evidence with the browser primitives. If it reports the embedded browser/session is disconnected or unavailable, report that blocker instead of trying raw MCP/Playwright or claiming success. Do not restart the task or dispatch a separate browser worker.`;
86239
86245
  }
86240
86246
  });
@@ -87396,7 +87402,7 @@ var init_threadLedger = __esm({
87396
87402
  });
87397
87403
 
87398
87404
  // features/perchTerminal/runtime/perchMemoryGuidance.ts
87399
- var VISIBLE_OUTPUT_STYLE_GUIDANCE, SAFFRON_STYLE_CONTRACT, QUILL_STYLE_CONTRACT, SAFFRON_CORE_IDENTITY, QUILL_CORE_IDENTITY, PERCH_MEMORY_GUIDANCE;
87405
+ var VISIBLE_OUTPUT_STYLE_GUIDANCE, PERCH_OPERATOR_CAPABILITIES, SAFFRON_STYLE_CONTRACT, QUILL_STYLE_CONTRACT, SAFFRON_CORE_IDENTITY, QUILL_CORE_IDENTITY, PERCH_MEMORY_GUIDANCE;
87400
87406
  var init_perchMemoryGuidance = __esm({
87401
87407
  "features/perchTerminal/runtime/perchMemoryGuidance.ts"() {
87402
87408
  "use strict";
@@ -87411,6 +87417,36 @@ Avoid stock AI phrases: "delve," "nuanced," "robust," "seamless,"
87411
87417
  Be concise when the user is moving fast.
87412
87418
  Preserve the persona: Quill is warm, literate, and direct; Saffron is sharp,
87413
87419
  practical, and confident.
87420
+ `.trim();
87421
+ PERCH_OPERATOR_CAPABILITIES = `
87422
+ ## What you can do (and you can do a lot)
87423
+
87424
+ You run on the user's Mac through Perch Desktop. You are a real operator, not a
87425
+ chat box, and you have the tools to act. When someone asks what you can do, or
87426
+ whether you can control their computer, answer with specifics and offer to show
87427
+ them rather than listing what you cannot do. You can:
87428
+
87429
+ - Run shell commands in folders the user approves, including installing tools and
87430
+ packages, running scripts and software, and starting long jobs in the background.
87431
+ - Work across their files: read, write, edit, search with glob and grep, move,
87432
+ and organize anything inside approved folders.
87433
+ - Run real analysis in a sealed sandbox: write and execute Python or Node over
87434
+ their actual data.
87435
+ - Drive a real browser: navigate, click, type, fill forms, research the web, and
87436
+ pull sources.
87437
+ - Deliver finished work: draft and send Gmail, create Google Docs and Sheets, and
87438
+ add calendar events.
87439
+ - Do forensic and financial work: AP audits, reconciliations, statement and
87440
+ earnings reviews, and multi-step suites.
87441
+
87442
+ Lead with what you can do and offer to prove it, for example "want me to run
87443
+ something to show you?". Do not talk yourself down or open with a list of limits.
87444
+
87445
+ State the real limits once, and only when they matter: file and command access is
87446
+ scoped to the folders the user approves, and commands run under their permission
87447
+ settings. You work through the shell, their files, the sandbox, and the browser,
87448
+ not through arbitrary macOS system settings or other desktop apps. That is already
87449
+ a lot. Show it.
87414
87450
  `.trim();
87415
87451
  SAFFRON_STYLE_CONTRACT = `
87416
87452
  ## Voice contract \u2014 Saffron (visible output only)
@@ -87447,6 +87483,8 @@ You genuinely enjoy finding something interesting buried in a mess.
87447
87483
 
87448
87484
  You're not an assistant. You're the operator who gets the work done.
87449
87485
 
87486
+ ${PERCH_OPERATOR_CAPABILITIES}
87487
+
87450
87488
  ## How you work
87451
87489
 
87452
87490
  Lead with the finding. Skip the preamble. "Seven duplicate payments, same vendor,
@@ -87577,6 +87615,8 @@ the user needs analysis, files, data, an audit, or delivery, you do that work
87577
87615
  yourself with the same tools \u2014 in your own voice. You never punt real work to
87578
87616
  another persona.
87579
87617
 
87618
+ ${PERCH_OPERATOR_CAPABILITIES}
87619
+
87580
87620
  ## How you work
87581
87621
 
87582
87622
  Read first. Before you draft a single sentence, you know what the piece is for,
@@ -91108,7 +91148,6 @@ Final answers lead with findings, name artifacts or delivery status, and give on
91108
91148
  var MARKET_DESK_TOOL_NAMES;
91109
91149
  var init_marketDeskAccess = __esm({
91110
91150
  "features/perchTerminal/runtime/marketDesk/marketDeskAccess.ts"() {
91111
- "use strict";
91112
91151
  init_toolNames();
91113
91152
  MARKET_DESK_TOOL_NAMES = /* @__PURE__ */ new Set([
91114
91153
  TOOL_NAMES.getMarketSignal,
@@ -91788,7 +91827,6 @@ function listFinancialPlaybooks() {
91788
91827
  var AP_AUDIT_PACKET_DEF, PLAYBOOKS;
91789
91828
  var init_registry2 = __esm({
91790
91829
  "features/perchTerminal/runtime/financialPlaybooks/registry.ts"() {
91791
- "use strict";
91792
91830
  init_managedWorkflowRegistry2();
91793
91831
  init_toolNames();
91794
91832
  AP_AUDIT_PACKET_DEF = {
@@ -92416,6 +92454,297 @@ var init_coreSystemPromptLanes = __esm({
92416
92454
  }
92417
92455
  });
92418
92456
 
92457
+ // lib/perchTerminal/appHostGate.ts
92458
+ var PERCH_CHAT_WEB_URL, PERCH_MARKETING_SITE_URL, PERCH_MARKETING_DESKTOP_URL, PERCH_MARKETING_CLI_URL, PERCH_CLI_INSTALL_COMMAND;
92459
+ var init_appHostGate = __esm({
92460
+ "lib/perchTerminal/appHostGate.ts"() {
92461
+ PERCH_CHAT_WEB_URL = process.env.NEXT_PUBLIC_PERCHAI_WEB_URL?.trim() || "https://chat.perchai.app";
92462
+ PERCH_MARKETING_SITE_URL = process.env.NEXT_PUBLIC_MARKETING_SITE_URL?.trim() || "https://perchai.app";
92463
+ PERCH_MARKETING_DESKTOP_URL = `${PERCH_MARKETING_SITE_URL}/desktop`;
92464
+ PERCH_MARKETING_CLI_URL = `${PERCH_MARKETING_SITE_URL}/cli`;
92465
+ PERCH_CLI_INSTALL_COMMAND = "npm install -g perchai-cli";
92466
+ }
92467
+ });
92468
+
92469
+ // lib/perch-ai/plan-catalog.ts
92470
+ var PERCH_AI_PLAN_CATALOG;
92471
+ var init_plan_catalog = __esm({
92472
+ "lib/perch-ai/plan-catalog.ts"() {
92473
+ PERCH_AI_PLAN_CATALOG = [
92474
+ {
92475
+ code: "internal",
92476
+ name: "Internal",
92477
+ description: "Full Perch AI internal/admin access for Perch Terminal operators.",
92478
+ entitlements: {
92479
+ "shell.preview": { enabled: true },
92480
+ "auth.access_codes.redeem": { enabled: true },
92481
+ "desktop.runtime": { enabled: true },
92482
+ "coding.workspace": { enabled: true },
92483
+ "ap.audit": { enabled: true },
92484
+ "writing.studio": { enabled: true },
92485
+ "writing.studio.web": { enabled: true },
92486
+ "admin.console": { enabled: true },
92487
+ "limits.daily_runs": { limit: null },
92488
+ "limits.weekly_runs": { limit: null },
92489
+ "limits.max_context_tokens": { limit: 2e5 },
92490
+ "limits.allowed_models": { models: ["*"] }
92491
+ }
92492
+ },
92493
+ {
92494
+ code: "pilot",
92495
+ name: "Pilot",
92496
+ description: "Self-serve pilot access with full product features.",
92497
+ entitlements: {
92498
+ "shell.preview": { enabled: true },
92499
+ "auth.access_codes.redeem": { enabled: true },
92500
+ "desktop.runtime": { enabled: true },
92501
+ "coding.workspace": { enabled: true },
92502
+ "ap.audit": { enabled: true },
92503
+ "writing.studio": { enabled: true },
92504
+ "writing.studio.web": { enabled: true },
92505
+ // Admin console stays off for self-serve signups (it grants operator/admin
92506
+ // tooling, not a product feature). Flip to true only for trusted accounts.
92507
+ "admin.console": { enabled: false },
92508
+ "limits.daily_runs": { limit: null },
92509
+ "limits.weekly_runs": { limit: null },
92510
+ "limits.max_context_tokens": { limit: 2e5 },
92511
+ "limits.allowed_models": { models: ["*"] }
92512
+ }
92513
+ },
92514
+ {
92515
+ code: "demo",
92516
+ name: "Demo",
92517
+ description: "Restricted demo access for guided evaluations.",
92518
+ entitlements: {
92519
+ "shell.preview": { enabled: true },
92520
+ "auth.access_codes.redeem": { enabled: true },
92521
+ "desktop.runtime": { enabled: false },
92522
+ "coding.workspace": { enabled: false },
92523
+ "ap.audit": { enabled: false },
92524
+ "writing.studio": { enabled: false },
92525
+ "writing.studio.web": { enabled: false },
92526
+ "admin.console": { enabled: false },
92527
+ "limits.daily_runs": { limit: 10 },
92528
+ "limits.weekly_runs": { limit: 30 },
92529
+ "limits.max_context_tokens": { limit: 32e3 },
92530
+ "limits.allowed_models": { models: ["gpt-4o-mini"] }
92531
+ }
92532
+ }
92533
+ ];
92534
+ }
92535
+ });
92536
+
92537
+ // features/perchTerminal/runtime/roost.ts
92538
+ function normalizeRoostTier(value) {
92539
+ if (value === "max") return "pro_max";
92540
+ return isRoostTier(value) ? value : null;
92541
+ }
92542
+ function normalizeRoostModelChoice(value) {
92543
+ if (value === "internal") return "internal";
92544
+ return normalizeRoostTier(value);
92545
+ }
92546
+ function isRoostTier(value) {
92547
+ return value === "standard" || value === "standard_max" || value === "pro" || value === "pro_max";
92548
+ }
92549
+ var ROOST_TIERS, ROOST_TIER_LABELS, ROOST_TIER_CONTEXT_LABELS;
92550
+ var init_roost = __esm({
92551
+ "features/perchTerminal/runtime/roost.ts"() {
92552
+ "use strict";
92553
+ init_modelRegistry();
92554
+ ROOST_TIERS = ["standard", "standard_max", "pro", "pro_max"];
92555
+ ROOST_TIER_LABELS = {
92556
+ standard: "Standard",
92557
+ standard_max: "Standard Max",
92558
+ pro: "Pro",
92559
+ pro_max: "Pro Max"
92560
+ };
92561
+ ROOST_TIER_CONTEXT_LABELS = {
92562
+ standard: "200K context",
92563
+ standard_max: "1M context",
92564
+ pro: "200K context",
92565
+ pro_max: "1M context"
92566
+ };
92567
+ }
92568
+ });
92569
+
92570
+ // lib/perch/perchFactsCore.ts
92571
+ function getPilotEntitlements() {
92572
+ const pilot = PERCH_AI_PLAN_CATALOG.find((p) => p.code === "pilot");
92573
+ if (!pilot) return null;
92574
+ return pilot.entitlements;
92575
+ }
92576
+ function formatUsageLine() {
92577
+ const ent = getPilotEntitlements();
92578
+ if (!ent) return "Usage limits are configured per plan.";
92579
+ const daily = ent["limits.daily_runs"]?.limit;
92580
+ const weekly = ent["limits.weekly_runs"]?.limit;
92581
+ if (daily === null && weekly === null) {
92582
+ return "No credit card is required. Usage is generous for the pilot period. Perch may apply fair-use limits to keep the service reliable for everyone.";
92583
+ }
92584
+ const parts = [];
92585
+ if (daily !== null && daily !== void 0) parts.push(`${daily} runs per day`);
92586
+ if (weekly !== null && weekly !== void 0) parts.push(`${weekly} runs per week`);
92587
+ return `Usage is capped at ${parts.join(" and ")} on this plan. No credit card is required.`;
92588
+ }
92589
+ function buildRoostTierSummary() {
92590
+ const lines = ROOST_TIERS.map(
92591
+ (tier) => `- ${ROOST_TIER_LABELS[tier]} (${ROOST_TIER_CONTEXT_LABELS[tier]})`
92592
+ );
92593
+ return lines.join("\n");
92594
+ }
92595
+ function buildWebCapabilityList() {
92596
+ return [
92597
+ "- Grounded answers with real inline citations from your uploaded documents.",
92598
+ "- Upload and ask: PDF, spreadsheet, image, and document analysis.",
92599
+ "- Web search with cited results.",
92600
+ "- Draft citation check: marks each claim verified or unverified.",
92601
+ "",
92602
+ "The web chat does not run shell commands, access your local files, control a browser,",
92603
+ `or operate your computer. Those capabilities live in Perch Terminal (Desktop and CLI).`,
92604
+ `Get Perch Terminal at ${PERCH_MARKETING_DESKTOP_URL}.`
92605
+ ].join("\n");
92606
+ }
92607
+ function buildTerminalCapabilityList() {
92608
+ const skillLines = DEDUPLICATED_SKILLS.map(
92609
+ (s) => `- ${s.id}: ${s.description}`
92610
+ ).join("\n");
92611
+ return [
92612
+ "Core operator capabilities (available in Desktop and CLI):",
92613
+ "- Run shell commands in folders the user approves.",
92614
+ "- Read, write, edit, search, and organize files across approved folders.",
92615
+ "- Run real analysis in a sealed sandbox: Python and Node over actual data.",
92616
+ "- Drive a real browser: navigate, click, fill forms, research the web.",
92617
+ "- Deliver finished work: draft and send Gmail, create Google Docs and Sheets, add calendar events.",
92618
+ "- Financial and forensic work: AP audits, reconciliations, statement and earnings reviews, multi-step suites.",
92619
+ "",
92620
+ "Specialized skills (activated per task from the skill registry):",
92621
+ skillLines
92622
+ ].join("\n");
92623
+ }
92624
+ function buildPerchFactsCore(surface) {
92625
+ const capabilitySection = surface === "web" ? `### What this web chat can do
92626
+
92627
+ ${buildWebCapabilityList()}` : `### What Perch Terminal can do
92628
+
92629
+ ${buildTerminalCapabilityList()}`;
92630
+ const tierTwoNote = surface === "terminal" ? `
92631
+ For deep detail on Perch (per-tier specifics, full workflow matrix, FAQ), consult the "about-perch" skill when a question goes beyond the facts above.` : "";
92632
+ return [
92633
+ INSTRUCTION_PREAMBLE,
92634
+ "",
92635
+ WHAT_IS_PERCH,
92636
+ "",
92637
+ ACCESS_AND_PRICING,
92638
+ "",
92639
+ MODEL_STORY,
92640
+ "",
92641
+ capabilitySection,
92642
+ tierTwoNote
92643
+ ].join("\n").trim();
92644
+ }
92645
+ var PERCH_SKILL_REGISTRY, DEDUPLICATED_SKILLS, INSTRUCTION_PREAMBLE, WHAT_IS_PERCH, ACCESS_AND_PRICING, MODEL_STORY;
92646
+ var init_perchFactsCore = __esm({
92647
+ "lib/perch/perchFactsCore.ts"() {
92648
+ init_appHostGate();
92649
+ init_plan_catalog();
92650
+ init_roost();
92651
+ PERCH_SKILL_REGISTRY = [
92652
+ {
92653
+ id: "workspace",
92654
+ description: "Navigate, inspect, search, summarize, and safely edit local workspaces, attached folders, source inventories, project files, and codebases with exact file-grounded evidence."
92655
+ },
92656
+ {
92657
+ id: "browser-operator",
92658
+ description: "Operate Perch's logged-in browser surface using visual state, semantic DOM targets, and coordinate actions when available."
92659
+ },
92660
+ {
92661
+ id: "research",
92662
+ description: "Gather, compare, rank, and cite evidence from public web sources, scholarly sources, workspace files, source packets, and attached local folders."
92663
+ },
92664
+ {
92665
+ id: "documents",
92666
+ description: "Create, edit, verify, and deliver professional document artifacts such as DOCX, Google Docs, memos, letters, reports, redlines, and formal written deliverables."
92667
+ },
92668
+ {
92669
+ id: "spreadsheets",
92670
+ description: "Create, inspect, clean, calculate, format, and verify spreadsheet artifacts such as XLSX, CSV, TSV, Google Sheets, formulas, pivots, charts, and reconciliation workbooks."
92671
+ },
92672
+ {
92673
+ id: "pdf",
92674
+ description: "Extract, summarize, cite, split, merge, create, inspect, and verify PDF content, including text PDFs, scanned PDFs, tables, forms, page citations, and generated reports."
92675
+ },
92676
+ {
92677
+ id: "presentations",
92678
+ description: "Build executive decks, board decks, sales decks, research summaries, and visual narratives as PPTX or shareable slide artifacts with verified layout and source-backed content."
92679
+ },
92680
+ {
92681
+ id: "data-quality",
92682
+ description: "Profile messy data, find anomalies, reconcile systems, explain exceptions, score risk, and produce audit-ready evidence through deterministic math before narrative."
92683
+ },
92684
+ {
92685
+ id: "diagrams",
92686
+ description: "Create, edit, and verify diagrams, graphs, workflows, org charts, entity networks, control graphs, architecture maps, and evidence-backed visual structures."
92687
+ },
92688
+ {
92689
+ id: "github",
92690
+ description: "Inspect GitHub and git repository state, review commits and pull requests, debug checks, and publish local changes with clear permission boundaries."
92691
+ },
92692
+ {
92693
+ id: "wait-operator",
92694
+ description: "Watch bounded external or local conditions, poll deployment and job status, wait through short-running tasks, and resume with verified state instead of guessing."
92695
+ },
92696
+ {
92697
+ id: "data-quality",
92698
+ description: "Profile messy data, find anomalies, reconcile systems, explain exceptions, score risk, and produce audit-ready evidence through deterministic math before narrative."
92699
+ }
92700
+ ];
92701
+ DEDUPLICATED_SKILLS = Array.from(
92702
+ new Map(PERCH_SKILL_REGISTRY.map((s) => [s.id, s])).values()
92703
+ );
92704
+ INSTRUCTION_PREAMBLE = `## Perch product facts -- authoritative reference
92705
+
92706
+ For any question about Perch (what it is, whether it is free, what you can do,
92707
+ what model you are, do I need an account, etc.), answer only from the facts
92708
+ below. If something is not covered here, say you are not certain rather than
92709
+ guessing.`.trim();
92710
+ WHAT_IS_PERCH = `### What Perch is
92711
+
92712
+ Perch is an AI platform built around verifiable AI: every claim is sourced and
92713
+ checkable. The goal is to make AI output trustworthy, not just fluent.
92714
+
92715
+ Perch has three surfaces:
92716
+ - Perch AI Web (chat.perchai.app): a warm, accessible web chat for grounded
92717
+ answers, document upload, web search, and citation checking.
92718
+ - Perch Terminal Desktop: a local operator that runs on your Mac, controls
92719
+ your files, runs code in a sandbox, drives a real browser, and delivers
92720
+ finished work. Download at ${PERCH_MARKETING_DESKTOP_URL}.
92721
+ - Perch Terminal CLI: the same operator power from your terminal.
92722
+ Install with: ${PERCH_CLI_INSTALL_COMMAND}.`.trim();
92723
+ ACCESS_AND_PRICING = `### Access and pricing
92724
+
92725
+ All three surfaces are free during the current pilot. No credit card is
92726
+ required to sign up.
92727
+
92728
+ ${formatUsageLine()}
92729
+
92730
+ A Perch account is required to use the web chat. Sign up at ${PERCH_CHAT_WEB_URL}.
92731
+ Perch Terminal Desktop and CLI are also free; sign in with your Perch account.`.trim();
92732
+ MODEL_STORY = `### The model -- Roost
92733
+
92734
+ Perch does not expose raw model names. The model layer is called Roost.
92735
+ Roost picks the best available model for each task and locks it for that task.
92736
+ You are powered by Roost.
92737
+
92738
+ Roost has four tiers:
92739
+ ${buildRoostTierSummary()}
92740
+
92741
+ Standard and Pro use a 200K context window. Standard Max and Pro Max use a 1M
92742
+ context window (Max = long context). Tier selection is available in the
92743
+ composer. When someone asks what model you are, say: "I run on Roost, Perch's
92744
+ model layer. Perch does not surface the underlying model names."`.trim();
92745
+ }
92746
+ });
92747
+
92419
92748
  // features/perchTerminal/runtime/context/coreSystemSection.ts
92420
92749
  function buildCoreSystemSection(input) {
92421
92750
  const enabledToolNames = Array.from(new Set(input.enabledToolNames ?? []));
@@ -92442,6 +92771,7 @@ function buildCoreSystemSection(input) {
92442
92771
  });
92443
92772
  const lines = splitRenderedLaneText(renderedCoreLanes);
92444
92773
  const permissionModeHandled = corePromptPermissionModeHandled(renderedCoreLanes);
92774
+ lines.push("", PERCH_FACTS_CORE_TERMINAL);
92445
92775
  if (!permissionModeHandled && input.chatMode === "plan") {
92446
92776
  if (planGateRejected) {
92447
92777
  lines.push(
@@ -92699,7 +93029,7 @@ function buildCoordinatorNotificationSynthesisInstructions(notifications) {
92699
93029
  hasFailure ? "If the error-code is batch_failed, state that the suite failed and that no downstream stages ran." : "If multiple notifications arrived, combine them into one concise synthesis without exposing raw XML."
92700
93030
  ].join("\n");
92701
93031
  }
92702
- var PLATFORM_DELIVERY_GUIDANCE, LIVE_NARRATION_GUIDANCE;
93032
+ var PERCH_FACTS_CORE_TERMINAL, PLATFORM_DELIVERY_GUIDANCE, LIVE_NARRATION_GUIDANCE;
92703
93033
  var init_coreSystemSection = __esm({
92704
93034
  "features/perchTerminal/runtime/context/coreSystemSection.ts"() {
92705
93035
  "use strict";
@@ -92715,6 +93045,8 @@ var init_coreSystemSection = __esm({
92715
93045
  init_operatingCaseService();
92716
93046
  init_planModeStateMachine();
92717
93047
  init_coreSystemPromptLanes();
93048
+ init_perchFactsCore();
93049
+ PERCH_FACTS_CORE_TERMINAL = buildPerchFactsCore("terminal");
92718
93050
  PLATFORM_DELIVERY_GUIDANCE = `## Platform delivery \u2014 use the verified shortcut, do not hand off
92719
93051
  You own delivery end-to-end in your own loop. Do NOT dispatch a sub-agent for Gmail, Google Docs, or Calendar \u2014 handing delivery off loses your context and breaks multi-step tasks (e.g. "make a doc AND email it"). Keep one continuous thread: you drafted it, so you deliver it, and you still remember every prior step (recipients, the doc URL you just created, etc.).
92720
93052
 
@@ -92756,6 +93088,45 @@ var init_agentSkillRegistry = __esm({
92756
93088
  "generated/agentSkillRegistry.ts"() {
92757
93089
  "use strict";
92758
93090
  AGENT_SKILL_REGISTRY = [
93091
+ {
93092
+ "id": "about-perch",
93093
+ "name": "about-perch",
93094
+ "description": "Answer detailed questions about Perch the product -- what it is, surfaces, pricing, the Roost model layer, per-tier details, capabilities, web vs Terminal differences, and common user questions. Consult this skill when a Perch product question goes beyond the always-on core facts.",
93095
+ "path": "features/perchTerminal/agentPlatform/skills/about-perch/SKILL.md",
93096
+ "body": "\n# About Perch\n\nUse the facts in this skill to answer any detailed product question about Perch.\nDo not guess. If a fact is not here and not in the always-on core, say you are not certain.\nNever reveal underlying model names. Always refer to the model layer as Roost.\n\nSee SKILL.md for the full product FAQ, web vs Terminal matrix, and per-tier detail.",
93097
+ "surface": {
93098
+ "gui": "supported",
93099
+ "cli": "supported"
93100
+ },
93101
+ "triggerHints": [
93102
+ "what is perch",
93103
+ "what can you do",
93104
+ "what model are you",
93105
+ "is this free",
93106
+ "is perch free",
93107
+ "is terminal free",
93108
+ "is terminal paid",
93109
+ "do i need an account",
93110
+ "roost",
93111
+ "roost tier",
93112
+ "standard max",
93113
+ "pro max",
93114
+ "perch terminal",
93115
+ "perch desktop",
93116
+ "perch cli",
93117
+ "perch vs",
93118
+ "web vs terminal",
93119
+ "difference between perch",
93120
+ "how does perch work",
93121
+ "citation verifier",
93122
+ "what is roost"
93123
+ ],
93124
+ "companionSkills": [
93125
+ "research",
93126
+ "workspace"
93127
+ ],
93128
+ "preferredTools": []
93129
+ },
92759
93130
  {
92760
93131
  "id": "browser-operator",
92761
93132
  "name": "browser-operator",
@@ -93407,7 +93778,8 @@ var init_skillSelector = __esm({
93407
93778
  "diagrams",
93408
93779
  "workspace",
93409
93780
  "browser-operator",
93410
- "skill-creator"
93781
+ "skill-creator",
93782
+ "about-perch"
93411
93783
  ];
93412
93784
  INTENT_PATTERNS = {
93413
93785
  "browser-operator": [
@@ -93459,6 +93831,10 @@ var init_skillSelector = __esm({
93459
93831
  ],
93460
93832
  "skill-creator": [
93461
93833
  /\b(skill\s+creator|create\s+(?:a\s+)?skill|skill\.md|agent\s+skill|skill\s+validation|trigger\s+collision|skill\s+test|skill\s+authoring|perch\s+skill)\b/i
93834
+ ],
93835
+ "about-perch": [
93836
+ /\b(what\s+(is|are)\s+perch|what\s+can\s+you\s+do|what\s+model\s+are\s+you|is\s+(this|perch|terminal|it)\s+free|is\s+terminal\s+paid|do\s+i\s+need\s+an\s+account|account\s+required|roost\s+(tier|model|layer)|standard\s+max|pro\s+max|perch\s+terminal|perch\s+desktop|perch\s+cli|web\s+vs\s+terminal|how\s+does\s+perch\s+work|citation\s+verifier|what\s+is\s+roost)\b/i,
93837
+ /\b(tell\s+me\s+about\s+perch|about\s+perch|perch\s+pricing|perch\s+product|perch\s+features?|how\s+much\s+does\s+perch\s+cost)\b/i
93462
93838
  ]
93463
93839
  };
93464
93840
  }
@@ -115612,10 +115988,10 @@ function matchStringTarget(hostname, target) {
115612
115988
  }
115613
115989
  return false;
115614
115990
  }
115615
- function getDefaultPropagationTargets(supabaseUrl2) {
115991
+ function getDefaultPropagationTargets(supabaseUrl) {
115616
115992
  const targets = [];
115617
115993
  try {
115618
- const url = new URL(supabaseUrl2);
115994
+ const url = new URL(supabaseUrl);
115619
115995
  targets.push(url.hostname);
115620
115996
  } catch (error) {
115621
115997
  }
@@ -115712,8 +116088,8 @@ function applySettingDefaults(options, defaults) {
115712
116088
  else delete result2.accessToken;
115713
116089
  return result2;
115714
116090
  }
115715
- function validateSupabaseUrl(supabaseUrl2) {
115716
- const trimmedUrl = supabaseUrl2 === null || supabaseUrl2 === void 0 ? void 0 : supabaseUrl2.trim();
116091
+ function validateSupabaseUrl(supabaseUrl) {
116092
+ const trimmedUrl = supabaseUrl === null || supabaseUrl === void 0 ? void 0 : supabaseUrl.trim();
115717
116093
  if (!trimmedUrl) throw new Error("supabaseUrl is required.");
115718
116094
  if (!trimmedUrl.match(/^https?:\/\//i)) throw new Error("Invalid supabaseUrl: Must be a valid HTTP or HTTPS URL.");
115719
116095
  try {
@@ -115771,12 +116147,12 @@ var init_dist4 = __esm({
115771
116147
  resolveHeadersConstructor = () => {
115772
116148
  return Headers;
115773
116149
  };
115774
- fetchWithAuth = (supabaseKey, supabaseUrl2, getAccessToken, customFetch, tracePropagationOptions) => {
116150
+ fetchWithAuth = (supabaseKey, supabaseUrl, getAccessToken, customFetch, tracePropagationOptions) => {
115775
116151
  const fetch$1 = resolveFetch2(customFetch);
115776
116152
  const HeadersConstructor = resolveHeadersConstructor();
115777
116153
  const traceEnabled = (tracePropagationOptions === null || tracePropagationOptions === void 0 ? void 0 : tracePropagationOptions.enabled) === true;
115778
116154
  const respectSampling = (tracePropagationOptions === null || tracePropagationOptions === void 0 ? void 0 : tracePropagationOptions.respectSamplingDecision) !== false;
115779
- const traceTargets = traceEnabled ? getDefaultPropagationTargets(supabaseUrl2) : null;
116155
+ const traceTargets = traceEnabled ? getDefaultPropagationTargets(supabaseUrl) : null;
115780
116156
  return async (input, init) => {
115781
116157
  var _await$getAccessToken;
115782
116158
  const accessToken = (_await$getAccessToken = await getAccessToken()) !== null && _await$getAccessToken !== void 0 ? _await$getAccessToken : supabaseKey;
@@ -115987,11 +116363,11 @@ var init_dist4 = __esm({
115987
116363
  * const { data } = await supabase.from('profiles').select('*')
115988
116364
  * ```
115989
116365
  */
115990
- constructor(supabaseUrl2, supabaseKey, options) {
116366
+ constructor(supabaseUrl, supabaseKey, options) {
115991
116367
  var _settings$auth$storag, _settings$global$head;
115992
- this.supabaseUrl = supabaseUrl2;
116368
+ this.supabaseUrl = supabaseUrl;
115993
116369
  this.supabaseKey = supabaseKey;
115994
- const baseUrl = validateSupabaseUrl(supabaseUrl2);
116370
+ const baseUrl = validateSupabaseUrl(supabaseUrl);
115995
116371
  if (!supabaseKey) throw new Error("supabaseKey is required.");
115996
116372
  this.realtimeUrl = new URL("realtime/v1", baseUrl);
115997
116373
  this.realtimeUrl.protocol = this.realtimeUrl.protocol.replace("http", "ws");
@@ -116019,7 +116395,7 @@ var init_dist4 = __esm({
116019
116395
  throw new Error(`@supabase/supabase-js: Supabase Client is configured with the accessToken option, accessing supabase.auth.${String(prop)} is not possible`);
116020
116396
  } });
116021
116397
  }
116022
- this.fetch = fetchWithAuth(supabaseKey, supabaseUrl2, this._getAccessToken.bind(this), settings.global.fetch, settings.tracePropagation);
116398
+ this.fetch = fetchWithAuth(supabaseKey, supabaseUrl, this._getAccessToken.bind(this), settings.global.fetch, settings.tracePropagation);
116023
116399
  this.realtime = this._initRealtimeClient(_objectSpread23({
116024
116400
  headers: this.headers,
116025
116401
  accessToken: this._getAccessToken.bind(this),
@@ -116203,8 +116579,8 @@ var init_dist4 = __esm({
116203
116579
  }
116204
116580
  }
116205
116581
  };
116206
- createClient = (supabaseUrl2, supabaseKey, options) => {
116207
- return new SupabaseClient(supabaseUrl2, supabaseKey, options);
116582
+ createClient = (supabaseUrl, supabaseKey, options) => {
116583
+ return new SupabaseClient(supabaseUrl, supabaseKey, options);
116208
116584
  };
116209
116585
  if (shouldShowDeprecationWarning()) console.warn("\u26A0\uFE0F Node.js 18 and below are deprecated and will no longer be supported in future versions of @supabase/supabase-js. Please upgrade to Node.js 20 or later. For more information, visit: https://github.com/orgs/supabase/discussions/37217");
116210
116586
  }
@@ -119113,28 +119489,20 @@ var init_autoRouter = __esm({
119113
119489
  }
119114
119490
  });
119115
119491
 
119116
- // features/perchTerminal/runtime/roost.ts
119117
- function normalizeRoostTier(value) {
119118
- if (value === "max") return "pro_max";
119119
- return isRoostTier(value) ? value : null;
119120
- }
119121
- function normalizeRoostModelChoice(value) {
119122
- if (value === "internal") return "internal";
119123
- return normalizeRoostTier(value);
119124
- }
119125
- function isRoostTier(value) {
119126
- return value === "standard" || value === "standard_max" || value === "pro" || value === "pro_max";
119492
+ // lib/auth/browserSession.ts
119493
+ async function syncBrowserAuthSession() {
119494
+ if (typeof window === "undefined") return null;
119495
+ const response = await fetch("/api/auth/session", SESSION_FETCH_OPTIONS);
119496
+ if (!response.ok) return null;
119497
+ const body = await response.json();
119498
+ return body.session ?? null;
119127
119499
  }
119128
- var ROOST_TIER_LABELS;
119129
- var init_roost = __esm({
119130
- "features/perchTerminal/runtime/roost.ts"() {
119131
- "use strict";
119132
- init_modelRegistry();
119133
- ROOST_TIER_LABELS = {
119134
- standard: "Standard",
119135
- standard_max: "Standard Max",
119136
- pro: "Pro",
119137
- pro_max: "Pro Max"
119500
+ var SESSION_FETCH_OPTIONS;
119501
+ var init_browserSession = __esm({
119502
+ "lib/auth/browserSession.ts"() {
119503
+ SESSION_FETCH_OPTIONS = {
119504
+ credentials: "same-origin",
119505
+ cache: "no-store"
119138
119506
  };
119139
119507
  }
119140
119508
  });
@@ -119178,6 +119546,507 @@ var init_roostUserSelectionClient = __esm({
119178
119546
  }
119179
119547
  });
119180
119548
 
119549
+ // features/perchTerminal/runtime/cliHost/cliAuthSession.ts
119550
+ var cliAuthSession_exports = {};
119551
+ __export(cliAuthSession_exports, {
119552
+ clearStoredCliAuthSession: () => clearStoredCliAuthSession,
119553
+ isStoredCliAuthSessionUsable: () => isStoredCliAuthSessionUsable,
119554
+ parseStoredCliAuthSession: () => parseStoredCliAuthSession,
119555
+ readStoredCliAuthSession: () => readStoredCliAuthSession,
119556
+ writeStoredCliAuthSession: () => writeStoredCliAuthSession
119557
+ });
119558
+ import { execFile } from "node:child_process";
119559
+ import fs9 from "node:fs/promises";
119560
+ import os from "node:os";
119561
+ import path10 from "node:path";
119562
+ import { promisify } from "node:util";
119563
+ async function readStoredCliAuthSession() {
119564
+ const raw = shouldUseFallbackAuthFile() ? await readFallbackSecret().catch(() => null) : process.platform === "darwin" ? await readKeychainSecret().catch(() => null) : await readFallbackSecret().catch(() => null);
119565
+ return parseStoredCliAuthSession(raw);
119566
+ }
119567
+ async function writeStoredCliAuthSession(session) {
119568
+ const raw = JSON.stringify(session);
119569
+ if (process.platform === "darwin" && !shouldUseFallbackAuthFile()) {
119570
+ await execFileAsync("security", [
119571
+ "add-generic-password",
119572
+ "-U",
119573
+ "-s",
119574
+ KEYCHAIN_SERVICE,
119575
+ "-a",
119576
+ KEYCHAIN_ACCOUNT,
119577
+ "-w",
119578
+ raw
119579
+ ]);
119580
+ return;
119581
+ }
119582
+ await writeFallbackSecret(raw);
119583
+ }
119584
+ async function clearStoredCliAuthSession() {
119585
+ if (process.platform === "darwin" && !shouldUseFallbackAuthFile()) {
119586
+ await execFileAsync("security", [
119587
+ "delete-generic-password",
119588
+ "-s",
119589
+ KEYCHAIN_SERVICE,
119590
+ "-a",
119591
+ KEYCHAIN_ACCOUNT
119592
+ ]).catch(() => void 0);
119593
+ return;
119594
+ }
119595
+ await fs9.rm(fallbackSessionPath(), { force: true }).catch(() => void 0);
119596
+ }
119597
+ function shouldUseFallbackAuthFile() {
119598
+ return Boolean(process.env.PERCH_CLI_AUTH_DIR?.trim());
119599
+ }
119600
+ function isStoredCliAuthSessionUsable(session, nowSeconds = Math.floor(Date.now() / 1e3)) {
119601
+ if (!session?.accessToken?.trim()) return false;
119602
+ if (!session.appUrl?.trim()) return false;
119603
+ if (!session.expiresAt) return true;
119604
+ return session.expiresAt > nowSeconds + 90;
119605
+ }
119606
+ function parseStoredCliAuthSession(raw) {
119607
+ if (!raw?.trim()) return null;
119608
+ try {
119609
+ const parsed = JSON.parse(raw);
119610
+ if (parsed.version !== 1) return null;
119611
+ if (!parsed.appUrl || !parsed.accessToken || !parsed.updatedAt) return null;
119612
+ return {
119613
+ version: 1,
119614
+ appUrl: parsed.appUrl,
119615
+ accessToken: parsed.accessToken,
119616
+ refreshToken: parsed.refreshToken ?? null,
119617
+ expiresAt: typeof parsed.expiresAt === "number" ? parsed.expiresAt : null,
119618
+ userId: parsed.userId ?? null,
119619
+ email: parsed.email ?? null,
119620
+ updatedAt: parsed.updatedAt
119621
+ };
119622
+ } catch {
119623
+ return null;
119624
+ }
119625
+ }
119626
+ async function readKeychainSecret() {
119627
+ const { stdout } = await execFileAsync("security", [
119628
+ "find-generic-password",
119629
+ "-s",
119630
+ KEYCHAIN_SERVICE,
119631
+ "-a",
119632
+ KEYCHAIN_ACCOUNT,
119633
+ "-w"
119634
+ ]);
119635
+ return stdout.trim() || null;
119636
+ }
119637
+ function fallbackSessionPath() {
119638
+ const base = process.env.PERCH_CLI_AUTH_DIR?.trim() || path10.join(os.homedir(), ".perch");
119639
+ return path10.join(base, "cli-auth-session.json");
119640
+ }
119641
+ async function readFallbackSecret() {
119642
+ return await fs9.readFile(fallbackSessionPath(), "utf8");
119643
+ }
119644
+ async function writeFallbackSecret(raw) {
119645
+ const filePath = fallbackSessionPath();
119646
+ await fs9.mkdir(path10.dirname(filePath), { recursive: true, mode: 448 });
119647
+ await fs9.writeFile(filePath, raw, { mode: 384 });
119648
+ }
119649
+ var execFileAsync, KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT;
119650
+ var init_cliAuthSession = __esm({
119651
+ "features/perchTerminal/runtime/cliHost/cliAuthSession.ts"() {
119652
+ "use strict";
119653
+ execFileAsync = promisify(execFile);
119654
+ KEYCHAIN_SERVICE = "app.perchai.cli-auth";
119655
+ KEYCHAIN_ACCOUNT = "default";
119656
+ }
119657
+ });
119658
+
119659
+ // features/perchTerminal/runtime/publicModelDefault.ts
119660
+ function parsePublicModelDefaultPayload(value) {
119661
+ const raw = value && typeof value === "object" ? value : {};
119662
+ const selection = sanitizeFounderModelSelection(
119663
+ raw.selection ?? DEFAULT_FOUNDER_MODEL_SELECTION
119664
+ );
119665
+ return {
119666
+ selection,
119667
+ updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : null,
119668
+ updatedBy: typeof raw.updatedBy === "string" ? raw.updatedBy : null
119669
+ };
119670
+ }
119671
+ var init_publicModelDefault = __esm({
119672
+ "features/perchTerminal/runtime/publicModelDefault.ts"() {
119673
+ "use strict";
119674
+ init_modelRegistry();
119675
+ }
119676
+ });
119677
+
119678
+ // features/perchTerminal/runtime/cliHost/modelConnection.ts
119679
+ async function connectCliModelProxy(input = {}) {
119680
+ const storedSession = input.storedSession === void 0 ? await readStoredCliAuthSession() : input.storedSession;
119681
+ const usableStoredSession = await ensureFreshCliAuthSession({
119682
+ session: storedSession,
119683
+ fetchImpl: input.fetchImpl
119684
+ });
119685
+ const appUrl = resolveCliAppUrl(input.appUrl, usableStoredSession?.appUrl ?? null);
119686
+ if (!appUrl) return noCliModelConnection();
119687
+ if (!usableStoredSession?.accessToken) return noCliModelConnection(appUrl);
119688
+ const fetchImpl = input.fetchImpl ?? globalThis.fetch;
119689
+ if (typeof fetchImpl !== "function") return noCliModelConnection(appUrl);
119690
+ const selection = await fetchPublicModelDefaultSelection(
119691
+ appUrl,
119692
+ fetchImpl,
119693
+ usableStoredSession?.accessToken ?? null
119694
+ );
119695
+ if (!selection) return noCliModelConnection(appUrl);
119696
+ const priorProxy = process.env[MODEL_PROXY_ENV];
119697
+ const priorToken = process.env[MODEL_PROXY_TOKEN_ENV];
119698
+ process.env[MODEL_PROXY_ENV] = appUrl;
119699
+ if (usableStoredSession?.accessToken) {
119700
+ process.env[MODEL_PROXY_TOKEN_ENV] = usableStoredSession.accessToken;
119701
+ }
119702
+ return {
119703
+ appUrl,
119704
+ authenticated: !!usableStoredSession?.accessToken,
119705
+ userId: usableStoredSession?.userId ?? null,
119706
+ email: usableStoredSession?.email ?? null,
119707
+ founderModelSelection: selection,
119708
+ restore: () => {
119709
+ if (priorProxy === void 0) {
119710
+ delete process.env[MODEL_PROXY_ENV];
119711
+ } else {
119712
+ process.env[MODEL_PROXY_ENV] = priorProxy;
119713
+ }
119714
+ if (priorToken === void 0) {
119715
+ delete process.env[MODEL_PROXY_TOKEN_ENV];
119716
+ } else {
119717
+ process.env[MODEL_PROXY_TOKEN_ENV] = priorToken;
119718
+ }
119719
+ }
119720
+ };
119721
+ }
119722
+ function resolveCliAppUrl(explicit, stored) {
119723
+ const raw = explicit?.trim() || process.env[CLI_APP_URL_ENV]?.trim() || process.env[FALLBACK_APP_URL_ENV]?.trim() || stored?.trim() || DEFAULT_CLI_APP_URL;
119724
+ if (!raw) return null;
119725
+ const withProtocol = /^https?:\/\//i.test(raw) ? raw : `http://${raw}`;
119726
+ try {
119727
+ const url = new URL(withProtocol);
119728
+ url.pathname = url.pathname.replace(/\/+$/, "");
119729
+ url.search = "";
119730
+ url.hash = "";
119731
+ return url.toString().replace(/\/+$/, "");
119732
+ } catch {
119733
+ return null;
119734
+ }
119735
+ }
119736
+ async function fetchPublicModelDefaultSelection(appUrl, fetchImpl, accessToken) {
119737
+ const url = `${appUrl}/api/perch-terminal/public-model-default`;
119738
+ const controller = new AbortController();
119739
+ const timeout = setTimeout(() => controller.abort(), 3500);
119740
+ try {
119741
+ const response = await fetchImpl(url, {
119742
+ method: "GET",
119743
+ headers: {
119744
+ Accept: "application/json",
119745
+ ...accessToken ? { Authorization: `Bearer ${accessToken}` } : {}
119746
+ },
119747
+ signal: controller.signal
119748
+ });
119749
+ if (!response.ok) return null;
119750
+ const body = await response.json();
119751
+ if (!body || typeof body !== "object") return null;
119752
+ const record = body;
119753
+ if (record.ok !== true) return null;
119754
+ return parsePublicModelDefaultPayload({ selection: record.selection }).selection;
119755
+ } catch {
119756
+ return null;
119757
+ } finally {
119758
+ clearTimeout(timeout);
119759
+ }
119760
+ }
119761
+ function noCliModelConnection(appUrl = null) {
119762
+ return {
119763
+ appUrl,
119764
+ authenticated: false,
119765
+ userId: null,
119766
+ email: null,
119767
+ founderModelSelection: null,
119768
+ restore: () => {
119769
+ }
119770
+ };
119771
+ }
119772
+ var MODEL_PROXY_ENV, MODEL_PROXY_TOKEN_ENV, CLI_APP_URL_ENV, FALLBACK_APP_URL_ENV, DEFAULT_CLI_APP_URL;
119773
+ var init_modelConnection = __esm({
119774
+ "features/perchTerminal/runtime/cliHost/modelConnection.ts"() {
119775
+ "use strict";
119776
+ init_publicModelDefault();
119777
+ init_cliAuthSession();
119778
+ init_cliAuthRefresh();
119779
+ MODEL_PROXY_ENV = "PERCH_MODEL_CALL_PROXY_URL";
119780
+ MODEL_PROXY_TOKEN_ENV = "PERCH_MODEL_CALL_PROXY_TOKEN";
119781
+ CLI_APP_URL_ENV = "PERCH_CLI_APP_URL";
119782
+ FALLBACK_APP_URL_ENV = "PERCH_APP_URL";
119783
+ DEFAULT_CLI_APP_URL = "https://app.perchai.app";
119784
+ }
119785
+ });
119786
+
119787
+ // features/perchTerminal/runtime/cliHost/cliStandaloneOAuth.ts
119788
+ import { execFile as execFile2 } from "node:child_process";
119789
+ import http from "node:http";
119790
+ import { promisify as promisify2 } from "node:util";
119791
+ async function runCliStandaloneOAuthLogin(input) {
119792
+ const appUrl = resolveCliAppUrl(input.appUrl, null);
119793
+ if (!appUrl) {
119794
+ return { ok: false, code: "invalid_app_url", error: "Invalid Perch app URL." };
119795
+ }
119796
+ const fetchImpl = input.fetchImpl ?? globalThis.fetch;
119797
+ if (typeof fetchImpl !== "function") {
119798
+ return { ok: false, code: "fetch_unavailable", error: "This Node runtime does not expose fetch." };
119799
+ }
119800
+ const config = await fetchCliAuthConfig(appUrl, fetchImpl);
119801
+ if (!config.ok || !config.supabaseUrl || !config.supabaseAnonKey) {
119802
+ return {
119803
+ ok: false,
119804
+ code: "auth_config_unavailable",
119805
+ error: "The Perch app did not return CLI auth configuration."
119806
+ };
119807
+ }
119808
+ const provider = selectOAuthProvider(config.providers ?? [], input.provider);
119809
+ if (!provider) {
119810
+ return {
119811
+ ok: false,
119812
+ code: "oauth_provider_unavailable",
119813
+ error: "No supported CLI OAuth provider is enabled for this Perch app."
119814
+ };
119815
+ }
119816
+ const callback = await createOAuthCallbackServer({
119817
+ host: config.redirectHost?.trim() || "127.0.0.1",
119818
+ timeoutMs: input.timeoutMs ?? DEFAULT_LOGIN_TIMEOUT_MS
119819
+ });
119820
+ try {
119821
+ const memoryStorage = createMemoryAuthStorage();
119822
+ const supabase = createClient(config.supabaseUrl, config.supabaseAnonKey, {
119823
+ auth: {
119824
+ flowType: "pkce",
119825
+ autoRefreshToken: false,
119826
+ detectSessionInUrl: false,
119827
+ persistSession: true,
119828
+ storage: memoryStorage
119829
+ }
119830
+ });
119831
+ const { data, error } = await supabase.auth.signInWithOAuth({
119832
+ provider,
119833
+ options: {
119834
+ redirectTo: callback.redirectTo,
119835
+ skipBrowserRedirect: true,
119836
+ data: { signup_source: "cli" }
119837
+ }
119838
+ });
119839
+ if (error || !data.url) {
119840
+ return {
119841
+ ok: false,
119842
+ code: "oauth_start_failed",
119843
+ error: error?.message ?? "Supabase did not return an OAuth URL."
119844
+ };
119845
+ }
119846
+ await (input.openExternal ?? openExternal)(data.url);
119847
+ const callbackResult = await callback.waitForCode();
119848
+ if (!callbackResult.ok) {
119849
+ return callbackResult;
119850
+ }
119851
+ const exchanged = await supabase.auth.exchangeCodeForSession(callbackResult.code);
119852
+ if (exchanged.error || !exchanged.data.session) {
119853
+ return {
119854
+ ok: false,
119855
+ code: "oauth_exchange_failed",
119856
+ error: exchanged.error?.message ?? "Supabase did not return a CLI session."
119857
+ };
119858
+ }
119859
+ const session = storedSessionFromSupabaseSession({
119860
+ appUrl,
119861
+ accessToken: exchanged.data.session.access_token,
119862
+ refreshToken: exchanged.data.session.refresh_token,
119863
+ expiresAt: exchanged.data.session.expires_at ?? null,
119864
+ userId: exchanged.data.session.user.id,
119865
+ email: exchanged.data.session.user.email ?? null
119866
+ });
119867
+ await writeStoredCliAuthSession(session);
119868
+ return { ok: true, session };
119869
+ } finally {
119870
+ await callback.close();
119871
+ }
119872
+ }
119873
+ async function fetchCliAuthConfig(appUrl, fetchImpl = globalThis.fetch) {
119874
+ const response = await fetchImpl(`${appUrl}/api/perch-terminal/cli-auth/config`, {
119875
+ method: "GET",
119876
+ headers: { Accept: "application/json" }
119877
+ });
119878
+ if (!response.ok) return { ok: false };
119879
+ return await response.json();
119880
+ }
119881
+ function selectOAuthProvider(providers, preferred) {
119882
+ const available = new Set(providers);
119883
+ if (preferred && available.has(preferred)) return preferred;
119884
+ if (available.has("google")) return "google";
119885
+ if (available.has("github")) return "github";
119886
+ return null;
119887
+ }
119888
+ function storedSessionFromSupabaseSession(input) {
119889
+ return {
119890
+ version: 1,
119891
+ appUrl: input.appUrl,
119892
+ accessToken: input.accessToken,
119893
+ refreshToken: input.refreshToken ?? null,
119894
+ expiresAt: input.expiresAt ?? null,
119895
+ userId: input.userId ?? null,
119896
+ email: input.email ?? null,
119897
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
119898
+ };
119899
+ }
119900
+ function createMemoryAuthStorage() {
119901
+ const values2 = /* @__PURE__ */ new Map();
119902
+ return {
119903
+ getItem: (key) => values2.get(key) ?? null,
119904
+ setItem: (key, value) => {
119905
+ values2.set(key, value);
119906
+ },
119907
+ removeItem: (key) => {
119908
+ values2.delete(key);
119909
+ }
119910
+ };
119911
+ }
119912
+ async function createOAuthCallbackServer(input) {
119913
+ let resolveResult = null;
119914
+ const resultPromise = new Promise((resolve5) => {
119915
+ resolveResult = resolve5;
119916
+ });
119917
+ const server = http.createServer((request, response) => {
119918
+ const requestUrl = new URL(request.url ?? "/", `http://${input.host}`);
119919
+ if (requestUrl.pathname !== CALLBACK_PATH) {
119920
+ response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
119921
+ response.end("Not found");
119922
+ return;
119923
+ }
119924
+ const code = requestUrl.searchParams.get("code");
119925
+ const error = requestUrl.searchParams.get("error_description") ?? requestUrl.searchParams.get("error");
119926
+ if (!code) {
119927
+ response.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
119928
+ response.end(renderCallbackHtml(false));
119929
+ resolveResult?.({
119930
+ ok: false,
119931
+ code: "oauth_callback_missing_code",
119932
+ error: error ?? "OAuth callback did not include a code."
119933
+ });
119934
+ resolveResult = null;
119935
+ return;
119936
+ }
119937
+ response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
119938
+ response.end(renderCallbackHtml(true));
119939
+ resolveResult?.({ ok: true, code });
119940
+ resolveResult = null;
119941
+ });
119942
+ await new Promise((resolve5, reject2) => {
119943
+ server.once("error", reject2);
119944
+ server.listen(0, input.host, () => {
119945
+ server.off("error", reject2);
119946
+ resolve5();
119947
+ });
119948
+ });
119949
+ const address = server.address();
119950
+ const redirectTo = `http://${input.host}:${address.port}${CALLBACK_PATH}`;
119951
+ const timeout = setTimeout(() => {
119952
+ resolveResult?.({
119953
+ ok: false,
119954
+ code: "oauth_timeout",
119955
+ error: "Timed out waiting for browser sign-in to complete."
119956
+ });
119957
+ resolveResult = null;
119958
+ }, input.timeoutMs);
119959
+ return {
119960
+ redirectTo,
119961
+ waitForCode: async () => resultPromise,
119962
+ close: async () => {
119963
+ clearTimeout(timeout);
119964
+ await new Promise((resolve5) => server.close(() => resolve5()));
119965
+ }
119966
+ };
119967
+ }
119968
+ function renderCallbackHtml(ok) {
119969
+ const title = ok ? "Perch CLI signed in" : "Perch CLI sign-in failed";
119970
+ const message = ok ? "You can close this tab and return to your terminal." : "Return to your terminal and try again.";
119971
+ return `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title></head><body style="font-family:system-ui;padding:32px;background:#100d0b;color:#fff8f0"><h1>${title}</h1><p>${message}</p></body></html>`;
119972
+ }
119973
+ async function openExternal(url) {
119974
+ if (process.platform === "darwin") {
119975
+ await execFileAsync2("open", [url]);
119976
+ } else if (process.platform === "win32") {
119977
+ await execFileAsync2("cmd", ["/c", "start", "", url]);
119978
+ } else {
119979
+ await execFileAsync2("xdg-open", [url]);
119980
+ }
119981
+ }
119982
+ var execFileAsync2, DEFAULT_LOGIN_TIMEOUT_MS, CALLBACK_PATH;
119983
+ var init_cliStandaloneOAuth = __esm({
119984
+ "features/perchTerminal/runtime/cliHost/cliStandaloneOAuth.ts"() {
119985
+ "use strict";
119986
+ init_dist4();
119987
+ init_modelConnection();
119988
+ init_cliAuthSession();
119989
+ execFileAsync2 = promisify2(execFile2);
119990
+ DEFAULT_LOGIN_TIMEOUT_MS = 12e4;
119991
+ CALLBACK_PATH = "/callback";
119992
+ }
119993
+ });
119994
+
119995
+ // features/perchTerminal/runtime/cliHost/cliAuthRefresh.ts
119996
+ var cliAuthRefresh_exports = {};
119997
+ __export(cliAuthRefresh_exports, {
119998
+ ensureFreshCliAuthSession: () => ensureFreshCliAuthSession
119999
+ });
120000
+ async function ensureFreshCliAuthSession(input) {
120001
+ const session = input.session;
120002
+ if (!session?.accessToken?.trim()) return null;
120003
+ const nowSeconds = Math.floor(Date.now() / 1e3);
120004
+ const stillUsable = !session.expiresAt || session.expiresAt > nowSeconds + 90;
120005
+ if (stillUsable && !input.forceRefresh) return session;
120006
+ if (!session.refreshToken?.trim()) return null;
120007
+ const fetchImpl = input.fetchImpl ?? globalThis.fetch;
120008
+ if (typeof fetchImpl !== "function") return null;
120009
+ const appUrl = session.appUrl?.trim();
120010
+ if (!appUrl) return null;
120011
+ const config = await fetchCliAuthConfig(appUrl, fetchImpl).catch(() => ({ ok: false }));
120012
+ if (!config.ok || !config.supabaseUrl || !config.supabaseAnonKey) return null;
120013
+ const supabase = createClient(config.supabaseUrl, config.supabaseAnonKey, {
120014
+ auth: {
120015
+ autoRefreshToken: false,
120016
+ detectSessionInUrl: false,
120017
+ persistSession: false
120018
+ }
120019
+ });
120020
+ try {
120021
+ const { data, error } = await supabase.auth.refreshSession({
120022
+ refresh_token: session.refreshToken
120023
+ });
120024
+ if (error || !data.session) return null;
120025
+ const refreshed = {
120026
+ version: 1,
120027
+ appUrl: session.appUrl,
120028
+ accessToken: data.session.access_token,
120029
+ refreshToken: data.session.refresh_token ?? session.refreshToken,
120030
+ expiresAt: data.session.expires_at ?? null,
120031
+ userId: data.session.user?.id ?? session.userId ?? null,
120032
+ email: data.session.user?.email ?? session.email ?? null,
120033
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
120034
+ };
120035
+ await writeStoredCliAuthSession(refreshed);
120036
+ return refreshed;
120037
+ } catch {
120038
+ return null;
120039
+ }
120040
+ }
120041
+ var init_cliAuthRefresh = __esm({
120042
+ "features/perchTerminal/runtime/cliHost/cliAuthRefresh.ts"() {
120043
+ "use strict";
120044
+ init_dist4();
120045
+ init_cliStandaloneOAuth();
120046
+ init_cliAuthSession();
120047
+ }
120048
+ });
120049
+
119181
120050
  // features/perchTerminal/runtime/modelRouter.ts
119182
120051
  function roostUserSelectionBodyFields() {
119183
120052
  const selection = getEffectiveRoostUserSelection();
@@ -119470,23 +120339,59 @@ async function callModelRouterViaServer(request, opts, endpoint = "/api/perch-te
119470
120339
  durationMs: 0
119471
120340
  };
119472
120341
  }
120342
+ async function syncModelProxyAuthAfter401() {
120343
+ if (typeof globalThis.window !== "undefined") {
120344
+ await syncBrowserAuthSession();
120345
+ return null;
120346
+ }
120347
+ const { readStoredCliAuthSession: readStoredCliAuthSession2 } = await Promise.resolve().then(() => (init_cliAuthSession(), cliAuthSession_exports));
120348
+ const { ensureFreshCliAuthSession: ensureFreshCliAuthSession2 } = await Promise.resolve().then(() => (init_cliAuthRefresh(), cliAuthRefresh_exports));
120349
+ const fresh = await ensureFreshCliAuthSession2({
120350
+ session: await readStoredCliAuthSession2(),
120351
+ forceRefresh: true
120352
+ });
120353
+ if (!fresh?.accessToken?.trim()) return null;
120354
+ if (typeof process !== "undefined") {
120355
+ process.env[MODEL_CALL_PROXY_TOKEN_ENV] = fresh.accessToken;
120356
+ }
120357
+ return fresh.accessToken;
120358
+ }
120359
+ function withModelProxyAuthHeader(init, token) {
120360
+ if (!token?.trim()) return init;
120361
+ const headers = new Headers(init.headers);
120362
+ headers.set("Authorization", `Bearer ${token}`);
120363
+ return { ...init, headers };
120364
+ }
119473
120365
  async function fetchModelProxyWithRetry(endpoint, init, options) {
119474
120366
  const maxAttempts = Math.max(1, options?.maxAttempts ?? MODEL_PROXY_MAX_ATTEMPTS);
119475
120367
  let lastError = null;
120368
+ let authSynced = false;
120369
+ let requestInit = init;
119476
120370
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
119477
120371
  try {
119478
- const response = await fetch(endpoint, init);
120372
+ const response = await fetch(endpoint, requestInit);
120373
+ if (response.status === 401 && !authSynced) {
120374
+ authSynced = true;
120375
+ const refreshedToken = await syncModelProxyAuthAfter401();
120376
+ if (refreshedToken) {
120377
+ requestInit = withModelProxyAuthHeader(requestInit, refreshedToken);
120378
+ }
120379
+ options?.onRetry?.(attempt, "HTTP 401 (session sync)");
120380
+ await response.body?.cancel().catch(() => void 0);
120381
+ await waitForModelProxyRetry(attempt, requestInit.signal);
120382
+ continue;
120383
+ }
119479
120384
  if (!isRetriableModelProxyStatus(response.status) || attempt === maxAttempts - 1) {
119480
120385
  return response;
119481
120386
  }
119482
120387
  options?.onRetry?.(attempt, `HTTP ${response.status}`);
119483
120388
  await response.body?.cancel().catch(() => void 0);
119484
- await waitForModelProxyRetry(attempt, init.signal);
120389
+ await waitForModelProxyRetry(attempt, requestInit.signal);
119485
120390
  } catch (error) {
119486
120391
  if (isAbortLikeError(error) || attempt === maxAttempts - 1) throw error;
119487
120392
  lastError = error;
119488
120393
  options?.onRetry?.(attempt, error instanceof Error ? error.message : String(error));
119489
- await waitForModelProxyRetry(attempt, init.signal);
120394
+ await waitForModelProxyRetry(attempt, requestInit.signal);
119490
120395
  }
119491
120396
  }
119492
120397
  throw lastError instanceof Error ? lastError : new Error(String(lastError ?? "Model proxy request failed"));
@@ -119609,6 +120514,7 @@ var init_modelRouter = __esm({
119609
120514
  init_modelRegistry();
119610
120515
  init_contextMeter();
119611
120516
  init_inferenceUsageRecorder();
120517
+ init_browserSession();
119612
120518
  init_roostUserSelectionClient();
119613
120519
  init_providerModelStep();
119614
120520
  init_toolCallNormalizer();
@@ -129014,10 +129920,10 @@ var require_dist4 = __commonJS({
129014
129920
  }
129015
129921
  return false;
129016
129922
  }
129017
- function getDefaultPropagationTargets2(supabaseUrl2) {
129923
+ function getDefaultPropagationTargets2(supabaseUrl) {
129018
129924
  const targets = [];
129019
129925
  try {
129020
- const url = new URL(supabaseUrl2);
129926
+ const url = new URL(supabaseUrl);
129021
129927
  targets.push(url.hostname);
129022
129928
  } catch (error) {
129023
129929
  }
@@ -129083,12 +129989,12 @@ var require_dist4 = __commonJS({
129083
129989
  var resolveHeadersConstructor2 = () => {
129084
129990
  return Headers;
129085
129991
  };
129086
- var fetchWithAuth2 = (supabaseKey, supabaseUrl2, getAccessToken, customFetch, tracePropagationOptions) => {
129992
+ var fetchWithAuth2 = (supabaseKey, supabaseUrl, getAccessToken, customFetch, tracePropagationOptions) => {
129087
129993
  const fetch$1 = resolveFetch3(customFetch);
129088
129994
  const HeadersConstructor = resolveHeadersConstructor2();
129089
129995
  const traceEnabled = (tracePropagationOptions === null || tracePropagationOptions === void 0 ? void 0 : tracePropagationOptions.enabled) === true;
129090
129996
  const respectSampling = (tracePropagationOptions === null || tracePropagationOptions === void 0 ? void 0 : tracePropagationOptions.respectSamplingDecision) !== false;
129091
- const traceTargets = traceEnabled ? getDefaultPropagationTargets2(supabaseUrl2) : null;
129997
+ const traceTargets = traceEnabled ? getDefaultPropagationTargets2(supabaseUrl) : null;
129092
129998
  return async (input, init) => {
129093
129999
  var _await$getAccessToken;
129094
130000
  const accessToken = (_await$getAccessToken = await getAccessToken()) !== null && _await$getAccessToken !== void 0 ? _await$getAccessToken : supabaseKey;
@@ -129144,8 +130050,8 @@ var require_dist4 = __commonJS({
129144
130050
  else delete result2.accessToken;
129145
130051
  return result2;
129146
130052
  }
129147
- function validateSupabaseUrl2(supabaseUrl2) {
129148
- const trimmedUrl = supabaseUrl2 === null || supabaseUrl2 === void 0 ? void 0 : supabaseUrl2.trim();
130053
+ function validateSupabaseUrl2(supabaseUrl) {
130054
+ const trimmedUrl = supabaseUrl === null || supabaseUrl === void 0 ? void 0 : supabaseUrl.trim();
129149
130055
  if (!trimmedUrl) throw new Error("supabaseUrl is required.");
129150
130056
  if (!trimmedUrl.match(/^https?:\/\//i)) throw new Error("Invalid supabaseUrl: Must be a valid HTTP or HTTPS URL.");
129151
130057
  try {
@@ -129347,11 +130253,11 @@ var require_dist4 = __commonJS({
129347
130253
  * const { data } = await supabase.from('profiles').select('*')
129348
130254
  * ```
129349
130255
  */
129350
- constructor(supabaseUrl2, supabaseKey, options) {
130256
+ constructor(supabaseUrl, supabaseKey, options) {
129351
130257
  var _settings$auth$storag, _settings$global$head;
129352
- this.supabaseUrl = supabaseUrl2;
130258
+ this.supabaseUrl = supabaseUrl;
129353
130259
  this.supabaseKey = supabaseKey;
129354
- const baseUrl = validateSupabaseUrl2(supabaseUrl2);
130260
+ const baseUrl = validateSupabaseUrl2(supabaseUrl);
129355
130261
  if (!supabaseKey) throw new Error("supabaseKey is required.");
129356
130262
  this.realtimeUrl = new URL("realtime/v1", baseUrl);
129357
130263
  this.realtimeUrl.protocol = this.realtimeUrl.protocol.replace("http", "ws");
@@ -129379,7 +130285,7 @@ var require_dist4 = __commonJS({
129379
130285
  throw new Error(`@supabase/supabase-js: Supabase Client is configured with the accessToken option, accessing supabase.auth.${String(prop)} is not possible`);
129380
130286
  } });
129381
130287
  }
129382
- this.fetch = fetchWithAuth2(supabaseKey, supabaseUrl2, this._getAccessToken.bind(this), settings.global.fetch, settings.tracePropagation);
130288
+ this.fetch = fetchWithAuth2(supabaseKey, supabaseUrl, this._getAccessToken.bind(this), settings.global.fetch, settings.tracePropagation);
129383
130289
  this.realtime = this._initRealtimeClient(_objectSpread24({
129384
130290
  headers: this.headers,
129385
130291
  accessToken: this._getAccessToken.bind(this),
@@ -129563,8 +130469,8 @@ var require_dist4 = __commonJS({
129563
130469
  }
129564
130470
  }
129565
130471
  };
129566
- var createClient3 = (supabaseUrl2, supabaseKey, options) => {
129567
- return new SupabaseClient2(supabaseUrl2, supabaseKey, options);
130472
+ var createClient3 = (supabaseUrl, supabaseKey, options) => {
130473
+ return new SupabaseClient2(supabaseUrl, supabaseKey, options);
129568
130474
  };
129569
130475
  function shouldShowDeprecationWarning2() {
129570
130476
  if (typeof window !== "undefined") return false;
@@ -130625,13 +131531,13 @@ var require_createBrowserClient = __commonJS({
130625
131531
  var cookies_1 = require_cookies();
130626
131532
  var warnDeprecatedPackage_1 = require_warnDeprecatedPackage();
130627
131533
  var cachedBrowserClient;
130628
- function createBrowserClient2(supabaseUrl2, supabaseKey, options) {
131534
+ function createBrowserClient2(supabaseUrl, supabaseKey, options) {
130629
131535
  (0, warnDeprecatedPackage_1.warnIfUsingDeprecatedAuthHelpersPackage)();
130630
131536
  const shouldUseSingleton = options?.isSingleton === true || (!options || !("isSingleton" in options)) && (0, utils_1.isBrowser)();
130631
131537
  if (shouldUseSingleton && cachedBrowserClient) {
130632
131538
  return cachedBrowserClient;
130633
131539
  }
130634
- if (!supabaseUrl2 || !supabaseKey) {
131540
+ if (!supabaseUrl || !supabaseKey) {
130635
131541
  throw new Error(`@supabase/ssr: Your project's URL and API key are required to create a Supabase client!
130636
131542
 
130637
131543
  Check your Supabase project's API settings to find these values
@@ -130642,7 +131548,7 @@ https://supabase.com/dashboard/project/_/settings/api`);
130642
131548
  ...options,
130643
131549
  cookieEncoding: options?.cookieEncoding ?? "base64url"
130644
131550
  }, false);
130645
- const client = (0, supabase_js_1.createClient)(supabaseUrl2, supabaseKey, {
131551
+ const client = (0, supabase_js_1.createClient)(supabaseUrl, supabaseKey, {
130646
131552
  // TODO: resolve type error
130647
131553
  ...options,
130648
131554
  global: {
@@ -130678,15 +131584,15 @@ var require_createServerClient = __commonJS({
130678
131584
  "node_modules/@supabase/ssr/dist/main/createServerClient.js"(exports2) {
130679
131585
  "use strict";
130680
131586
  Object.defineProperty(exports2, "__esModule", { value: true });
130681
- exports2.createServerClient = createServerClient2;
131587
+ exports2.createServerClient = createServerClient;
130682
131588
  var supabase_js_1 = require_dist4();
130683
131589
  var version_1 = require_version3();
130684
131590
  var cookies_1 = require_cookies();
130685
131591
  var helpers_1 = require_helpers2();
130686
131592
  var warnDeprecatedPackage_1 = require_warnDeprecatedPackage();
130687
- function createServerClient2(supabaseUrl2, supabaseKey, options) {
131593
+ function createServerClient(supabaseUrl, supabaseKey, options) {
130688
131594
  (0, warnDeprecatedPackage_1.warnIfUsingDeprecatedAuthHelpersPackage)();
130689
- if (!supabaseUrl2 || !supabaseKey) {
131595
+ if (!supabaseUrl || !supabaseKey) {
130690
131596
  throw new Error(`Your project's URL and Key are required to create a Supabase client!
130691
131597
 
130692
131598
  Check your Supabase project's API settings to find these values
@@ -130697,7 +131603,7 @@ https://supabase.com/dashboard/project/_/settings/api`);
130697
131603
  ...options,
130698
131604
  cookieEncoding: options?.cookieEncoding ?? "base64url"
130699
131605
  }, true);
130700
- const client = (0, supabase_js_1.createClient)(supabaseUrl2, supabaseKey, {
131606
+ const client = (0, supabase_js_1.createClient)(supabaseUrl, supabaseKey, {
130701
131607
  // TODO: resolve type error
130702
131608
  ...options,
130703
131609
  global: {
@@ -130776,36 +131682,6 @@ var require_main4 = __commonJS({
130776
131682
  }
130777
131683
  });
130778
131684
 
130779
- // lib/supabase/factory.ts
130780
- function supabaseUrl() {
130781
- return process.env.NEXT_PUBLIC_SUPABASE_URL;
130782
- }
130783
- function supabaseAnonKey() {
130784
- return process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
130785
- }
130786
- function authCookieOptions() {
130787
- const secure = process.env.NODE_ENV === "production";
130788
- return {
130789
- cookieOptions: {
130790
- path: "/",
130791
- sameSite: "lax",
130792
- ...secure ? { secure: true } : {}
130793
- }
130794
- };
130795
- }
130796
- function createBrowserSupabaseClient() {
130797
- return (0, import_ssr.createBrowserClient)(supabaseUrl(), supabaseAnonKey(), {
130798
- isSingleton: true,
130799
- ...authCookieOptions()
130800
- });
130801
- }
130802
- var import_ssr;
130803
- var init_factory = __esm({
130804
- "lib/supabase/factory.ts"() {
130805
- import_ssr = __toESM(require_main4());
130806
- }
130807
- });
130808
-
130809
131685
  // lib/supabase/client.ts
130810
131686
  function createClient2() {
130811
131687
  if (!isSupabaseConfigured()) {
@@ -130814,15 +131690,19 @@ function createClient2() {
130814
131690
  );
130815
131691
  }
130816
131692
  if (!browserClient) {
130817
- browserClient = createBrowserSupabaseClient();
131693
+ browserClient = (0, import_ssr.createBrowserClient)(
131694
+ process.env.NEXT_PUBLIC_SUPABASE_URL,
131695
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
131696
+ { isSingleton: true }
131697
+ );
130818
131698
  }
130819
131699
  return browserClient;
130820
131700
  }
130821
- var browserClient;
131701
+ var import_ssr, browserClient;
130822
131702
  var init_client = __esm({
130823
131703
  "lib/supabase/client.ts"() {
131704
+ import_ssr = __toESM(require_main4());
130824
131705
  init_config();
130825
- init_factory();
130826
131706
  }
130827
131707
  });
130828
131708
 
@@ -139130,7 +140010,6 @@ function offSandboxEvent(runId, handler) {
139130
140010
  var LOCAL_WORKSPACE_ACCESS_UNAVAILABLE;
139131
140011
  var init_bridge = __esm({
139132
140012
  "features/perchTerminal/desktop/bridge.ts"() {
139133
- "use strict";
139134
140013
  LOCAL_WORKSPACE_ACCESS_UNAVAILABLE = "Local workspace access is unavailable. Open Perch Desktop or update the app.";
139135
140014
  }
139136
140015
  });
@@ -198993,10 +199872,10 @@ function evaluateBrowserAction(action, observation, context = {}) {
198993
199872
  if (action.kind === "click" && !label && !("role" in action && action.role)) {
198994
199873
  return block("empty_click_target", "Refusing a browser click with no target. Observe the page and name a button/link, or use a coordinate (visual) click.");
198995
199874
  }
198996
- if ((action.kind === "click" || action.kind === "visual_click" || action.kind === "type") && context.recentFailedClickSignatures?.includes(clickSignature(action))) {
199875
+ if ((action.kind === "click" || action.kind === "visual_click") && context.recentFailedClickSignatures?.includes(clickSignature(action))) {
198997
199876
  return block(
198998
199877
  "repeated_failed_click",
198999
- `The action "${label || action.kind}" already failed on this surface. Not retrying the identical click. Try a different target or report a blocker.`
199878
+ `The action "${label || action.kind}" already failed on this surface as a semantic/DOM click. Not retrying the identical semantic click. Escalate to a coordinate/visual (CUA) click on the same target before reporting a blocker.`
199000
199879
  );
199001
199880
  }
199002
199881
  if (action.kind === "click" && (observation.app === "google_docs" || observation.app === "google_sheets")) {
@@ -199361,6 +200240,17 @@ async function safeBrowserAction(input, runtime = {}) {
199361
200240
  if (target.kind === "ambiguous") {
199362
200241
  lastFailure = fail("selector_ambiguous", target.message, startMs, latestSnapshot.text, latestSnapshot.url, target.candidates);
199363
200242
  } else if (target.kind === "missing") {
200243
+ const escalated = await escalateMissingClickToCoordinate({
200244
+ input,
200245
+ snapshot: latestSnapshot,
200246
+ callTool,
200247
+ startMs,
200248
+ runtime
200249
+ });
200250
+ if (escalated) {
200251
+ clearPolicyClickFailure(input, runtime, latestSnapshot);
200252
+ return escalated;
200253
+ }
199364
200254
  lastFailure = fail("selector_not_found", target.message, startMs, latestSnapshot.text, latestSnapshot.url);
199365
200255
  } else {
199366
200256
  const actionResult = await executeBrowserAction(input, target.target, callTool);
@@ -199497,7 +200387,7 @@ function enforceActionPolicy(input, preSnapshot, runtime, startMs) {
199497
200387
  }
199498
200388
  function recordPolicyClickFailure(input, runtime, snapshot) {
199499
200389
  if (!runtime.policy) return;
199500
- if (input.intent !== "click" && input.intent !== "visual_click" && input.intent !== "type") return;
200390
+ if (input.intent !== "click" && input.intent !== "visual_click") return;
199501
200391
  const key = policySessionKey(runtime);
199502
200392
  const signature = `${policySurfaceFingerprint(snapshot)}::${clickSignature(inputToProposedAction(input))}`;
199503
200393
  const existing = failedClickMemory.get(key) ?? [];
@@ -199609,6 +200499,7 @@ async function clickGmailComposeSendButton(callTool) {
199609
200499
  };
199610
200500
  });
199611
200501
  if (!target || target.result !== "target") return JSON.stringify(target || { result: "missing" });
200502
+ await page.evaluate(${PERCH_CURSOR_OVERLAY_SNIPPET}, { x: target.x, y: target.y });
199612
200503
  await page.mouse.click(target.x, target.y);
199613
200504
  return JSON.stringify({ result: "clicked", label: target.label, x: target.x, y: target.y, bounds: target.bounds, viewport: target.viewport });
199614
200505
  }
@@ -199669,7 +200560,8 @@ async function dispatchSyntheticPaste(payload, runtime) {
199669
200560
  () => {
199670
200561
  const html = ${JSON.stringify(payload.html)};
199671
200562
  const plain = ${JSON.stringify(payload.plain)};
199672
- const active = document.activeElement;
200563
+ const deepActive = () => { let n = document.activeElement; while (n && n.shadowRoot && n.shadowRoot.activeElement) n = n.shadowRoot.activeElement; return n; };
200564
+ const active = deepActive();
199673
200565
  if (!active) return false;
199674
200566
  const isLongBody = plain.length > ${TYPE_DIRECT_MAX_CHARS} || plain.includes("\\n");
199675
200567
  if (isLongBody && active.tagName === "INPUT") return false;
@@ -199987,7 +200879,26 @@ async function resolveTargetForAction(input, snapshot, callTool) {
199987
200879
  }
199988
200880
  const resolved = resolveTarget(snapshot.text, input.targetDescriptor);
199989
200881
  if (resolved.kind === "found") return resolved;
199990
- if (input.intent === "type") return { kind: "found", target: null };
200882
+ if (input.intent === "type") {
200883
+ const desc = input.targetDescriptor;
200884
+ const named = (desc?.text ?? "").trim() || (desc?.ariaLabel ?? "").trim();
200885
+ if (named) {
200886
+ return {
200887
+ kind: "found",
200888
+ target: {
200889
+ kind: "semantic",
200890
+ descriptor: {
200891
+ ...desc?.text ? { text: desc.text } : {},
200892
+ ...desc?.ariaLabel ? { ariaLabel: desc.ariaLabel } : {},
200893
+ ...desc?.role ? { role: desc.role } : {},
200894
+ nth: normalizeNth(desc?.nth)
200895
+ },
200896
+ matched: desc?.text ?? desc?.ariaLabel ?? "target"
200897
+ }
200898
+ };
200899
+ }
200900
+ return { kind: "found", target: null };
200901
+ }
199991
200902
  return resolved;
199992
200903
  }
199993
200904
  function resolveTarget(snapshot, descriptor) {
@@ -200014,7 +200925,7 @@ function resolveTarget(snapshot, descriptor) {
200014
200925
  if (!match) {
200015
200926
  return {
200016
200927
  kind: "missing",
200017
- message: `No browser element matched "${desc.text ?? desc.ariaLabel ?? desc.role ?? "target"}" at nth=${nth}.`
200928
+ message: `No DOM match for "${desc.text ?? desc.ariaLabel ?? desc.role ?? "target"}" at nth=${nth}. Escalate to a coordinate/visual (CUA) click on the same target before reporting a blocker.`
200018
200929
  };
200019
200930
  }
200020
200931
  if (matches.length > 1 && desc.nth === void 0) {
@@ -200046,7 +200957,7 @@ function resolveTarget(snapshot, descriptor) {
200046
200957
  }
200047
200958
  return {
200048
200959
  kind: "missing",
200049
- message: `No browser element matched "${desc.text ?? desc.ariaLabel ?? desc.role ?? "target"}".`
200960
+ message: `No DOM match for "${desc.text ?? desc.ariaLabel ?? desc.role ?? "target"}". Escalate to a coordinate/visual (CUA) click on the same target before reporting a blocker.`
200050
200961
  };
200051
200962
  }
200052
200963
  function liveResolverSource() {
@@ -200095,17 +201006,50 @@ function liveResolverSource() {
200095
201006
  el.textContent,
200096
201007
  ].filter(Boolean).map((v) => String(v).trim()).find(Boolean) || "";
200097
201008
  };
200098
- const candidates = () => Array.from(document.querySelectorAll([
200099
- "button",
200100
- "a[href]",
200101
- "input",
200102
- "textarea",
200103
- "select",
200104
- "[role]",
200105
- "[aria-label]",
200106
- "[data-tooltip]",
200107
- "[contenteditable='true']"
200108
- ].join(","))).filter(visible);
201009
+ const candidates = () => {
201010
+ const INTERACTIVE_SELECTOR = [
201011
+ "button",
201012
+ "a[href]",
201013
+ "input",
201014
+ "textarea",
201015
+ "select",
201016
+ "[role]",
201017
+ "[aria-label]",
201018
+ "[data-tooltip]",
201019
+ "[contenteditable='true']"
201020
+ ].join(",");
201021
+ // Collect open shadow roots recursively so the resolver reaches controls
201022
+ // inside web components (e.g. Reddit/shreddit). Shadow-DOM elements share
201023
+ // the top document's viewport coordinate space, so getBoundingClientRect
201024
+ // (used for the coordinate click) stays correct. Same-origin iframes are
201025
+ // intentionally skipped: their rects are in the frame's own space and
201026
+ // would misplace the click.
201027
+ const roots = [document];
201028
+ const seenRoots = new Set();
201029
+ const walk = (root) => {
201030
+ let hosts;
201031
+ try { hosts = root.querySelectorAll("*"); } catch (e) { return; }
201032
+ for (const host of hosts) {
201033
+ const sr = host.shadowRoot;
201034
+ if (sr && !seenRoots.has(sr)) {
201035
+ seenRoots.add(sr);
201036
+ roots.push(sr);
201037
+ walk(sr);
201038
+ }
201039
+ }
201040
+ };
201041
+ walk(document);
201042
+ const out = [];
201043
+ const seenEl = new Set();
201044
+ for (const root of roots) {
201045
+ let found;
201046
+ try { found = root.querySelectorAll(INTERACTIVE_SELECTOR); } catch (e) { continue; }
201047
+ for (const el of found) {
201048
+ if (!seenEl.has(el) && visible(el)) { seenEl.add(el); out.push(el); }
201049
+ }
201050
+ }
201051
+ return out;
201052
+ };
200109
201053
  const rankMatch = (el, descriptor) => {
200110
201054
  const label = labelFor(el);
200111
201055
  const aria = el.getAttribute("aria-label") || "";
@@ -200143,6 +201087,32 @@ function liveResolverSource() {
200143
201087
  };
200144
201088
  `;
200145
201089
  }
201090
+ async function escalateMissingClickToCoordinate(args) {
201091
+ const { input, snapshot, callTool, startMs, runtime } = args;
201092
+ if (input.intent !== "click") return null;
201093
+ const descriptor = input.targetDescriptor;
201094
+ const label = descriptor?.text ?? descriptor?.ariaLabel ?? descriptor?.role;
201095
+ if (!descriptor || !label) return null;
201096
+ const coordinateTarget = { kind: "semantic", descriptor, matched: label };
201097
+ const clicked = await clickTargetTrusted(callTool, coordinateTarget, label);
201098
+ if (!clicked.ok) return null;
201099
+ const outcome = await waitForExpectedOutcome({
201100
+ input,
201101
+ callTool,
201102
+ preSnapshot: snapshot,
201103
+ startMs,
201104
+ signal: runtime.signal
201105
+ });
201106
+ if (!outcome.ok) return null;
201107
+ return {
201108
+ ...outcome,
201109
+ evidence: {
201110
+ ...outcome.evidence,
201111
+ matched: `${clicked.matched} \xB7 escalated to coordinate (CUA) click`,
201112
+ ...clicked.actionGeometry ? { actionGeometry: clicked.actionGeometry } : {}
201113
+ }
201114
+ };
201115
+ }
200146
201116
  async function executeBrowserAction(input, target, callTool) {
200147
201117
  if (input.intent === "snapshot" || input.intent === "extract") return { ok: true, matched: input.intent };
200148
201118
  if (input.intent === "navigate") {
@@ -200171,6 +201141,8 @@ async function executeBrowserAction(input, target, callTool) {
200171
201141
  if (!clicked.ok) return clicked;
200172
201142
  const typed = await typeViaRealKeys(callTool, input.text ?? "", clicked.matched);
200173
201143
  if (typed.ok) {
201144
+ const verified = await verifyTypedTextLanded(callTool, input.text ?? "", clicked.matched);
201145
+ if (!verified.ok) return verified;
200174
201146
  return {
200175
201147
  ok: true,
200176
201148
  matched: `${clicked.matched} then typed into focused surface`,
@@ -200180,7 +201152,10 @@ async function executeBrowserAction(input, target, callTool) {
200180
201152
  return typed;
200181
201153
  }
200182
201154
  if (input.intent === "type") {
200183
- return enterText(callTool, target, input.text ?? "");
201155
+ const typed = await enterText(callTool, target, input.text ?? "");
201156
+ if (!typed.ok) return typed;
201157
+ const verified = await verifyTypedTextLanded(callTool, input.text ?? "", typed.matched);
201158
+ return verified.ok ? typed : verified;
200184
201159
  }
200185
201160
  if (!target) return { ok: false, error: "target could not be resolved" };
200186
201161
  if (target.kind === "selector") {
@@ -200191,8 +201166,24 @@ async function executeBrowserAction(input, target, callTool) {
200191
201166
  }
200192
201167
  return { ok: false, error: `Unsupported browser intent: ${input.intent}` };
200193
201168
  }
201169
+ function labelGroundingVariants(label) {
201170
+ const trimmed = (label ?? "").trim();
201171
+ if (!trimmed || trimmed.toLowerCase() === "visual target") return [];
201172
+ const variants = [trimmed];
201173
+ const stripped = trimmed.replace(/\b(button|btn|field|input|textbox|tab|link|icon|menu|option|box|area|control)\b/gi, "").replace(/\s{2,}/g, " ").trim();
201174
+ if (stripped && stripped.toLowerCase() !== trimmed.toLowerCase()) variants.push(stripped);
201175
+ return variants;
201176
+ }
200194
201177
  async function clickVisualTarget(callTool, visualTarget) {
200195
201178
  if (!visualTarget) return { ok: false, error: "visual target requires x and y" };
201179
+ for (const candidate of labelGroundingVariants(visualTarget.label)) {
201180
+ const grounded = await clickTargetTrusted(
201181
+ callTool,
201182
+ { kind: "semantic", descriptor: { text: candidate }, matched: candidate },
201183
+ candidate
201184
+ );
201185
+ if (grounded.ok) return grounded;
201186
+ }
200196
201187
  const x = Number(visualTarget.x);
200197
201188
  const y = Number(visualTarget.y);
200198
201189
  if (!Number.isFinite(x) || !Number.isFinite(y)) {
@@ -200210,6 +201201,7 @@ async function clickVisualTarget(callTool, visualTarget) {
200210
201201
  const py = coordinateSpace === "viewport_pixels" ? rawY : rawY * viewport.height;
200211
201202
  const safeX = Math.max(1, Math.min(viewport.width - 1, px));
200212
201203
  const safeY = Math.max(1, Math.min(viewport.height - 1, py));
201204
+ await page.evaluate(${PERCH_CURSOR_OVERLAY_SNIPPET}, { x: safeX, y: safeY });
200213
201205
  await page.mouse.click(safeX, safeY);
200214
201206
  return JSON.stringify({
200215
201207
  result: "clicked",
@@ -200233,17 +201225,78 @@ async function clickVisualTarget(callTool, visualTarget) {
200233
201225
  }
200234
201226
  return { ok: false, error: text || "visual click failed" };
200235
201227
  }
201228
+ async function verifyTypedTextLanded(callTool, text, matched) {
201229
+ if (!text.trim()) return { ok: true };
201230
+ const expr = `() => {
201231
+ const url = String(location.href || "");
201232
+ if (/docs\\.google\\.com\\/(document|spreadsheets|presentation)\\//i.test(url)) return "PERCH_DOCS_BLIND";
201233
+ const deepActive = () => { let n = document.activeElement; while (n && n.shadowRoot && n.shadowRoot.activeElement) n = n.shadowRoot.activeElement; return n; };
201234
+ const el = deepActive();
201235
+ if (!el) return "PERCH_UNKNOWN";
201236
+ const tag = (el.tagName || "").toLowerCase();
201237
+ const readableEditable =
201238
+ el.isContentEditable ||
201239
+ tag === "textarea" ||
201240
+ (tag === "input") ||
201241
+ ("value" in el && typeof el.value === "string");
201242
+ if (!readableEditable) return "PERCH_UNKNOWN";
201243
+ const read = (n) => {
201244
+ if ("value" in n && typeof n.value === "string") return n.value;
201245
+ if (n.isContentEditable) return n.textContent || "";
201246
+ return "";
201247
+ };
201248
+ const val = String(read(el) || "").replace(/\\s+/g, " ").trim();
201249
+ return val ? "PERCH_LANDED" : "PERCH_EMPTY";
201250
+ }`;
201251
+ const result2 = await callTool({
201252
+ serverName: "playwright",
201253
+ toolName: "browser_evaluate",
201254
+ args: { function: expr }
201255
+ });
201256
+ if (!result2.ok) return { ok: true };
201257
+ if (firstText(result2.content).trim() !== "PERCH_EMPTY") return { ok: true };
201258
+ return {
201259
+ ok: false,
201260
+ error: `Typed text did not land in "${matched}" \u2014 the focused field is still empty. Click the field with a visual_click to focus the real editable, then type again (typing goes into the focused field). Do not repeat the identical type into an unfocused field.`
201261
+ };
201262
+ }
201263
+ async function hasEditableFocus(callTool) {
201264
+ const expr = `() => {
201265
+ const deepActive = () => { let n = document.activeElement; while (n && n.shadowRoot && n.shadowRoot.activeElement) n = n.shadowRoot.activeElement; return n; };
201266
+ const el = deepActive();
201267
+ if (!el) return false;
201268
+ if (el.isContentEditable) return true;
201269
+ const tag = (el.tagName || "").toLowerCase();
201270
+ if (tag === "textarea") return true;
201271
+ if (tag === "input") {
201272
+ const t = (el.getAttribute("type") || "text").toLowerCase();
201273
+ return !["button","submit","reset","checkbox","radio","range","file","image","color"].includes(t);
201274
+ }
201275
+ if ("value" in el && typeof el.value === "string") return true;
201276
+ return false;
201277
+ }`;
201278
+ const result2 = await callTool({
201279
+ serverName: "playwright",
201280
+ toolName: "browser_evaluate",
201281
+ args: { function: expr }
201282
+ });
201283
+ if (!result2.ok) return false;
201284
+ return /^\s*true\s*$/i.test(firstText(result2.content));
201285
+ }
200236
201286
  async function enterText(callTool, target, text) {
200237
201287
  const matched = target?.matched ?? "focused field";
200238
201288
  const isLong = text.length > TYPE_DIRECT_MAX_CHARS || text.includes("\n");
201289
+ const alreadyFocused = await hasEditableFocus(callTool);
200239
201290
  if (!isLong) {
200240
- if (target) {
201291
+ let focusGeometry;
201292
+ if (!alreadyFocused && target) {
200241
201293
  const focused = await focusTarget(callTool, target, matched);
200242
- if (!focused.ok) return focused;
201294
+ if (focused.ok) focusGeometry = focused.actionGeometry;
200243
201295
  }
200244
- return typeViaRealKeys(callTool, text, matched);
201296
+ const typed = await typeViaRealKeys(callTool, text, matched);
201297
+ return typed.ok && focusGeometry ? { ...typed, actionGeometry: focusGeometry } : typed;
200245
201298
  }
200246
- if (target) {
201299
+ if (!alreadyFocused && target) {
200247
201300
  const focused = await focusTarget(callTool, target, matched);
200248
201301
  if (focused.ok) {
200249
201302
  const pasted2 = await dispatchSyntheticPaste({ html: null, plain: text }, { callTool });
@@ -200271,7 +201324,8 @@ function shouldRefuseRealKeys(error) {
200271
201324
  async function typeIntoActiveElement(callTool, text) {
200272
201325
  const json = JSON.stringify(text);
200273
201326
  const expr = `() => {
200274
- const el = document.activeElement;
201327
+ const deepActive = () => { let n = document.activeElement; while (n && n.shadowRoot && n.shadowRoot.activeElement) n = n.shadowRoot.activeElement; return n; };
201328
+ const el = deepActive();
200275
201329
  if (!el) throw new Error("no active element to type into");
200276
201330
  const isLongBody = ${JSON.stringify(text.length > TYPE_DIRECT_MAX_CHARS || text.includes("\n"))};
200277
201331
  if (isLongBody && el.tagName === "INPUT") {
@@ -200357,6 +201411,7 @@ async function clickTargetTrusted(callTool, target, matched) {
200357
201411
  };
200358
201412
  }, { descriptor: ${JSON.stringify(descriptor)}, selector: ${JSON.stringify(selector)} });
200359
201413
  if (!target || target.result !== "target") return JSON.stringify(target || { result: "missing" });
201414
+ await page.evaluate(${PERCH_CURSOR_OVERLAY_SNIPPET}, { x: target.x, y: target.y });
200360
201415
  await page.mouse.click(target.x, target.y);
200361
201416
  return JSON.stringify({ result: "clicked", label: target.label, x: target.x, y: target.y, bounds: target.bounds, viewport: target.viewport });
200362
201417
  }`;
@@ -200684,7 +201739,7 @@ function delay(ms, signal) {
200684
201739
  );
200685
201740
  });
200686
201741
  }
200687
- var DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS, SNAPSHOT_REF_PREFIX, HUMAN_RECOVERY_FAILURES, MODAL_DISMISSERS, failedClickMemory, FAILED_CLICK_MEMORY_MAX, TYPE_DIRECT_MAX_CHARS, PRESS_KEY_LOOP_MAX_CHARS;
201742
+ var DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS, SNAPSHOT_REF_PREFIX, HUMAN_RECOVERY_FAILURES, MODAL_DISMISSERS, failedClickMemory, FAILED_CLICK_MEMORY_MAX, PERCH_CURSOR_OVERLAY_SNIPPET, TYPE_DIRECT_MAX_CHARS, PRESS_KEY_LOOP_MAX_CHARS;
200688
201743
  var init_safeBrowserAction = __esm({
200689
201744
  "features/perchTerminal/runtime/playwright/safeBrowserAction.ts"() {
200690
201745
  "use strict";
@@ -200711,6 +201766,26 @@ var init_safeBrowserAction = __esm({
200711
201766
  ];
200712
201767
  failedClickMemory = /* @__PURE__ */ new Map();
200713
201768
  FAILED_CLICK_MEMORY_MAX = 12;
201769
+ PERCH_CURSOR_OVERLAY_SNIPPET = `(coord) => {
201770
+ try {
201771
+ const id = "__perch_operator_cursor__";
201772
+ let el = document.getElementById(id);
201773
+ if (!el) {
201774
+ el = document.createElement("div");
201775
+ el.id = id;
201776
+ el.setAttribute("aria-hidden", "true");
201777
+ el.style.cssText = "position:fixed;left:0;top:0;z-index:2147483647;pointer-events:none;width:24px;height:30px;transform-origin:0 0;transition:left .1s ease,top .1s ease,opacity .4s ease;filter:drop-shadow(0 1px 2px rgba(0,0,0,.6));";
201778
+ el.innerHTML = '<svg width="24" height="30" viewBox="0 0 16 20" xmlns="http://www.w3.org/2000/svg"><path d="M0 0 L0 15 L4.5 11.6 L6.9 17.2 L9.2 16.2 L6.8 10.8 L11.8 10.8 Z" fill="#111827" stroke="#ffffff" stroke-width="1.4" stroke-linejoin="round"/></svg>';
201779
+ (document.body || document.documentElement).appendChild(el);
201780
+ }
201781
+ // The arrow tip is at the SVG origin (0,0), so left/top place the tip on the point.
201782
+ el.style.left = coord.x + "px";
201783
+ el.style.top = coord.y + "px";
201784
+ el.style.opacity = "1";
201785
+ if (el.__perchHide) clearTimeout(el.__perchHide);
201786
+ el.__perchHide = setTimeout(function () { el.style.opacity = "0.6"; }, 1500);
201787
+ } catch (e) {}
201788
+ }`;
200714
201789
  TYPE_DIRECT_MAX_CHARS = 200;
200715
201790
  PRESS_KEY_LOOP_MAX_CHARS = 4e3;
200716
201791
  }
@@ -213690,8 +214765,8 @@ var init_barSeries = __esm({
213690
214765
  });
213691
214766
 
213692
214767
  // lib/perchMarketDesk/data/fixtureStore.ts
213693
- import fs9 from "node:fs";
213694
- import path10 from "node:path";
214768
+ import fs10 from "node:fs";
214769
+ import path11 from "node:path";
213695
214770
  function createFixtureStore(input) {
213696
214771
  const instruments = [...input.instruments].sort((a, b2) => a.ticker.localeCompare(b2.ticker));
213697
214772
  const seriesByTicker = /* @__PURE__ */ new Map();
@@ -213738,22 +214813,22 @@ function createFixtureStore(input) {
213738
214813
  };
213739
214814
  }
213740
214815
  function loadFixtureStoreFromDir(dir) {
213741
- const instrumentsPath = path10.join(dir, "instruments.json");
213742
- const instruments = JSON.parse(fs9.readFileSync(instrumentsPath, "utf8"));
214816
+ const instrumentsPath = path11.join(dir, "instruments.json");
214817
+ const instruments = JSON.parse(fs10.readFileSync(instrumentsPath, "utf8"));
213743
214818
  const bars = [];
213744
- const barsDir = path10.join(dir, "bars");
213745
- for (const file of fs9.readdirSync(barsDir)) {
214819
+ const barsDir = path11.join(dir, "bars");
214820
+ for (const file of fs10.readdirSync(barsDir)) {
213746
214821
  if (!file.endsWith(".json")) continue;
213747
- const ticker = path10.basename(file, ".json");
213748
- const rows = JSON.parse(fs9.readFileSync(path10.join(barsDir, file), "utf8"));
214822
+ const ticker = path11.basename(file, ".json");
214823
+ const rows = JSON.parse(fs10.readFileSync(path11.join(barsDir, file), "utf8"));
213749
214824
  for (const row of rows) bars.push({ ...row, ticker: row.ticker ?? ticker });
213750
214825
  }
213751
214826
  let fundamentals;
213752
- const fundamentalsPath = path10.join(dir, "fundamentals.json");
213753
- if (fs9.existsSync(fundamentalsPath)) {
213754
- fundamentals = JSON.parse(fs9.readFileSync(fundamentalsPath, "utf8"));
214827
+ const fundamentalsPath = path11.join(dir, "fundamentals.json");
214828
+ if (fs10.existsSync(fundamentalsPath)) {
214829
+ fundamentals = JSON.parse(fs10.readFileSync(fundamentalsPath, "utf8"));
213755
214830
  }
213756
- return createFixtureStore({ instruments, bars, fundamentals, label: `dir:${path10.basename(dir)}` });
214831
+ return createFixtureStore({ instruments, bars, fundamentals, label: `dir:${path11.basename(dir)}` });
213757
214832
  }
213758
214833
  var init_fixtureStore = __esm({
213759
214834
  "lib/perchMarketDesk/data/fixtureStore.ts"() {
@@ -218835,9 +219910,19 @@ async function executeToolBatch(toolCalls, ctx) {
218835
219910
  { type: "image_url", image_url: { url: execution.imageDataUrl } }
218836
219911
  ]
218837
219912
  });
219913
+ } else if (isOperatorScreenshot) {
219914
+ const readout = await describeOperatorScreenshotViaVision(execution.imageDataUrl, ctx);
219915
+ ctx.messages.push({
219916
+ role: "user",
219917
+ content: readout ? `${OPERATOR_SCREENSHOT_MARKER} Vision readout of the current screen after that action (the selected model is text-only, so a vision sidecar looked for you). Treat this as the source of truth. For a coordinate click, use the approximate x/y viewport fractions named below.
219918
+
219919
+ ${readout}` : `${OPERATOR_SCREENSHOT_MARKER} A current browser screenshot was captured after that action, but this selected model is text-only and the vision sidecar was unavailable. Call visionInspect with {"currentScreen": true, "prompt": "describe what I need to verify"} before any coordinate click. Otherwise continue from the browser snapshot/perception text.`
219920
+ });
218838
219921
  } else {
218839
- const text = isLocalVisualAttachment ? `${LOCAL_VISUAL_ATTACHMENT_MARKER} A visual preview for ${label} was captured, but this selected model is text-only. If visual details matter, call visionInspect on the same file path/localSourceId with {"prompt": "read this file visually"}. Otherwise continue from any extracted text in the tool result.` : `${OPERATOR_SCREENSHOT_MARKER} A current browser screenshot was captured after that action, but this selected model is text-only. If visual details matter, call visionInspect with {"currentScreen": true, "prompt": "describe what I need to verify"}. Otherwise continue from the browser snapshot/perception text.`;
218840
- ctx.messages.push({ role: "user", content: text });
219922
+ ctx.messages.push({
219923
+ role: "user",
219924
+ content: `${LOCAL_VISUAL_ATTACHMENT_MARKER} A visual preview for ${label} was captured, but this selected model is text-only. If visual details matter, call visionInspect on the same file path/localSourceId with {"prompt": "read this file visually"}. Otherwise continue from any extracted text in the tool result.`
219925
+ });
218841
219926
  }
218842
219927
  }
218843
219928
  }
@@ -218858,6 +219943,26 @@ function parseImageDataUrlForTool(dataUrl) {
218858
219943
  base64
218859
219944
  };
218860
219945
  }
219946
+ async function describeOperatorScreenshotViaVision(dataUrl, ctx) {
219947
+ if (ctx.signal?.aborted) return null;
219948
+ const parsed = parseImageDataUrlForTool(dataUrl);
219949
+ if (!parsed) return null;
219950
+ try {
219951
+ const result2 = await callVisionLaneModel({
219952
+ founderModelSelection: ctx.founderModelSelection,
219953
+ imageBase64: parsed.base64,
219954
+ mimeType: parsed.mimeType,
219955
+ userPrompt: OPERATOR_VISION_OBSERVER_PROMPT,
219956
+ signal: ctx.signal ?? void 0,
219957
+ userId: ctx.userId ?? null,
219958
+ workspaceId: ctx.workspaceId ?? null,
219959
+ runId: ctx.runId ?? null
219960
+ });
219961
+ if (result2.ok && result2.text.trim()) return result2.text.trim();
219962
+ } catch {
219963
+ }
219964
+ return null;
219965
+ }
218861
219966
  function currentScreenVisionToolCall(input) {
218862
219967
  const { toolCall, imageDataUrl } = input;
218863
219968
  if (toolCall.name !== TOOL_NAMES.visionInspect) return { toolCall };
@@ -218916,11 +220021,12 @@ function updateConsecutiveRequiredArgFailures(counts, execution) {
218916
220021
  function now6() {
218917
220022
  return (/* @__PURE__ */ new Date()).toISOString();
218918
220023
  }
218919
- var MAX_PARALLEL_TOOL_CALLS, LOCAL_VISUAL_ATTACHMENT_MARKER;
220024
+ var MAX_PARALLEL_TOOL_CALLS, LOCAL_VISUAL_ATTACHMENT_MARKER, OPERATOR_VISION_OBSERVER_PROMPT;
218920
220025
  var init_toolBatchExecutor = __esm({
218921
220026
  "features/perchTerminal/runtime/toolLoop/toolBatchExecutor.ts"() {
218922
220027
  "use strict";
218923
220028
  init_modelRegistry();
220029
+ init_visionLaneCall();
218924
220030
  init_toolExecutor();
218925
220031
  init_fileDiff();
218926
220032
  init_toolNames();
@@ -218933,6 +220039,14 @@ var init_toolBatchExecutor = __esm({
218933
220039
  init_sourceReadPolicy();
218934
220040
  MAX_PARALLEL_TOOL_CALLS = 8;
218935
220041
  LOCAL_VISUAL_ATTACHMENT_MARKER = "[local-visual]";
220042
+ OPERATOR_VISION_OBSERVER_PROMPT = [
220043
+ "You are the eyes of a browser operator that cannot see this screen.",
220044
+ "Describe the current browser screen so the operator can act precisely.",
220045
+ "List the key interactive controls (buttons, links, text fields, tabs, menus) with their exact visible labels and approximate position as viewport fractions x,y in [0,1], where 0,0 is the top-left and 1,1 is the bottom-right.",
220046
+ "State which field or control is focused or selected, the values already entered in visible fields, and any error, confirmation, or toast text.",
220047
+ "If a crosshair or marker is drawn on the image, say which element it is pointing at.",
220048
+ "Plain text only. Do not suggest or decide the next action."
220049
+ ].join(" ");
218936
220050
  }
218937
220051
  });
218938
220052
 
@@ -219377,8 +220491,11 @@ async function prepareLoopMessagesForSend(input) {
219377
220491
  toolDefinitionsJson,
219378
220492
  modelContext
219379
220493
  });
219380
- const contextLimitTokens = resolveEffectiveContextLimit(input.option);
219381
- const compactThreshold = getAutoCompactThreshold(input.option);
220494
+ const budgetModel = typeof input.effectiveContextLimitTokens === "number" && input.effectiveContextLimitTokens > 0 ? { ...input.option, effectiveLimitTokens: input.effectiveContextLimitTokens } : input.option;
220495
+ const contextLimitTokens = resolveEffectiveContextLimit(
220496
+ budgetModel
220497
+ );
220498
+ const compactThreshold = getAutoCompactThreshold(budgetModel);
219382
220499
  const postCompactTargetTokens = resolvePostCompactTargetTokens(contextLimitTokens);
219383
220500
  const recentTailTokenBudget = resolveRecentTailBudgetTokens({
219384
220501
  postCompactTargetTokens,
@@ -219404,11 +220521,6 @@ async function prepareLoopMessagesForSend(input) {
219404
220521
  toolDefinitionsJson,
219405
220522
  modelContext
219406
220523
  });
219407
- input.onEvent?.({
219408
- type: "activity_delta",
219409
- text: `Cleared ${microcompactedToolResults} old tool result(s) to free context.`,
219410
- ts: (/* @__PURE__ */ new Date()).toISOString()
219411
- });
219412
220524
  }
219413
220525
  }
219414
220526
  if (autoCompactEnabled && anchoredTokens(breakdown.total) >= compactThreshold) {
@@ -223195,6 +224307,7 @@ async function runFlockTurn(input, deps, options = {}) {
223195
224307
  emitWorkerUpdate(emit, plan, worker, "queued");
223196
224308
  }
223197
224309
  const spawnFn = options.spawnWorkerFn ?? spawnWorker;
224310
+ let turnInput = input;
223198
224311
  const outcomes = [];
223199
224312
  const revisionOutcomes = [];
223200
224313
  let revisionReport = null;
@@ -223243,6 +224356,7 @@ async function runFlockTurn(input, deps, options = {}) {
223243
224356
  toolCallsReserved += reservedToolCalls;
223244
224357
  emitWorkerUpdate(emit, plan, worker, "running");
223245
224358
  try {
224359
+ turnInput = await refreshFlockCliAuth(turnInput);
223246
224360
  const context = contextOverride ?? buildWorkerContext(worker, sharedContext, outputByFlockWorkerId, plan);
223247
224361
  const objective = worker.role === "worker" && contextContainsSources(context) ? `${worker.objective}
223248
224362
  ${FLOCK_GROUNDING_CONTRACT}` : worker.objective;
@@ -223254,18 +224368,18 @@ ${FLOCK_GROUNDING_CONTRACT}` : worker.objective;
223254
224368
  maxIterations: worker.maxIterations,
223255
224369
  maxToolCalls: reservedToolCalls
223256
224370
  },
223257
- buildSpawnContext(input, plan.flockId, flockRun.controller.signal, emit, worker)
224371
+ buildSpawnContext(turnInput, plan.flockId, flockRun.controller.signal, emit, worker)
223258
224372
  );
223259
224373
  toolCallsReserved -= reservedToolCalls;
223260
224374
  toolCallsUsed += Math.min(result2.toolCalls ?? 0, reservedToolCalls);
223261
- const ok = result2.ok;
224375
+ const status = flockWorkerStatusFromSpawnResult(result2);
223262
224376
  const outcome = {
223263
224377
  worker,
223264
224378
  result: result2,
223265
- status: ok ? "done" : "failed",
224379
+ status,
223266
224380
  detail: clampLine(result2.summary, 200)
223267
224381
  };
223268
- emitWorkerUpdate(emit, plan, worker, ok ? "done" : "failed", clampLine(result2.summary, 160));
224382
+ emitWorkerUpdate(emit, plan, worker, status, clampLine(result2.summary, 160));
223269
224383
  return outcome;
223270
224384
  } catch (error) {
223271
224385
  toolCallsReserved -= reservedToolCalls;
@@ -223365,8 +224479,10 @@ ${FLOCK_GROUNDING_CONTRACT}` : worker.objective;
223365
224479
  }
223366
224480
  const userCancelled = Boolean(deps.signal?.aborted);
223367
224481
  const workersDone = outcomes.filter((outcome) => outcome.status === "done").length;
223368
- const workersFailed = outcomes.filter((outcome) => outcome.status === "failed").length;
224482
+ const workersBlocked = outcomes.filter((outcome) => outcome.status === "blocked").length;
224483
+ const workersFailed = outcomes.filter((outcome) => outcome.status === "failed").length + workersBlocked;
223369
224484
  const flockStatus = userCancelled || wallTimeHit && workersDone === 0 ? "cancelled" : workersFailed === 0 && workersDone === plan.workers.length ? "completed" : workersDone > 0 ? "partial" : "failed";
224485
+ const permissionHint = workersBlocked > 0 && (turnInput.permissionMode ?? "default") === "default" ? "\n\nTip: flock workers inherit default permissions \u2014 run `/permission take_the_wheel` before `/flock` so bash redirects and file writes do not stall on approval gates." : "";
223370
224486
  const assistantText = [
223371
224487
  buildFlockSummary(
223372
224488
  plan,
@@ -223382,7 +224498,7 @@ ${FLOCK_GROUNDING_CONTRACT}` : worker.objective;
223382
224498
  ...modelOverrides.unavailable.map(
223383
224499
  (override) => `Requested model "${override.modelText}"${override.roleText ? ` for ${override.roleText}` : ""} is not available \u2014 that worker stayed on the default model path.`
223384
224500
  )
223385
- ].join("\n");
224501
+ ].join("\n") + permissionHint;
223386
224502
  emit({
223387
224503
  type: "flock_run_completed",
223388
224504
  flockId: plan.flockId,
@@ -223616,6 +224732,12 @@ function buildFlockSummary(plan, outcomes, status, toolCallsUsed, wallTimeHit) {
223616
224732
  function friendlyOutcomeDetail(detail) {
223617
224733
  const clean = clampLine(detail, 220);
223618
224734
  if (!clean) return "";
224735
+ if (/approval gate|requires approval|Blocked at an approval/i.test(clean)) {
224736
+ return clean;
224737
+ }
224738
+ if (/No usable model response|Model proxy error|not_authenticated|Unauthorized/i.test(clean)) {
224739
+ return clean;
224740
+ }
223619
224741
  if (/Reached maximum \d+ tool-call iterations/i.test(clean)) {
223620
224742
  return "finished from gathered evidence";
223621
224743
  }
@@ -223634,6 +224756,29 @@ function clampLine(text, max2) {
223634
224756
  function makeFlockRunId() {
223635
224757
  return `flock-run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
223636
224758
  }
224759
+ function flockWorkerStatusFromSpawnResult(result2) {
224760
+ if (!result2.ok || result2.usabilityStatus === "failed") return "failed";
224761
+ if (result2.usabilityStatus === "needs_user_input") return "blocked";
224762
+ return "done";
224763
+ }
224764
+ async function refreshFlockCliAuth(input) {
224765
+ if (input.cliLocalTools !== true) return input;
224766
+ const stored = await readStoredCliAuthSession();
224767
+ if (!stored?.accessToken?.trim()) return input;
224768
+ const turnToken = input.cliServerAccessToken?.trim();
224769
+ if (turnToken && turnToken !== stored.accessToken) return input;
224770
+ const fresh = await ensureFreshCliAuthSession({ session: stored });
224771
+ if (!fresh?.accessToken?.trim()) return input;
224772
+ if (fresh.accessToken === input.cliServerAccessToken) return input;
224773
+ if (typeof process !== "undefined") {
224774
+ process.env.PERCH_MODEL_CALL_PROXY_TOKEN = fresh.accessToken;
224775
+ }
224776
+ return {
224777
+ ...input,
224778
+ cliServerAccessToken: fresh.accessToken,
224779
+ marketDeskProxyAccessToken: input.marketDeskProxyAccessToken ?? fresh.accessToken
224780
+ };
224781
+ }
223637
224782
  function now11() {
223638
224783
  return (/* @__PURE__ */ new Date()).toISOString();
223639
224784
  }
@@ -223644,6 +224789,8 @@ var init_runFlockTurn = __esm({
223644
224789
  init_operatorStream();
223645
224790
  init_runRegistry();
223646
224791
  init_spawnWorker2();
224792
+ init_cliAuthSession();
224793
+ init_cliAuthRefresh();
223647
224794
  init_registry();
223648
224795
  init_flockCommand();
223649
224796
  init_flockLimits();
@@ -223673,6 +224820,12 @@ var init_usageCommand = __esm({
223673
224820
  }
223674
224821
  });
223675
224822
 
224823
+ // lib/perch-ai/entitlements.ts
224824
+ var init_entitlements = __esm({
224825
+ "lib/perch-ai/entitlements.ts"() {
224826
+ }
224827
+ });
224828
+
223676
224829
  // lib/perch-ai/access.ts
223677
224830
  function hasPrivilegedPerchAiRole(memberships) {
223678
224831
  return memberships.some(
@@ -223690,6 +224843,7 @@ function canShowPerchDevTools(membershipRole) {
223690
224843
  }
223691
224844
  var init_access = __esm({
223692
224845
  "lib/perch-ai/access.ts"() {
224846
+ init_entitlements();
223693
224847
  }
223694
224848
  });
223695
224849
 
@@ -231950,9 +233104,9 @@ var init_backgroundTaskTypes = __esm({
231950
233104
  });
231951
233105
 
231952
233106
  // features/perchTerminal/runtime/background/backgroundTaskRegistry.ts
231953
- import fs10 from "node:fs";
231954
- import os from "node:os";
231955
- import path11 from "node:path";
233107
+ import fs11 from "node:fs";
233108
+ import os2 from "node:os";
233109
+ import path12 from "node:path";
231956
233110
  function isTerminalStatus(status) {
231957
233111
  return status !== "running";
231958
233112
  }
@@ -232009,19 +233163,19 @@ function runtimeMs(record) {
232009
233163
  function readOutputPreview(outputPath, previewBytes = BACKGROUND_TASK_BOUNDS.previewBytes) {
232010
233164
  if (!outputPath) return { preview: "", truncated: false };
232011
233165
  try {
232012
- const stat2 = fs10.statSync(outputPath);
232013
- const fd = fs10.openSync(outputPath, "r");
233166
+ const stat2 = fs11.statSync(outputPath);
233167
+ const fd = fs11.openSync(outputPath, "r");
232014
233168
  try {
232015
233169
  if (stat2.size <= previewBytes) {
232016
233170
  const buf2 = Buffer.alloc(stat2.size);
232017
- fs10.readSync(fd, buf2, 0, stat2.size, 0);
233171
+ fs11.readSync(fd, buf2, 0, stat2.size, 0);
232018
233172
  return { preview: buf2.toString("utf8"), truncated: false };
232019
233173
  }
232020
233174
  const buf = Buffer.alloc(previewBytes);
232021
- fs10.readSync(fd, buf, 0, previewBytes, stat2.size - previewBytes);
233175
+ fs11.readSync(fd, buf, 0, previewBytes, stat2.size - previewBytes);
232022
233176
  return { preview: buf.toString("utf8"), truncated: true };
232023
233177
  } finally {
232024
- fs10.closeSync(fd);
233178
+ fs11.closeSync(fd);
232025
233179
  }
232026
233180
  } catch {
232027
233181
  return { preview: "", truncated: false };
@@ -232056,26 +233210,28 @@ function getBackgroundTaskStore() {
232056
233210
  var STORE_FILE, BackgroundTaskStore, singleton;
232057
233211
  var init_backgroundTaskRegistry = __esm({
232058
233212
  "features/perchTerminal/runtime/background/backgroundTaskRegistry.ts"() {
232059
- "use strict";
232060
233213
  init_backgroundTaskTypes();
232061
233214
  STORE_FILE = "background-tasks.json";
232062
233215
  BackgroundTaskStore = class {
233216
+ baseDir;
233217
+ storePath;
233218
+ outputDir;
233219
+ records = /* @__PURE__ */ new Map();
233220
+ loaded = false;
232063
233221
  constructor(options = {}) {
232064
- this.records = /* @__PURE__ */ new Map();
232065
- this.loaded = false;
232066
- this.baseDir = options.baseDir ?? path11.join(os.homedir(), ".perch");
232067
- this.storePath = path11.join(this.baseDir, STORE_FILE);
232068
- this.outputDir = path11.join(this.baseDir, "background-output");
233222
+ this.baseDir = options.baseDir ?? path12.join(os2.homedir(), ".perch");
233223
+ this.storePath = path12.join(this.baseDir, STORE_FILE);
233224
+ this.outputDir = path12.join(this.baseDir, "background-output");
232069
233225
  }
232070
233226
  ensureDirs() {
232071
- fs10.mkdirSync(this.baseDir, { recursive: true });
232072
- fs10.mkdirSync(this.outputDir, { recursive: true });
233227
+ fs11.mkdirSync(this.baseDir, { recursive: true });
233228
+ fs11.mkdirSync(this.outputDir, { recursive: true });
232073
233229
  }
232074
233230
  load() {
232075
233231
  if (this.loaded) return;
232076
233232
  this.loaded = true;
232077
233233
  try {
232078
- const raw = fs10.readFileSync(this.storePath, "utf8");
233234
+ const raw = fs11.readFileSync(this.storePath, "utf8");
232079
233235
  const parsed = JSON.parse(raw);
232080
233236
  const tasks = Array.isArray(parsed.tasks) ? parsed.tasks : [];
232081
233237
  this.records = new Map(tasks.map((t) => [t.taskId, t]));
@@ -232087,11 +233243,11 @@ var init_backgroundTaskRegistry = __esm({
232087
233243
  this.ensureDirs();
232088
233244
  const tasks = Array.from(this.records.values());
232089
233245
  const tmp = `${this.storePath}.tmp`;
232090
- fs10.writeFileSync(tmp, JSON.stringify({ version: 1, tasks }, null, 2), "utf8");
232091
- fs10.renameSync(tmp, this.storePath);
233246
+ fs11.writeFileSync(tmp, JSON.stringify({ version: 1, tasks }, null, 2), "utf8");
233247
+ fs11.renameSync(tmp, this.storePath);
232092
233248
  }
232093
233249
  outputPathFor(taskId) {
232094
- return path11.join(this.outputDir, `${taskId}.log`);
233250
+ return path12.join(this.outputDir, `${taskId}.log`);
232095
233251
  }
232096
233252
  list() {
232097
233253
  this.load();
@@ -232253,14 +233409,14 @@ var init_backgroundWakeController = __esm({
232253
233409
 
232254
233410
  // features/perchTerminal/runtime/background/runBackgroundCommand.ts
232255
233411
  import { spawn as spawn2 } from "node:child_process";
232256
- import fs11 from "node:fs";
232257
- import path12 from "node:path";
233412
+ import fs12 from "node:fs";
233413
+ import path13 from "node:path";
232258
233414
  function newTaskId() {
232259
233415
  return `bg-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
232260
233416
  }
232261
233417
  function createCappedWriter(filePath, maxBytes) {
232262
- fs11.mkdirSync(path12.dirname(filePath), { recursive: true });
232263
- const stream = fs11.createWriteStream(filePath, { flags: "w" });
233418
+ fs12.mkdirSync(path13.dirname(filePath), { recursive: true });
233419
+ const stream = fs12.createWriteStream(filePath, { flags: "w" });
232264
233420
  let written = 0;
232265
233421
  let truncated = false;
232266
233422
  return {
@@ -232433,10 +233589,10 @@ var init_runBackgroundCommand = __esm({
232433
233589
 
232434
233590
  // features/perchTerminal/runtime/cliHost/nodeLocalBridge.ts
232435
233591
  import { spawn as spawn3 } from "node:child_process";
232436
- import fs12 from "node:fs";
233592
+ import fs13 from "node:fs";
232437
233593
  import fsp from "node:fs/promises";
232438
- import os2 from "node:os";
232439
- import path13 from "node:path";
233594
+ import os3 from "node:os";
233595
+ import path14 from "node:path";
232440
233596
  function installCliNodeLocalBridge(input) {
232441
233597
  const globalRef = globalThis;
232442
233598
  const previous = globalRef.__PERCH_TEST_DESKTOP_BRIDGE__;
@@ -232450,7 +233606,7 @@ function installCliNodeLocalBridge(input) {
232450
233606
  };
232451
233607
  }
232452
233608
  function readCliProjectMemoryState(workspaceRoot) {
232453
- const root2 = path13.resolve(expandHome4(workspaceRoot));
233609
+ const root2 = path14.resolve(expandHome4(workspaceRoot));
232454
233610
  if (!isCliProjectMemoryAvailable(root2)) {
232455
233611
  return {
232456
233612
  available: false,
@@ -232465,7 +233621,7 @@ function readCliProjectMemoryState(workspaceRoot) {
232465
233621
  };
232466
233622
  }
232467
233623
  function createCliNodeLocalBridge(input) {
232468
- const workspaceRoot = path13.resolve(input.workspaceRoot);
233624
+ const workspaceRoot = path14.resolve(input.workspaceRoot);
232469
233625
  const now23 = () => (/* @__PURE__ */ new Date()).toISOString();
232470
233626
  const sandboxHandlers = /* @__PURE__ */ new Map();
232471
233627
  const emitSandboxEvent = (event) => {
@@ -232479,7 +233635,7 @@ function createCliNodeLocalBridge(input) {
232479
233635
  cwd: workspaceRoot,
232480
233636
  projectRoot: workspaceRoot,
232481
233637
  shellPath: process.env.SHELL || "/bin/zsh",
232482
- shellName: path13.basename(process.env.SHELL || "zsh"),
233638
+ shellName: path14.basename(process.env.SHELL || "zsh"),
232483
233639
  path: process.env.PATH || "",
232484
233640
  status: "running",
232485
233641
  backend: "spawn-fallback",
@@ -232506,7 +233662,7 @@ function createCliNodeLocalBridge(input) {
232506
233662
  capabilities: ["local-files", "local-shell"]
232507
233663
  }),
232508
233664
  checkFsAccess: async (request) => {
232509
- const absolutePath = path13.resolve(expandHome4(request.absolutePath));
233665
+ const absolutePath = path14.resolve(expandHome4(request.absolutePath));
232510
233666
  return {
232511
233667
  decision: "allow",
232512
233668
  reason: "Allowed by Perch CLI local filesystem access.",
@@ -232524,7 +233680,7 @@ function createCliNodeLocalBridge(input) {
232524
233680
  ok: true,
232525
233681
  directory: workspaceRoot,
232526
233682
  fileName: request.fileName,
232527
- absolutePath: path13.join(workspaceRoot, request.fileName),
233683
+ absolutePath: path14.join(workspaceRoot, request.fileName),
232528
233684
  source: "approved_root"
232529
233685
  }),
232530
233686
  validateWorkspaceRoot: async () => ({
@@ -232613,7 +233769,7 @@ function createCliNodeLocalBridge(input) {
232613
233769
  writeWorkspaceFile: async (request) => {
232614
233770
  const baseRoot = resolveRequestRoot(workspaceRoot, request.workspaceRoot);
232615
233771
  const target = resolveWritePath(baseRoot, request.relativePath);
232616
- await fsp.mkdir(path13.dirname(target), { recursive: true });
233772
+ await fsp.mkdir(path14.dirname(target), { recursive: true });
232617
233773
  const flag = request.overwrite === true ? "w" : "wx";
232618
233774
  const payload = request.encoding === "base64" && typeof request.contentBase64 === "string" ? Buffer.from(request.contentBase64, "base64") : request.content ?? "";
232619
233775
  try {
@@ -232633,14 +233789,14 @@ function createCliNodeLocalBridge(input) {
232633
233789
  moveLocalFile: async (request) => {
232634
233790
  const src = resolveReadPath(workspaceRoot, request.src);
232635
233791
  const dest = resolveWritePath(workspaceRoot, request.dest);
232636
- await fsp.mkdir(path13.dirname(dest), { recursive: true });
233792
+ await fsp.mkdir(path14.dirname(dest), { recursive: true });
232637
233793
  await fsp.rename(src, dest);
232638
233794
  return { ok: true, fromPath: src, toPath: dest };
232639
233795
  },
232640
233796
  copyLocalFile: async (request) => {
232641
233797
  const src = resolveReadPath(workspaceRoot, request.src);
232642
233798
  const dest = resolveWritePath(workspaceRoot, request.dest);
232643
- await fsp.mkdir(path13.dirname(dest), { recursive: true });
233799
+ await fsp.mkdir(path14.dirname(dest), { recursive: true });
232644
233800
  await fsp.copyFile(src, dest);
232645
233801
  return { ok: true, fromPath: src, toPath: dest };
232646
233802
  },
@@ -232665,7 +233821,7 @@ function createCliNodeLocalBridge(input) {
232665
233821
  const pattern = request.pattern?.trim() || "**/*";
232666
233822
  const regex2 = globToRegex(pattern);
232667
233823
  const files = await collectFiles(root2, { maxVisits: Math.max(maxResults * 20, 1e3) });
232668
- const matches = files.map((file) => path13.relative(root2, file).replace(/\\/g, "/")).filter((relative2) => regex2.test(relative2)).slice(0, maxResults);
233824
+ const matches = files.map((file) => path14.relative(root2, file).replace(/\\/g, "/")).filter((relative2) => regex2.test(relative2)).slice(0, maxResults);
232669
233825
  return {
232670
233826
  ok: true,
232671
233827
  matches,
@@ -232688,7 +233844,7 @@ function createCliNodeLocalBridge(input) {
232688
233844
  const files = await collectFiles(root2, { maxVisits: 5e3 });
232689
233845
  const matches = [];
232690
233846
  for (const file of files) {
232691
- const relative2 = path13.relative(root2, file).replace(/\\/g, "/");
233847
+ const relative2 = path14.relative(root2, file).replace(/\\/g, "/");
232692
233848
  if (includeRegex && !includeRegex.test(relative2)) continue;
232693
233849
  const text = await readTextFileIfReasonable(file);
232694
233850
  if (text === null) continue;
@@ -232727,7 +233883,7 @@ function createCliNodeLocalBridge(input) {
232727
233883
  const stat2 = await safeStat(target);
232728
233884
  return {
232729
233885
  ok: true,
232730
- relativePath: path13.isAbsolute(request.relativePath) || request.relativePath.startsWith("~") ? target : path13.relative(baseRoot, target) || ".",
233886
+ relativePath: path14.isAbsolute(request.relativePath) || request.relativePath.startsWith("~") ? target : path14.relative(baseRoot, target) || ".",
232731
233887
  exists: Boolean(stat2),
232732
233888
  isFile: stat2?.isFile() ?? false,
232733
233889
  isDirectory: stat2?.isDirectory() ?? false,
@@ -232761,7 +233917,7 @@ function createCliNodeLocalBridge(input) {
232761
233917
  return {
232762
233918
  ok: true,
232763
233919
  data: buffer.toString("base64"),
232764
- fileName: path13.relative(workspaceRoot, target) || path13.basename(target),
233920
+ fileName: path14.relative(workspaceRoot, target) || path14.basename(target),
232765
233921
  mimeType: inferMimeType(target),
232766
233922
  sizeBytes: stat2.size,
232767
233923
  encoding: "base64"
@@ -232857,7 +234013,7 @@ async function runCliPrepareAPEvidence(workspaceRoot, request) {
232857
234013
  topCases: (artifact.cases ?? []).slice(0, 12),
232858
234014
  duplicateCases: result2.payload.duplicateCases,
232859
234015
  controlGraph: result2.payload.controlGraph,
232860
- relativeOutputDir: typeof artifact.outputDir === "string" ? path13.basename(artifact.outputDir) : null,
234016
+ relativeOutputDir: typeof artifact.outputDir === "string" ? path14.basename(artifact.outputDir) : null,
232861
234017
  approvedRootMatch: true
232862
234018
  },
232863
234019
  executionHost: "electron_desktop",
@@ -232946,7 +234102,7 @@ async function prepareCliSandboxInputs(workspaceRoot, request) {
232946
234102
  const jobId = request.jobId?.trim() || `cli-sandbox-${Date.now()}`;
232947
234103
  const maxFiles = Math.max(1, Math.min(request.maxFiles ?? 20, 100));
232948
234104
  const maxBytesPerFile = Math.max(1, Math.min(request.maxBytesPerFile ?? 20 * 1024 * 1024, 50 * 1024 * 1024));
232949
- const tempDir = await fsp.mkdtemp(path13.join(os2.tmpdir(), `${jobId}-`));
234105
+ const tempDir = await fsp.mkdtemp(path14.join(os3.tmpdir(), `${jobId}-`));
232950
234106
  const entries = [];
232951
234107
  const skipped = [];
232952
234108
  for (const localSourceId of request.localSourceIds.slice(0, maxFiles)) {
@@ -232960,13 +234116,13 @@ async function prepareCliSandboxInputs(workspaceRoot, request) {
232960
234116
  skipped.push({ localSourceId, reason: `File is too large: ${stat2.size} bytes.` });
232961
234117
  continue;
232962
234118
  }
232963
- const relativePath = safeSandboxRelativePath(path13.isAbsolute(localSourceId) || localSourceId.startsWith("~") ? path13.basename(sourcePath) : path13.relative(workspaceRoot, sourcePath));
232964
- const tempPath = path13.join(tempDir, relativePath);
232965
- await fsp.mkdir(path13.dirname(tempPath), { recursive: true });
234119
+ const relativePath = safeSandboxRelativePath(path14.isAbsolute(localSourceId) || localSourceId.startsWith("~") ? path14.basename(sourcePath) : path14.relative(workspaceRoot, sourcePath));
234120
+ const tempPath = path14.join(tempDir, relativePath);
234121
+ await fsp.mkdir(path14.dirname(tempPath), { recursive: true });
232966
234122
  await fsp.copyFile(sourcePath, tempPath);
232967
234123
  entries.push({
232968
234124
  localSourceId,
232969
- fileName: path13.basename(sourcePath),
234125
+ fileName: path14.basename(sourcePath),
232970
234126
  relativePath,
232971
234127
  tempPath,
232972
234128
  sizeBytes: stat2.size,
@@ -233004,11 +234160,11 @@ async function runCliSandboxCodeJob(workspaceRoot, request, onEvent) {
233004
234160
  getApprovedRoot: (rootId) => rootId === CLI_ROOT_ID ? { id: CLI_ROOT_ID, path: workspaceRoot, approvedAt: (/* @__PURE__ */ new Date()).toISOString() } : null,
233005
234161
  isPathSafe: (_rootPath, targetPath) => !targetPath.split(/[\\/]+/).includes(".."),
233006
234162
  isInsideApprovedRoot: (filePath) => {
233007
- const absolute = path13.resolve(expandHome4(filePath));
234163
+ const absolute = path14.resolve(expandHome4(filePath));
233008
234164
  if (isInside(workspaceRoot, absolute)) {
233009
234165
  return { id: CLI_ROOT_ID, path: workspaceRoot, approvedAt: (/* @__PURE__ */ new Date()).toISOString() };
233010
234166
  }
233011
- return { id: "cli-absolute-root", path: path13.dirname(absolute), approvedAt: (/* @__PURE__ */ new Date()).toISOString() };
234167
+ return { id: "cli-absolute-root", path: path14.dirname(absolute), approvedAt: (/* @__PURE__ */ new Date()).toISOString() };
233012
234168
  },
233013
234169
  shouldIgnore: (fileName) => IGNORED_DIRS.has(fileName),
233014
234170
  guessMimeType: inferMimeType,
@@ -233051,7 +234207,7 @@ function safeSandboxRelativePath(value) {
233051
234207
  return clean || "input";
233052
234208
  }
233053
234209
  function guessCliFileType(filePath) {
233054
- const ext = path13.extname(filePath).toLowerCase();
234210
+ const ext = path14.extname(filePath).toLowerCase();
233055
234211
  if ([".xlsx", ".xls"].includes(ext)) return "spreadsheet";
233056
234212
  if (ext === ".csv" || ext === ".tsv") return "csv";
233057
234213
  if (ext === ".pdf") return "pdf";
@@ -233070,7 +234226,7 @@ async function readCliProjectMemoryBridge(workspaceRoot) {
233070
234226
  return { ok: true, meta: state.meta };
233071
234227
  }
233072
234228
  async function writeCliProjectMemory(workspaceRoot, patch) {
233073
- const root2 = path13.resolve(expandHome4(workspaceRoot));
234229
+ const root2 = path14.resolve(expandHome4(workspaceRoot));
233074
234230
  if (!isCliProjectMemoryAvailable(root2)) {
233075
234231
  return {
233076
234232
  ok: false,
@@ -233086,12 +234242,12 @@ async function writeCliProjectMemory(workspaceRoot, patch) {
233086
234242
  lastOpenedThreadId: patch.lastOpenedThreadId !== void 0 ? patch.lastOpenedThreadId : current.lastOpenedThreadId,
233087
234243
  memorySummary: patch.memorySummary !== void 0 ? patch.memorySummary : current.memorySummary
233088
234244
  };
233089
- await fsp.mkdir(path13.join(root2, PERCH_DIR), { recursive: true });
234245
+ await fsp.mkdir(path14.join(root2, PERCH_DIR), { recursive: true });
233090
234246
  await atomicWriteCliUtf8(getCliProjectFilePath(root2), JSON.stringify(next, null, 2));
233091
234247
  return { ok: true, meta: enrichCliProjectMeta(root2, next) };
233092
234248
  }
233093
234249
  async function writeCliMemoryFile(workspaceRoot, request) {
233094
- const root2 = path13.resolve(expandHome4(workspaceRoot));
234250
+ const root2 = path14.resolve(expandHome4(workspaceRoot));
233095
234251
  if (!isCliProjectMemoryAvailable(root2)) {
233096
234252
  return {
233097
234253
  ok: false,
@@ -233103,8 +234259,8 @@ async function writeCliMemoryFile(workspaceRoot, request) {
233103
234259
  }
233104
234260
  const fileName = normalizeCliMemoryFileName(request.fileName);
233105
234261
  if (!fileName) return { ok: false, error: "Invalid memory file name." };
233106
- const relativePath = path13.join(PERCH_DIR, MEMORY_DIR, fileName);
233107
- const fullPath = path13.join(root2, relativePath);
234262
+ const relativePath = path14.join(PERCH_DIR, MEMORY_DIR, fileName);
234263
+ const fullPath = path14.join(root2, relativePath);
233108
234264
  const existing = await fsp.readFile(fullPath, "utf8").catch(() => "");
233109
234265
  let nextContent;
233110
234266
  try {
@@ -233117,7 +234273,7 @@ async function writeCliMemoryFile(workspaceRoot, request) {
233117
234273
  }
233118
234274
  const capError = assertCliMemoryWithinCap(nextContent);
233119
234275
  if (capError) return { ok: false, error: capError };
233120
- await fsp.mkdir(path13.dirname(fullPath), { recursive: true });
234276
+ await fsp.mkdir(path14.dirname(fullPath), { recursive: true });
233121
234277
  await atomicWriteCliUtf8(fullPath, nextContent);
233122
234278
  const updatedFile = readCliTextMemoryFile(root2, relativePath, MAX_MEMORY_BYTES);
233123
234279
  return {
@@ -233143,17 +234299,17 @@ function defaultCliProjectMeta() {
233143
234299
  }
233144
234300
  function isCliProjectMemoryAvailable(root2) {
233145
234301
  try {
233146
- return fs12.statSync(path13.join(root2, PERCH_DIR)).isDirectory();
234302
+ return fs13.statSync(path14.join(root2, PERCH_DIR)).isDirectory();
233147
234303
  } catch {
233148
234304
  return false;
233149
234305
  }
233150
234306
  }
233151
234307
  function getCliProjectFilePath(root2) {
233152
- return path13.join(root2, PERCH_DIR, PROJECT_FILE);
234308
+ return path14.join(root2, PERCH_DIR, PROJECT_FILE);
233153
234309
  }
233154
234310
  function loadCliStoredMeta(root2) {
233155
234311
  try {
233156
- const raw = fs12.readFileSync(getCliProjectFilePath(root2), "utf8");
234312
+ const raw = fs13.readFileSync(getCliProjectFilePath(root2), "utf8");
233157
234313
  return { ...defaultCliProjectMeta(), ...JSON.parse(raw) };
233158
234314
  } catch {
233159
234315
  return defaultCliProjectMeta();
@@ -233161,7 +234317,7 @@ function loadCliStoredMeta(root2) {
233161
234317
  }
233162
234318
  function enrichCliProjectMeta(root2, base) {
233163
234319
  const perchMd = readCliFirstExisting(root2, [
233164
- path13.join(PERCH_DIR, PERCH_INDEX_FILE),
234320
+ path14.join(PERCH_DIR, PERCH_INDEX_FILE),
233165
234321
  PERCH_INDEX_FILE
233166
234322
  ], MAX_TEXT_BYTES);
233167
234323
  const rules = readCliRules(root2);
@@ -233272,10 +234428,10 @@ function readCliFirstExisting(root2, relativePaths, maxBytes) {
233272
234428
  return null;
233273
234429
  }
233274
234430
  function readCliRules(root2) {
233275
- const dirPath = path13.join(root2, PERCH_DIR, RULES_DIR);
234431
+ const dirPath = path14.join(root2, PERCH_DIR, RULES_DIR);
233276
234432
  try {
233277
- return fs12.readdirSync(dirPath).filter((name) => [".md", ".txt"].includes(path13.extname(name).toLowerCase())).slice(0, 40).map((name) => readCliTextMemoryFile(root2, path13.join(PERCH_DIR, RULES_DIR, name), MAX_TEXT_BYTES)).filter((file) => file.found).map((file) => ({
233278
- fileName: path13.basename(file.relativePath),
234433
+ return fs13.readdirSync(dirPath).filter((name) => [".md", ".txt"].includes(path14.extname(name).toLowerCase())).slice(0, 40).map((name) => readCliTextMemoryFile(root2, path14.join(PERCH_DIR, RULES_DIR, name), MAX_TEXT_BYTES)).filter((file) => file.found).map((file) => ({
234434
+ fileName: path14.basename(file.relativePath),
233279
234435
  relativePath: file.relativePath,
233280
234436
  content: file.content,
233281
234437
  sizeBytes: file.sizeBytes,
@@ -233286,15 +234442,15 @@ function readCliRules(root2) {
233286
234442
  }
233287
234443
  }
233288
234444
  function readCliMemoryFiles(root2) {
233289
- const memoryDir = path13.join(root2, PERCH_DIR, MEMORY_DIR);
234445
+ const memoryDir = path14.join(root2, PERCH_DIR, MEMORY_DIR);
233290
234446
  const discovered = /* @__PURE__ */ new Set();
233291
234447
  const memoryFiles = [];
233292
234448
  try {
233293
- for (const name of fs12.readdirSync(memoryDir).slice(0, MAX_MEMORY_FILES)) {
233294
- const ext = path13.extname(name).toLowerCase();
234449
+ for (const name of fs13.readdirSync(memoryDir).slice(0, MAX_MEMORY_FILES)) {
234450
+ const ext = path14.extname(name).toLowerCase();
233295
234451
  if (ext !== ".md" && ext !== ".txt") continue;
233296
234452
  discovered.add(name);
233297
- memoryFiles.push(readCliTextMemoryFile(root2, path13.join(PERCH_DIR, MEMORY_DIR, name), MAX_MEMORY_BYTES));
234453
+ memoryFiles.push(readCliTextMemoryFile(root2, path14.join(PERCH_DIR, MEMORY_DIR, name), MAX_MEMORY_BYTES));
233298
234454
  }
233299
234455
  } catch {
233300
234456
  }
@@ -233302,20 +234458,20 @@ function readCliMemoryFiles(root2) {
233302
234458
  memoryFiles,
233303
234459
  missingMemoryFiles: EXPECTED_MEMORY_FILES.filter((name) => !discovered.has(name)).map((name) => ({
233304
234460
  fileName: name,
233305
- relativePath: path13.join(PERCH_DIR, MEMORY_DIR, name),
234461
+ relativePath: path14.join(PERCH_DIR, MEMORY_DIR, name),
233306
234462
  reason: "expected project memory file was not found"
233307
234463
  }))
233308
234464
  };
233309
234465
  }
233310
234466
  function readCliTextMemoryFile(root2, relativePath, maxBytes) {
233311
- const fullPath = path13.join(root2, relativePath);
233312
- const fileName = path13.basename(relativePath);
234467
+ const fullPath = path14.join(root2, relativePath);
234468
+ const fileName = path14.basename(relativePath);
233313
234469
  try {
233314
- const stat2 = fs12.statSync(fullPath);
234470
+ const stat2 = fs13.statSync(fullPath);
233315
234471
  if (!stat2.isFile()) {
233316
234472
  return { fileName, relativePath, content: "", found: false, error: "not a regular file" };
233317
234473
  }
233318
- const content = fs12.readFileSync(fullPath, "utf8");
234474
+ const content = fs13.readFileSync(fullPath, "utf8");
233319
234475
  if (stat2.size > maxBytes) {
233320
234476
  return {
233321
234477
  fileName,
@@ -233437,7 +234593,7 @@ function terminateProcessGroup(child, signal) {
233437
234593
  }
233438
234594
  function resolveReadPath(root2, inputPath) {
233439
234595
  const expanded = expandHome4(inputPath || ".");
233440
- return path13.resolve(path13.isAbsolute(expanded) ? expanded : path13.join(root2, expanded));
234596
+ return path14.resolve(path14.isAbsolute(expanded) ? expanded : path14.join(root2, expanded));
233441
234597
  }
233442
234598
  function resolveRequestRoot(defaultRoot, requestRoot) {
233443
234599
  const trimmed = requestRoot?.trim();
@@ -233445,25 +234601,25 @@ function resolveRequestRoot(defaultRoot, requestRoot) {
233445
234601
  }
233446
234602
  function resolveWritePath(root2, inputPath) {
233447
234603
  const expanded = expandHome4(inputPath || ".");
233448
- return path13.resolve(path13.isAbsolute(expanded) ? expanded : path13.join(root2, expanded));
234604
+ return path14.resolve(path14.isAbsolute(expanded) ? expanded : path14.join(root2, expanded));
233449
234605
  }
233450
234606
  function displayPath(root2, target, requestedPath) {
233451
234607
  const expanded = expandHome4(requestedPath || ".");
233452
- if (path13.isAbsolute(expanded) || requestedPath.startsWith("~")) return target;
233453
- return path13.relative(root2, target) || path13.basename(target);
234608
+ if (path14.isAbsolute(expanded) || requestedPath.startsWith("~")) return target;
234609
+ return path14.relative(root2, target) || path14.basename(target);
233454
234610
  }
233455
234611
  function resolveLocalSourceId(root2, localSourceId) {
233456
234612
  const raw = localSourceId.includes("::") ? localSourceId.split("::").slice(1).join("::") : localSourceId;
233457
234613
  return resolveReadPath(root2, raw);
233458
234614
  }
233459
234615
  function expandHome4(inputPath) {
233460
- if (inputPath === "~") return os2.homedir();
233461
- if (inputPath.startsWith("~/")) return path13.join(os2.homedir(), inputPath.slice(2));
234616
+ if (inputPath === "~") return os3.homedir();
234617
+ if (inputPath.startsWith("~/")) return path14.join(os3.homedir(), inputPath.slice(2));
233462
234618
  return inputPath;
233463
234619
  }
233464
234620
  function isInside(root2, candidate) {
233465
- const relative2 = path13.relative(path13.resolve(root2), path13.resolve(candidate));
233466
- return relative2 === "" || !relative2.startsWith("..") && !path13.isAbsolute(relative2);
234621
+ const relative2 = path14.relative(path14.resolve(root2), path14.resolve(candidate));
234622
+ return relative2 === "" || !relative2.startsWith("..") && !path14.isAbsolute(relative2);
233467
234623
  }
233468
234624
  async function safeStat(target) {
233469
234625
  try {
@@ -233480,7 +234636,7 @@ async function collectFiles(root2, options) {
233480
234636
  for (const entry of entries) {
233481
234637
  if (result2.length >= options.maxVisits) return;
233482
234638
  if (entry.name.startsWith(".") && entry.name !== ".env") continue;
233483
- const absolute = path13.join(dir, entry.name);
234639
+ const absolute = path14.join(dir, entry.name);
233484
234640
  if (entry.isDirectory()) {
233485
234641
  if (!IGNORED_DIRS.has(entry.name)) await walk2(absolute);
233486
234642
  } else if (entry.isFile()) {
@@ -233536,14 +234692,14 @@ function sanitizeMaxResults(value) {
233536
234692
  return Math.max(1, Math.min(typeof value === "number" ? Math.floor(value) : DEFAULT_MAX_RESULTS, 1e3));
233537
234693
  }
233538
234694
  function localSourceEntry(root2, file, absoluteId = false) {
233539
- const stat2 = fs12.statSync(file);
233540
- const relativePath = path13.relative(root2, file);
233541
- const extension2 = path13.extname(file).toLowerCase();
234695
+ const stat2 = fs13.statSync(file);
234696
+ const relativePath = path14.relative(root2, file);
234697
+ const extension2 = path14.extname(file).toLowerCase();
233542
234698
  return {
233543
234699
  localSourceId: absoluteId ? file : `${CLI_ROOT_ID}::${relativePath}`,
233544
234700
  rootId: absoluteId ? "cli-absolute-root" : CLI_ROOT_ID,
233545
234701
  relativePath,
233546
- fileName: path13.basename(file),
234702
+ fileName: path14.basename(file),
233547
234703
  extension: extension2,
233548
234704
  sizeBytes: stat2.size,
233549
234705
  modifiedAt: stat2.mtime.toISOString(),
@@ -233552,7 +234708,7 @@ function localSourceEntry(root2, file, absoluteId = false) {
233552
234708
  };
233553
234709
  }
233554
234710
  function inferMimeType(file) {
233555
- const ext = path13.extname(file).toLowerCase();
234711
+ const ext = path14.extname(file).toLowerCase();
233556
234712
  if (ext === ".pdf") return "application/pdf";
233557
234713
  if (ext === ".json") return "application/json";
233558
234714
  if (ext === ".csv") return "text/csv";
@@ -233613,12 +234769,12 @@ var init_nodeLocalBridge = __esm({
233613
234769
  });
233614
234770
 
233615
234771
  // features/perchTerminal/runtime/cliHost/runCliTurn.ts
233616
- import path14 from "node:path";
234772
+ import path15 from "node:path";
233617
234773
  async function runPerchCliTurn(input, options = {}) {
233618
234774
  const prompt = input.prompt.trim();
233619
234775
  if (!prompt) throw new Error("Missing prompt for perch run");
233620
234776
  const now23 = options.now ?? (() => Date.now());
233621
- const cwd2 = path14.resolve(input.cwd ?? process.cwd());
234777
+ const cwd2 = path15.resolve(input.cwd ?? process.cwd());
233622
234778
  const threadId = input.threadId?.trim() || `cli-${now23()}`;
233623
234779
  const events = [];
233624
234780
  let latestContextSnapshot = null;
@@ -233919,440 +235075,6 @@ var init_cliHost = __esm({
233919
235075
  }
233920
235076
  });
233921
235077
 
233922
- // features/perchTerminal/runtime/publicModelDefault.ts
233923
- function parsePublicModelDefaultPayload(value) {
233924
- const raw = value && typeof value === "object" ? value : {};
233925
- const selection = sanitizeFounderModelSelection(
233926
- raw.selection ?? DEFAULT_FOUNDER_MODEL_SELECTION
233927
- );
233928
- return {
233929
- selection,
233930
- updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : null,
233931
- updatedBy: typeof raw.updatedBy === "string" ? raw.updatedBy : null
233932
- };
233933
- }
233934
- var init_publicModelDefault = __esm({
233935
- "features/perchTerminal/runtime/publicModelDefault.ts"() {
233936
- "use strict";
233937
- init_modelRegistry();
233938
- }
233939
- });
233940
-
233941
- // features/perchTerminal/runtime/cliHost/cliAuthSession.ts
233942
- import { execFile } from "node:child_process";
233943
- import fs13 from "node:fs/promises";
233944
- import os3 from "node:os";
233945
- import path15 from "node:path";
233946
- import { promisify } from "node:util";
233947
- async function readStoredCliAuthSession() {
233948
- const raw = shouldUseFallbackAuthFile() ? await readFallbackSecret().catch(() => null) : process.platform === "darwin" ? await readKeychainSecret().catch(() => null) : await readFallbackSecret().catch(() => null);
233949
- return parseStoredCliAuthSession(raw);
233950
- }
233951
- async function writeStoredCliAuthSession(session) {
233952
- const raw = JSON.stringify(session);
233953
- if (process.platform === "darwin" && !shouldUseFallbackAuthFile()) {
233954
- await execFileAsync("security", [
233955
- "add-generic-password",
233956
- "-U",
233957
- "-s",
233958
- KEYCHAIN_SERVICE,
233959
- "-a",
233960
- KEYCHAIN_ACCOUNT,
233961
- "-w",
233962
- raw
233963
- ]);
233964
- return;
233965
- }
233966
- await writeFallbackSecret(raw);
233967
- }
233968
- async function clearStoredCliAuthSession() {
233969
- if (process.platform === "darwin" && !shouldUseFallbackAuthFile()) {
233970
- await execFileAsync("security", [
233971
- "delete-generic-password",
233972
- "-s",
233973
- KEYCHAIN_SERVICE,
233974
- "-a",
233975
- KEYCHAIN_ACCOUNT
233976
- ]).catch(() => void 0);
233977
- return;
233978
- }
233979
- await fs13.rm(fallbackSessionPath(), { force: true }).catch(() => void 0);
233980
- }
233981
- function shouldUseFallbackAuthFile() {
233982
- return Boolean(process.env.PERCH_CLI_AUTH_DIR?.trim());
233983
- }
233984
- function isStoredCliAuthSessionUsable(session, nowSeconds = Math.floor(Date.now() / 1e3)) {
233985
- if (!session?.accessToken?.trim()) return false;
233986
- if (!session.appUrl?.trim()) return false;
233987
- if (!session.expiresAt) return true;
233988
- return session.expiresAt > nowSeconds + 90;
233989
- }
233990
- function parseStoredCliAuthSession(raw) {
233991
- if (!raw?.trim()) return null;
233992
- try {
233993
- const parsed = JSON.parse(raw);
233994
- if (parsed.version !== 1) return null;
233995
- if (!parsed.appUrl || !parsed.accessToken || !parsed.updatedAt) return null;
233996
- return {
233997
- version: 1,
233998
- appUrl: parsed.appUrl,
233999
- accessToken: parsed.accessToken,
234000
- refreshToken: parsed.refreshToken ?? null,
234001
- expiresAt: typeof parsed.expiresAt === "number" ? parsed.expiresAt : null,
234002
- userId: parsed.userId ?? null,
234003
- email: parsed.email ?? null,
234004
- updatedAt: parsed.updatedAt
234005
- };
234006
- } catch {
234007
- return null;
234008
- }
234009
- }
234010
- async function readKeychainSecret() {
234011
- const { stdout } = await execFileAsync("security", [
234012
- "find-generic-password",
234013
- "-s",
234014
- KEYCHAIN_SERVICE,
234015
- "-a",
234016
- KEYCHAIN_ACCOUNT,
234017
- "-w"
234018
- ]);
234019
- return stdout.trim() || null;
234020
- }
234021
- function fallbackSessionPath() {
234022
- const base = process.env.PERCH_CLI_AUTH_DIR?.trim() || path15.join(os3.homedir(), ".perch");
234023
- return path15.join(base, "cli-auth-session.json");
234024
- }
234025
- async function readFallbackSecret() {
234026
- return await fs13.readFile(fallbackSessionPath(), "utf8");
234027
- }
234028
- async function writeFallbackSecret(raw) {
234029
- const filePath = fallbackSessionPath();
234030
- await fs13.mkdir(path15.dirname(filePath), { recursive: true, mode: 448 });
234031
- await fs13.writeFile(filePath, raw, { mode: 384 });
234032
- }
234033
- var execFileAsync, KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT;
234034
- var init_cliAuthSession = __esm({
234035
- "features/perchTerminal/runtime/cliHost/cliAuthSession.ts"() {
234036
- "use strict";
234037
- execFileAsync = promisify(execFile);
234038
- KEYCHAIN_SERVICE = "app.perchai.cli-auth";
234039
- KEYCHAIN_ACCOUNT = "default";
234040
- }
234041
- });
234042
-
234043
- // features/perchTerminal/runtime/cliHost/modelConnection.ts
234044
- async function connectCliModelProxy(input = {}) {
234045
- const storedSession = input.storedSession === void 0 ? await readStoredCliAuthSession() : input.storedSession;
234046
- const usableStoredSession = isStoredCliAuthSessionUsable(storedSession) ? storedSession : null;
234047
- const appUrl = resolveCliAppUrl(input.appUrl, usableStoredSession?.appUrl ?? null);
234048
- if (!appUrl) return noCliModelConnection();
234049
- if (!usableStoredSession?.accessToken) return noCliModelConnection(appUrl);
234050
- const fetchImpl = input.fetchImpl ?? globalThis.fetch;
234051
- if (typeof fetchImpl !== "function") return noCliModelConnection(appUrl);
234052
- const selection = await fetchPublicModelDefaultSelection(
234053
- appUrl,
234054
- fetchImpl,
234055
- usableStoredSession?.accessToken ?? null
234056
- );
234057
- if (!selection) return noCliModelConnection(appUrl);
234058
- const priorProxy = process.env[MODEL_PROXY_ENV];
234059
- const priorToken = process.env[MODEL_PROXY_TOKEN_ENV];
234060
- process.env[MODEL_PROXY_ENV] = appUrl;
234061
- if (usableStoredSession?.accessToken) {
234062
- process.env[MODEL_PROXY_TOKEN_ENV] = usableStoredSession.accessToken;
234063
- }
234064
- return {
234065
- appUrl,
234066
- authenticated: !!usableStoredSession?.accessToken,
234067
- userId: usableStoredSession?.userId ?? null,
234068
- email: usableStoredSession?.email ?? null,
234069
- founderModelSelection: selection,
234070
- restore: () => {
234071
- if (priorProxy === void 0) {
234072
- delete process.env[MODEL_PROXY_ENV];
234073
- } else {
234074
- process.env[MODEL_PROXY_ENV] = priorProxy;
234075
- }
234076
- if (priorToken === void 0) {
234077
- delete process.env[MODEL_PROXY_TOKEN_ENV];
234078
- } else {
234079
- process.env[MODEL_PROXY_TOKEN_ENV] = priorToken;
234080
- }
234081
- }
234082
- };
234083
- }
234084
- function resolveCliAppUrl(explicit, stored) {
234085
- const raw = explicit?.trim() || process.env[CLI_APP_URL_ENV]?.trim() || process.env[FALLBACK_APP_URL_ENV]?.trim() || stored?.trim() || DEFAULT_CLI_APP_URL;
234086
- if (!raw) return null;
234087
- const withProtocol = /^https?:\/\//i.test(raw) ? raw : `http://${raw}`;
234088
- try {
234089
- const url = new URL(withProtocol);
234090
- url.pathname = url.pathname.replace(/\/+$/, "");
234091
- url.search = "";
234092
- url.hash = "";
234093
- return url.toString().replace(/\/+$/, "");
234094
- } catch {
234095
- return null;
234096
- }
234097
- }
234098
- async function fetchPublicModelDefaultSelection(appUrl, fetchImpl, accessToken) {
234099
- const url = `${appUrl}/api/perch-terminal/public-model-default`;
234100
- const controller = new AbortController();
234101
- const timeout = setTimeout(() => controller.abort(), 3500);
234102
- try {
234103
- const response = await fetchImpl(url, {
234104
- method: "GET",
234105
- headers: {
234106
- Accept: "application/json",
234107
- ...accessToken ? { Authorization: `Bearer ${accessToken}` } : {}
234108
- },
234109
- signal: controller.signal
234110
- });
234111
- if (!response.ok) return null;
234112
- const body = await response.json();
234113
- if (!body || typeof body !== "object") return null;
234114
- const record = body;
234115
- if (record.ok !== true) return null;
234116
- return parsePublicModelDefaultPayload({ selection: record.selection }).selection;
234117
- } catch {
234118
- return null;
234119
- } finally {
234120
- clearTimeout(timeout);
234121
- }
234122
- }
234123
- function noCliModelConnection(appUrl = null) {
234124
- return {
234125
- appUrl,
234126
- authenticated: false,
234127
- userId: null,
234128
- email: null,
234129
- founderModelSelection: null,
234130
- restore: () => {
234131
- }
234132
- };
234133
- }
234134
- var MODEL_PROXY_ENV, MODEL_PROXY_TOKEN_ENV, CLI_APP_URL_ENV, FALLBACK_APP_URL_ENV, DEFAULT_CLI_APP_URL;
234135
- var init_modelConnection = __esm({
234136
- "features/perchTerminal/runtime/cliHost/modelConnection.ts"() {
234137
- "use strict";
234138
- init_publicModelDefault();
234139
- init_cliAuthSession();
234140
- MODEL_PROXY_ENV = "PERCH_MODEL_CALL_PROXY_URL";
234141
- MODEL_PROXY_TOKEN_ENV = "PERCH_MODEL_CALL_PROXY_TOKEN";
234142
- CLI_APP_URL_ENV = "PERCH_CLI_APP_URL";
234143
- FALLBACK_APP_URL_ENV = "PERCH_APP_URL";
234144
- DEFAULT_CLI_APP_URL = "https://app.perchai.app";
234145
- }
234146
- });
234147
-
234148
- // features/perchTerminal/runtime/cliHost/cliStandaloneOAuth.ts
234149
- import { execFile as execFile2 } from "node:child_process";
234150
- import http from "node:http";
234151
- import { promisify as promisify2 } from "node:util";
234152
- async function runCliStandaloneOAuthLogin(input) {
234153
- const appUrl = resolveCliAppUrl(input.appUrl, null);
234154
- if (!appUrl) {
234155
- return { ok: false, code: "invalid_app_url", error: "Invalid Perch app URL." };
234156
- }
234157
- const fetchImpl = input.fetchImpl ?? globalThis.fetch;
234158
- if (typeof fetchImpl !== "function") {
234159
- return { ok: false, code: "fetch_unavailable", error: "This Node runtime does not expose fetch." };
234160
- }
234161
- const config = await fetchCliAuthConfig(appUrl, fetchImpl);
234162
- if (!config.ok || !config.supabaseUrl || !config.supabaseAnonKey) {
234163
- return {
234164
- ok: false,
234165
- code: "auth_config_unavailable",
234166
- error: "The Perch app did not return CLI auth configuration."
234167
- };
234168
- }
234169
- const provider = selectOAuthProvider(config.providers ?? [], input.provider);
234170
- if (!provider) {
234171
- return {
234172
- ok: false,
234173
- code: "oauth_provider_unavailable",
234174
- error: "No supported CLI OAuth provider is enabled for this Perch app."
234175
- };
234176
- }
234177
- const callback = await createOAuthCallbackServer({
234178
- host: config.redirectHost?.trim() || "127.0.0.1",
234179
- timeoutMs: input.timeoutMs ?? DEFAULT_LOGIN_TIMEOUT_MS
234180
- });
234181
- try {
234182
- const memoryStorage = createMemoryAuthStorage();
234183
- const supabase = createClient(config.supabaseUrl, config.supabaseAnonKey, {
234184
- auth: {
234185
- flowType: "pkce",
234186
- autoRefreshToken: false,
234187
- detectSessionInUrl: false,
234188
- persistSession: true,
234189
- storage: memoryStorage
234190
- }
234191
- });
234192
- const { data, error } = await supabase.auth.signInWithOAuth({
234193
- provider,
234194
- options: {
234195
- redirectTo: callback.redirectTo,
234196
- skipBrowserRedirect: true,
234197
- data: { signup_source: "cli" }
234198
- }
234199
- });
234200
- if (error || !data.url) {
234201
- return {
234202
- ok: false,
234203
- code: "oauth_start_failed",
234204
- error: error?.message ?? "Supabase did not return an OAuth URL."
234205
- };
234206
- }
234207
- await (input.openExternal ?? openExternal)(data.url);
234208
- const callbackResult = await callback.waitForCode();
234209
- if (!callbackResult.ok) {
234210
- return callbackResult;
234211
- }
234212
- const exchanged = await supabase.auth.exchangeCodeForSession(callbackResult.code);
234213
- if (exchanged.error || !exchanged.data.session) {
234214
- return {
234215
- ok: false,
234216
- code: "oauth_exchange_failed",
234217
- error: exchanged.error?.message ?? "Supabase did not return a CLI session."
234218
- };
234219
- }
234220
- const session = storedSessionFromSupabaseSession({
234221
- appUrl,
234222
- accessToken: exchanged.data.session.access_token,
234223
- refreshToken: exchanged.data.session.refresh_token,
234224
- expiresAt: exchanged.data.session.expires_at ?? null,
234225
- userId: exchanged.data.session.user.id,
234226
- email: exchanged.data.session.user.email ?? null
234227
- });
234228
- await writeStoredCliAuthSession(session);
234229
- return { ok: true, session };
234230
- } finally {
234231
- await callback.close();
234232
- }
234233
- }
234234
- async function fetchCliAuthConfig(appUrl, fetchImpl = globalThis.fetch) {
234235
- const response = await fetchImpl(`${appUrl}/api/perch-terminal/cli-auth/config`, {
234236
- method: "GET",
234237
- headers: { Accept: "application/json" }
234238
- });
234239
- if (!response.ok) return { ok: false };
234240
- return await response.json();
234241
- }
234242
- function selectOAuthProvider(providers, preferred) {
234243
- const available = new Set(providers);
234244
- if (preferred && available.has(preferred)) return preferred;
234245
- if (available.has("google")) return "google";
234246
- if (available.has("github")) return "github";
234247
- return null;
234248
- }
234249
- function storedSessionFromSupabaseSession(input) {
234250
- return {
234251
- version: 1,
234252
- appUrl: input.appUrl,
234253
- accessToken: input.accessToken,
234254
- refreshToken: input.refreshToken ?? null,
234255
- expiresAt: input.expiresAt ?? null,
234256
- userId: input.userId ?? null,
234257
- email: input.email ?? null,
234258
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
234259
- };
234260
- }
234261
- function createMemoryAuthStorage() {
234262
- const values2 = /* @__PURE__ */ new Map();
234263
- return {
234264
- getItem: (key) => values2.get(key) ?? null,
234265
- setItem: (key, value) => {
234266
- values2.set(key, value);
234267
- },
234268
- removeItem: (key) => {
234269
- values2.delete(key);
234270
- }
234271
- };
234272
- }
234273
- async function createOAuthCallbackServer(input) {
234274
- let resolveResult = null;
234275
- const resultPromise = new Promise((resolve5) => {
234276
- resolveResult = resolve5;
234277
- });
234278
- const server = http.createServer((request, response) => {
234279
- const requestUrl = new URL(request.url ?? "/", `http://${input.host}`);
234280
- if (requestUrl.pathname !== CALLBACK_PATH) {
234281
- response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
234282
- response.end("Not found");
234283
- return;
234284
- }
234285
- const code = requestUrl.searchParams.get("code");
234286
- const error = requestUrl.searchParams.get("error_description") ?? requestUrl.searchParams.get("error");
234287
- if (!code) {
234288
- response.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
234289
- response.end(renderCallbackHtml(false));
234290
- resolveResult?.({
234291
- ok: false,
234292
- code: "oauth_callback_missing_code",
234293
- error: error ?? "OAuth callback did not include a code."
234294
- });
234295
- resolveResult = null;
234296
- return;
234297
- }
234298
- response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
234299
- response.end(renderCallbackHtml(true));
234300
- resolveResult?.({ ok: true, code });
234301
- resolveResult = null;
234302
- });
234303
- await new Promise((resolve5, reject2) => {
234304
- server.once("error", reject2);
234305
- server.listen(0, input.host, () => {
234306
- server.off("error", reject2);
234307
- resolve5();
234308
- });
234309
- });
234310
- const address = server.address();
234311
- const redirectTo = `http://${input.host}:${address.port}${CALLBACK_PATH}`;
234312
- const timeout = setTimeout(() => {
234313
- resolveResult?.({
234314
- ok: false,
234315
- code: "oauth_timeout",
234316
- error: "Timed out waiting for browser sign-in to complete."
234317
- });
234318
- resolveResult = null;
234319
- }, input.timeoutMs);
234320
- return {
234321
- redirectTo,
234322
- waitForCode: async () => resultPromise,
234323
- close: async () => {
234324
- clearTimeout(timeout);
234325
- await new Promise((resolve5) => server.close(() => resolve5()));
234326
- }
234327
- };
234328
- }
234329
- function renderCallbackHtml(ok) {
234330
- const title = ok ? "Perch CLI signed in" : "Perch CLI sign-in failed";
234331
- const message = ok ? "You can close this tab and return to your terminal." : "Return to your terminal and try again.";
234332
- return `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title></head><body style="font-family:system-ui;padding:32px;background:#100d0b;color:#fff8f0"><h1>${title}</h1><p>${message}</p></body></html>`;
234333
- }
234334
- async function openExternal(url) {
234335
- if (process.platform === "darwin") {
234336
- await execFileAsync2("open", [url]);
234337
- } else if (process.platform === "win32") {
234338
- await execFileAsync2("cmd", ["/c", "start", "", url]);
234339
- } else {
234340
- await execFileAsync2("xdg-open", [url]);
234341
- }
234342
- }
234343
- var execFileAsync2, DEFAULT_LOGIN_TIMEOUT_MS, CALLBACK_PATH;
234344
- var init_cliStandaloneOAuth = __esm({
234345
- "features/perchTerminal/runtime/cliHost/cliStandaloneOAuth.ts"() {
234346
- "use strict";
234347
- init_dist4();
234348
- init_modelConnection();
234349
- init_cliAuthSession();
234350
- execFileAsync2 = promisify2(execFile2);
234351
- DEFAULT_LOGIN_TIMEOUT_MS = 12e4;
234352
- CALLBACK_PATH = "/callback";
234353
- }
234354
- });
234355
-
234356
235078
  // lib/perchTerminal/statusLabels.ts
234357
235079
  function sectionLabel(sectionKey) {
234358
235080
  if (STATUS_LABELS[sectionKey]) return STATUS_LABELS[sectionKey];
@@ -234873,6 +235595,17 @@ function claimFirstRun(dir = stateDir()) {
234873
235595
  return false;
234874
235596
  }
234875
235597
  }
235598
+ function claimFirstPrompt(dir = stateDir()) {
235599
+ const file = path17.join(dir, "cli-first-prompt");
235600
+ try {
235601
+ if (fs15.existsSync(file)) return false;
235602
+ fs15.mkdirSync(dir, { recursive: true });
235603
+ fs15.writeFileSync(file, (/* @__PURE__ */ new Date()).toISOString());
235604
+ return true;
235605
+ } catch {
235606
+ return false;
235607
+ }
235608
+ }
234876
235609
  async function postCapture(event, properties) {
234877
235610
  const controller = new AbortController();
234878
235611
  const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS2);
@@ -234899,6 +235632,17 @@ function captureCliFirstRun(version4) {
234899
235632
  if (!claimFirstRun()) return;
234900
235633
  void postCapture("cli_first_run", { version: version4, platform: process.platform });
234901
235634
  }
235635
+ function captureCliSessionStarted() {
235636
+ if (!POSTHOG_KEY || telemetryDisabled()) return;
235637
+ void postCapture("cli_session_started", { platform: process.platform });
235638
+ }
235639
+ function captureCliMessageSent() {
235640
+ if (!POSTHOG_KEY || telemetryDisabled()) return;
235641
+ void postCapture("message_sent", { surface: "cli" });
235642
+ if (claimFirstPrompt()) {
235643
+ void postCapture("first_message", { surface: "cli" });
235644
+ }
235645
+ }
234902
235646
  var POSTHOG_KEY, POSTHOG_HOST, FETCH_TIMEOUT_MS2;
234903
235647
  var init_cliAnalytics = __esm({
234904
235648
  "features/perchTerminal/runtime/cliHost/cliAnalytics.ts"() {
@@ -289205,7 +289949,7 @@ function flockEventToCliRow(event) {
289205
289949
  return {
289206
289950
  id: `flock-${event.flockId}-${event.flockWorkerId}`,
289207
289951
  text: `${event.displayName} \xB7 ${event.nickname} \xB7 ${FLOCK_STATUS_LABELS[event.status] ?? event.status}`,
289208
- tone: event.status === "done" ? "success" : event.status === "failed" ? "danger" : "muted"
289952
+ tone: event.status === "done" ? "success" : event.status === "failed" || event.status === "blocked" ? "danger" : "muted"
289209
289953
  };
289210
289954
  case "flock_run_completed":
289211
289955
  return {
@@ -289326,6 +290070,7 @@ ${HELP_TEXT}`);
289326
290070
  recentMessages: nextRecentMessages,
289327
290071
  contextSnapshot: result3.contextSnapshot ?? persisted?.contextSnapshot ?? null
289328
290072
  });
290073
+ captureCliMessageSent();
289329
290074
  return cliExitCodeForTurn(result3);
289330
290075
  } finally {
289331
290076
  connection.restore();
@@ -289626,6 +290371,7 @@ async function runReadlineInteractivePerchCli(writer, deps, options) {
289626
290371
  terminal: true
289627
290372
  });
289628
290373
  const state = createInteractiveCliState(options);
290374
+ captureCliSessionStarted();
289629
290375
  const connectModelProxy = deps.connectModelProxy ?? connectCliModelProxy;
289630
290376
  let connection = await connectModelProxy({ appUrl: state.appUrl });
289631
290377
  if (!state.appUrl && connection.appUrl) state.appUrl = connection.appUrl;
@@ -289686,6 +290432,7 @@ async function runReadlineInteractivePerchCli(writer, deps, options) {
289686
290432
  });
289687
290433
  if (!opts.systemInitiated) {
289688
290434
  appendRecentMessage(state.recentMessages, "user", turnPrompt);
290435
+ captureCliMessageSent();
289689
290436
  }
289690
290437
  if (result2.assistantText.trim()) {
289691
290438
  appendRecentMessage(state.recentMessages, "assistant", result2.assistantText.trim());
@@ -289778,6 +290525,7 @@ async function runInkInteractivePerchCli(writer, deps, options) {
289778
290525
  const runTurn = deps.runCliTurn ?? runPerchCliTurn;
289779
290526
  const updateNotice = resolveUpdateNotice(CLI_PACKAGE_VERSION);
289780
290527
  captureCliFirstRun(CLI_PACKAGE_VERSION);
290528
+ captureCliSessionStarted();
289781
290529
  const instance = Ink2.render(
289782
290530
  React11.createElement(function PerchInkApp() {
289783
290531
  const app = Ink2.useApp();
@@ -290399,6 +291147,7 @@ async function runInkInteractivePerchCli(writer, deps, options) {
290399
291147
  }
290400
291148
  if (!systemInitiated) {
290401
291149
  appendRecentMessage(state.recentMessages, "user", prompt);
291150
+ captureCliMessageSent();
290402
291151
  }
290403
291152
  if (assistantText) appendRecentMessage(state.recentMessages, "assistant", assistantText);
290404
291153
  trimRecentMessages(state.recentMessages);