perchai-cli 2.4.48 → 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 +1357 -672
  2. package/package.json +1 -1
package/dist/perch.mjs CHANGED
@@ -75918,7 +75918,6 @@ function isTurnAbortedError(error) {
75918
75918
  var TURN_STOPPED_BY_USER_MESSAGE;
75919
75919
  var init_turnAbort = __esm({
75920
75920
  "features/perchTerminal/runtime/turnAbort.ts"() {
75921
- "use strict";
75922
75921
  TURN_STOPPED_BY_USER_MESSAGE = "Turn stopped by user.";
75923
75922
  }
75924
75923
  });
@@ -76224,7 +76223,6 @@ function getToolDisplayName(toolName) {
76224
76223
  var NON_MODULE_TOOL_OWNERS, TOOL_RISK, TOOL_DISPLAY_NAMES;
76225
76224
  var init_catalog = __esm({
76226
76225
  "features/perchTerminal/runtime/toolSystem/catalog.ts"() {
76227
- "use strict";
76228
76226
  init_toolNames();
76229
76227
  NON_MODULE_TOOL_OWNERS = {
76230
76228
  [TOOL_NAMES.listSources]: "lane",
@@ -83486,6 +83484,8 @@ Browser Operator skill contract:
83486
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.
83487
83485
  - Use semantic browser tools for ordinary controls with text, role, or aria labels.
83488
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.
83489
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.
83490
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.
83491
83491
  - Never claim completion from intent alone; require visible proof, save/sent status, URL, receipt, or screenshot evidence.
@@ -83591,17 +83591,20 @@ Rules:
83591
83591
  name: "Browser Operator",
83592
83592
  description: "Operates a logged-in browser turn-by-turn for Quill delivery and web tasks.",
83593
83593
  // Browser work runs on the selected main model. Native vision models see
83594
- // screenshots directly; text-only models can call the plain vision observer
83595
- // 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.
83596
83598
  lane: "chat",
83597
83599
  systemPrompt: `You are Quill's Browser Operator. You operate the user's logged-in browser like a careful human.
83598
83600
 
83599
83601
  Rules:
83600
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.
83601
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.
83602
- - 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.
83603
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.
83604
- - 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.
83605
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".
83606
83609
  - Do not claim completion unless the browser shows a confirmation, URL, saved state, sent state, or other visible receipt.
83607
83610
 
@@ -86236,7 +86239,8 @@ var init_desktopContextSection = __esm({
86236
86239
  BROWSER_EXECUTION_GUIDANCE = `## Browser execution \u2014 logged-in web surface
86237
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.
86238
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.
86239
- 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.
86240
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.`;
86241
86245
  }
86242
86246
  });
@@ -87398,7 +87402,7 @@ var init_threadLedger = __esm({
87398
87402
  });
87399
87403
 
87400
87404
  // features/perchTerminal/runtime/perchMemoryGuidance.ts
87401
- 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;
87402
87406
  var init_perchMemoryGuidance = __esm({
87403
87407
  "features/perchTerminal/runtime/perchMemoryGuidance.ts"() {
87404
87408
  "use strict";
@@ -87413,6 +87417,36 @@ Avoid stock AI phrases: "delve," "nuanced," "robust," "seamless,"
87413
87417
  Be concise when the user is moving fast.
87414
87418
  Preserve the persona: Quill is warm, literate, and direct; Saffron is sharp,
87415
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.
87416
87450
  `.trim();
87417
87451
  SAFFRON_STYLE_CONTRACT = `
87418
87452
  ## Voice contract \u2014 Saffron (visible output only)
@@ -87449,6 +87483,8 @@ You genuinely enjoy finding something interesting buried in a mess.
87449
87483
 
87450
87484
  You're not an assistant. You're the operator who gets the work done.
87451
87485
 
87486
+ ${PERCH_OPERATOR_CAPABILITIES}
87487
+
87452
87488
  ## How you work
87453
87489
 
87454
87490
  Lead with the finding. Skip the preamble. "Seven duplicate payments, same vendor,
@@ -87579,6 +87615,8 @@ the user needs analysis, files, data, an audit, or delivery, you do that work
87579
87615
  yourself with the same tools \u2014 in your own voice. You never punt real work to
87580
87616
  another persona.
87581
87617
 
87618
+ ${PERCH_OPERATOR_CAPABILITIES}
87619
+
87582
87620
  ## How you work
87583
87621
 
87584
87622
  Read first. Before you draft a single sentence, you know what the piece is for,
@@ -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
  }
@@ -119131,32 +119507,6 @@ var init_browserSession = __esm({
119131
119507
  }
119132
119508
  });
119133
119509
 
119134
- // features/perchTerminal/runtime/roost.ts
119135
- function normalizeRoostTier(value) {
119136
- if (value === "max") return "pro_max";
119137
- return isRoostTier(value) ? value : null;
119138
- }
119139
- function normalizeRoostModelChoice(value) {
119140
- if (value === "internal") return "internal";
119141
- return normalizeRoostTier(value);
119142
- }
119143
- function isRoostTier(value) {
119144
- return value === "standard" || value === "standard_max" || value === "pro" || value === "pro_max";
119145
- }
119146
- var ROOST_TIER_LABELS;
119147
- var init_roost = __esm({
119148
- "features/perchTerminal/runtime/roost.ts"() {
119149
- "use strict";
119150
- init_modelRegistry();
119151
- ROOST_TIER_LABELS = {
119152
- standard: "Standard",
119153
- standard_max: "Standard Max",
119154
- pro: "Pro",
119155
- pro_max: "Pro Max"
119156
- };
119157
- }
119158
- });
119159
-
119160
119510
  // features/perchTerminal/runtime/roostUserSelectionClient.ts
119161
119511
  function coerceSelection(raw) {
119162
119512
  const choice = normalizeRoostModelChoice(raw?.choice);
@@ -119196,6 +119546,507 @@ var init_roostUserSelectionClient = __esm({
119196
119546
  }
119197
119547
  });
119198
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
+
119199
120050
  // features/perchTerminal/runtime/modelRouter.ts
119200
120051
  function roostUserSelectionBodyFields() {
119201
120052
  const selection = getEffectiveRoostUserSelection();
@@ -119488,19 +120339,46 @@ async function callModelRouterViaServer(request, opts, endpoint = "/api/perch-te
119488
120339
  durationMs: 0
119489
120340
  };
119490
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
+ }
119491
120365
  async function fetchModelProxyWithRetry(endpoint, init, options) {
119492
120366
  const maxAttempts = Math.max(1, options?.maxAttempts ?? MODEL_PROXY_MAX_ATTEMPTS);
119493
120367
  let lastError = null;
119494
120368
  let authSynced = false;
120369
+ let requestInit = init;
119495
120370
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
119496
120371
  try {
119497
- const response = await fetch(endpoint, init);
119498
- if (response.status === 401 && !authSynced && typeof globalThis.window !== "undefined") {
120372
+ const response = await fetch(endpoint, requestInit);
120373
+ if (response.status === 401 && !authSynced) {
119499
120374
  authSynced = true;
119500
- await syncBrowserAuthSession();
120375
+ const refreshedToken = await syncModelProxyAuthAfter401();
120376
+ if (refreshedToken) {
120377
+ requestInit = withModelProxyAuthHeader(requestInit, refreshedToken);
120378
+ }
119501
120379
  options?.onRetry?.(attempt, "HTTP 401 (session sync)");
119502
120380
  await response.body?.cancel().catch(() => void 0);
119503
- await waitForModelProxyRetry(attempt, init.signal);
120381
+ await waitForModelProxyRetry(attempt, requestInit.signal);
119504
120382
  continue;
119505
120383
  }
119506
120384
  if (!isRetriableModelProxyStatus(response.status) || attempt === maxAttempts - 1) {
@@ -119508,12 +120386,12 @@ async function fetchModelProxyWithRetry(endpoint, init, options) {
119508
120386
  }
119509
120387
  options?.onRetry?.(attempt, `HTTP ${response.status}`);
119510
120388
  await response.body?.cancel().catch(() => void 0);
119511
- await waitForModelProxyRetry(attempt, init.signal);
120389
+ await waitForModelProxyRetry(attempt, requestInit.signal);
119512
120390
  } catch (error) {
119513
120391
  if (isAbortLikeError(error) || attempt === maxAttempts - 1) throw error;
119514
120392
  lastError = error;
119515
120393
  options?.onRetry?.(attempt, error instanceof Error ? error.message : String(error));
119516
- await waitForModelProxyRetry(attempt, init.signal);
120394
+ await waitForModelProxyRetry(attempt, requestInit.signal);
119517
120395
  }
119518
120396
  }
119519
120397
  throw lastError instanceof Error ? lastError : new Error(String(lastError ?? "Model proxy request failed"));
@@ -139132,7 +140010,6 @@ function offSandboxEvent(runId, handler) {
139132
140010
  var LOCAL_WORKSPACE_ACCESS_UNAVAILABLE;
139133
140011
  var init_bridge = __esm({
139134
140012
  "features/perchTerminal/desktop/bridge.ts"() {
139135
- "use strict";
139136
140013
  LOCAL_WORKSPACE_ACCESS_UNAVAILABLE = "Local workspace access is unavailable. Open Perch Desktop or update the app.";
139137
140014
  }
139138
140015
  });
@@ -198995,10 +199872,10 @@ function evaluateBrowserAction(action, observation, context = {}) {
198995
199872
  if (action.kind === "click" && !label && !("role" in action && action.role)) {
198996
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.");
198997
199874
  }
198998
- 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))) {
198999
199876
  return block(
199000
199877
  "repeated_failed_click",
199001
- `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.`
199002
199879
  );
199003
199880
  }
199004
199881
  if (action.kind === "click" && (observation.app === "google_docs" || observation.app === "google_sheets")) {
@@ -199363,6 +200240,17 @@ async function safeBrowserAction(input, runtime = {}) {
199363
200240
  if (target.kind === "ambiguous") {
199364
200241
  lastFailure = fail("selector_ambiguous", target.message, startMs, latestSnapshot.text, latestSnapshot.url, target.candidates);
199365
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
+ }
199366
200254
  lastFailure = fail("selector_not_found", target.message, startMs, latestSnapshot.text, latestSnapshot.url);
199367
200255
  } else {
199368
200256
  const actionResult = await executeBrowserAction(input, target.target, callTool);
@@ -199499,7 +200387,7 @@ function enforceActionPolicy(input, preSnapshot, runtime, startMs) {
199499
200387
  }
199500
200388
  function recordPolicyClickFailure(input, runtime, snapshot) {
199501
200389
  if (!runtime.policy) return;
199502
- if (input.intent !== "click" && input.intent !== "visual_click" && input.intent !== "type") return;
200390
+ if (input.intent !== "click" && input.intent !== "visual_click") return;
199503
200391
  const key = policySessionKey(runtime);
199504
200392
  const signature = `${policySurfaceFingerprint(snapshot)}::${clickSignature(inputToProposedAction(input))}`;
199505
200393
  const existing = failedClickMemory.get(key) ?? [];
@@ -199611,6 +200499,7 @@ async function clickGmailComposeSendButton(callTool) {
199611
200499
  };
199612
200500
  });
199613
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 });
199614
200503
  await page.mouse.click(target.x, target.y);
199615
200504
  return JSON.stringify({ result: "clicked", label: target.label, x: target.x, y: target.y, bounds: target.bounds, viewport: target.viewport });
199616
200505
  }
@@ -199671,7 +200560,8 @@ async function dispatchSyntheticPaste(payload, runtime) {
199671
200560
  () => {
199672
200561
  const html = ${JSON.stringify(payload.html)};
199673
200562
  const plain = ${JSON.stringify(payload.plain)};
199674
- 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();
199675
200565
  if (!active) return false;
199676
200566
  const isLongBody = plain.length > ${TYPE_DIRECT_MAX_CHARS} || plain.includes("\\n");
199677
200567
  if (isLongBody && active.tagName === "INPUT") return false;
@@ -199989,7 +200879,26 @@ async function resolveTargetForAction(input, snapshot, callTool) {
199989
200879
  }
199990
200880
  const resolved = resolveTarget(snapshot.text, input.targetDescriptor);
199991
200881
  if (resolved.kind === "found") return resolved;
199992
- 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
+ }
199993
200902
  return resolved;
199994
200903
  }
199995
200904
  function resolveTarget(snapshot, descriptor) {
@@ -200016,7 +200925,7 @@ function resolveTarget(snapshot, descriptor) {
200016
200925
  if (!match) {
200017
200926
  return {
200018
200927
  kind: "missing",
200019
- 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.`
200020
200929
  };
200021
200930
  }
200022
200931
  if (matches.length > 1 && desc.nth === void 0) {
@@ -200048,7 +200957,7 @@ function resolveTarget(snapshot, descriptor) {
200048
200957
  }
200049
200958
  return {
200050
200959
  kind: "missing",
200051
- 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.`
200052
200961
  };
200053
200962
  }
200054
200963
  function liveResolverSource() {
@@ -200097,17 +201006,50 @@ function liveResolverSource() {
200097
201006
  el.textContent,
200098
201007
  ].filter(Boolean).map((v) => String(v).trim()).find(Boolean) || "";
200099
201008
  };
200100
- const candidates = () => Array.from(document.querySelectorAll([
200101
- "button",
200102
- "a[href]",
200103
- "input",
200104
- "textarea",
200105
- "select",
200106
- "[role]",
200107
- "[aria-label]",
200108
- "[data-tooltip]",
200109
- "[contenteditable='true']"
200110
- ].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
+ };
200111
201053
  const rankMatch = (el, descriptor) => {
200112
201054
  const label = labelFor(el);
200113
201055
  const aria = el.getAttribute("aria-label") || "";
@@ -200145,6 +201087,32 @@ function liveResolverSource() {
200145
201087
  };
200146
201088
  `;
200147
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
+ }
200148
201116
  async function executeBrowserAction(input, target, callTool) {
200149
201117
  if (input.intent === "snapshot" || input.intent === "extract") return { ok: true, matched: input.intent };
200150
201118
  if (input.intent === "navigate") {
@@ -200173,6 +201141,8 @@ async function executeBrowserAction(input, target, callTool) {
200173
201141
  if (!clicked.ok) return clicked;
200174
201142
  const typed = await typeViaRealKeys(callTool, input.text ?? "", clicked.matched);
200175
201143
  if (typed.ok) {
201144
+ const verified = await verifyTypedTextLanded(callTool, input.text ?? "", clicked.matched);
201145
+ if (!verified.ok) return verified;
200176
201146
  return {
200177
201147
  ok: true,
200178
201148
  matched: `${clicked.matched} then typed into focused surface`,
@@ -200182,7 +201152,10 @@ async function executeBrowserAction(input, target, callTool) {
200182
201152
  return typed;
200183
201153
  }
200184
201154
  if (input.intent === "type") {
200185
- 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;
200186
201159
  }
200187
201160
  if (!target) return { ok: false, error: "target could not be resolved" };
200188
201161
  if (target.kind === "selector") {
@@ -200193,8 +201166,24 @@ async function executeBrowserAction(input, target, callTool) {
200193
201166
  }
200194
201167
  return { ok: false, error: `Unsupported browser intent: ${input.intent}` };
200195
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
+ }
200196
201177
  async function clickVisualTarget(callTool, visualTarget) {
200197
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
+ }
200198
201187
  const x = Number(visualTarget.x);
200199
201188
  const y = Number(visualTarget.y);
200200
201189
  if (!Number.isFinite(x) || !Number.isFinite(y)) {
@@ -200212,6 +201201,7 @@ async function clickVisualTarget(callTool, visualTarget) {
200212
201201
  const py = coordinateSpace === "viewport_pixels" ? rawY : rawY * viewport.height;
200213
201202
  const safeX = Math.max(1, Math.min(viewport.width - 1, px));
200214
201203
  const safeY = Math.max(1, Math.min(viewport.height - 1, py));
201204
+ await page.evaluate(${PERCH_CURSOR_OVERLAY_SNIPPET}, { x: safeX, y: safeY });
200215
201205
  await page.mouse.click(safeX, safeY);
200216
201206
  return JSON.stringify({
200217
201207
  result: "clicked",
@@ -200235,17 +201225,78 @@ async function clickVisualTarget(callTool, visualTarget) {
200235
201225
  }
200236
201226
  return { ok: false, error: text || "visual click failed" };
200237
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
+ }
200238
201286
  async function enterText(callTool, target, text) {
200239
201287
  const matched = target?.matched ?? "focused field";
200240
201288
  const isLong = text.length > TYPE_DIRECT_MAX_CHARS || text.includes("\n");
201289
+ const alreadyFocused = await hasEditableFocus(callTool);
200241
201290
  if (!isLong) {
200242
- if (target) {
201291
+ let focusGeometry;
201292
+ if (!alreadyFocused && target) {
200243
201293
  const focused = await focusTarget(callTool, target, matched);
200244
- if (!focused.ok) return focused;
201294
+ if (focused.ok) focusGeometry = focused.actionGeometry;
200245
201295
  }
200246
- return typeViaRealKeys(callTool, text, matched);
201296
+ const typed = await typeViaRealKeys(callTool, text, matched);
201297
+ return typed.ok && focusGeometry ? { ...typed, actionGeometry: focusGeometry } : typed;
200247
201298
  }
200248
- if (target) {
201299
+ if (!alreadyFocused && target) {
200249
201300
  const focused = await focusTarget(callTool, target, matched);
200250
201301
  if (focused.ok) {
200251
201302
  const pasted2 = await dispatchSyntheticPaste({ html: null, plain: text }, { callTool });
@@ -200273,7 +201324,8 @@ function shouldRefuseRealKeys(error) {
200273
201324
  async function typeIntoActiveElement(callTool, text) {
200274
201325
  const json = JSON.stringify(text);
200275
201326
  const expr = `() => {
200276
- 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();
200277
201329
  if (!el) throw new Error("no active element to type into");
200278
201330
  const isLongBody = ${JSON.stringify(text.length > TYPE_DIRECT_MAX_CHARS || text.includes("\n"))};
200279
201331
  if (isLongBody && el.tagName === "INPUT") {
@@ -200359,6 +201411,7 @@ async function clickTargetTrusted(callTool, target, matched) {
200359
201411
  };
200360
201412
  }, { descriptor: ${JSON.stringify(descriptor)}, selector: ${JSON.stringify(selector)} });
200361
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 });
200362
201415
  await page.mouse.click(target.x, target.y);
200363
201416
  return JSON.stringify({ result: "clicked", label: target.label, x: target.x, y: target.y, bounds: target.bounds, viewport: target.viewport });
200364
201417
  }`;
@@ -200686,7 +201739,7 @@ function delay(ms, signal) {
200686
201739
  );
200687
201740
  });
200688
201741
  }
200689
- 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;
200690
201743
  var init_safeBrowserAction = __esm({
200691
201744
  "features/perchTerminal/runtime/playwright/safeBrowserAction.ts"() {
200692
201745
  "use strict";
@@ -200713,6 +201766,26 @@ var init_safeBrowserAction = __esm({
200713
201766
  ];
200714
201767
  failedClickMemory = /* @__PURE__ */ new Map();
200715
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
+ }`;
200716
201789
  TYPE_DIRECT_MAX_CHARS = 200;
200717
201790
  PRESS_KEY_LOOP_MAX_CHARS = 4e3;
200718
201791
  }
@@ -213692,8 +214765,8 @@ var init_barSeries = __esm({
213692
214765
  });
213693
214766
 
213694
214767
  // lib/perchMarketDesk/data/fixtureStore.ts
213695
- import fs9 from "node:fs";
213696
- import path10 from "node:path";
214768
+ import fs10 from "node:fs";
214769
+ import path11 from "node:path";
213697
214770
  function createFixtureStore(input) {
213698
214771
  const instruments = [...input.instruments].sort((a, b2) => a.ticker.localeCompare(b2.ticker));
213699
214772
  const seriesByTicker = /* @__PURE__ */ new Map();
@@ -213740,22 +214813,22 @@ function createFixtureStore(input) {
213740
214813
  };
213741
214814
  }
213742
214815
  function loadFixtureStoreFromDir(dir) {
213743
- const instrumentsPath = path10.join(dir, "instruments.json");
213744
- 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"));
213745
214818
  const bars = [];
213746
- const barsDir = path10.join(dir, "bars");
213747
- for (const file of fs9.readdirSync(barsDir)) {
214819
+ const barsDir = path11.join(dir, "bars");
214820
+ for (const file of fs10.readdirSync(barsDir)) {
213748
214821
  if (!file.endsWith(".json")) continue;
213749
- const ticker = path10.basename(file, ".json");
213750
- 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"));
213751
214824
  for (const row of rows) bars.push({ ...row, ticker: row.ticker ?? ticker });
213752
214825
  }
213753
214826
  let fundamentals;
213754
- const fundamentalsPath = path10.join(dir, "fundamentals.json");
213755
- if (fs9.existsSync(fundamentalsPath)) {
213756
- 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"));
213757
214830
  }
213758
- return createFixtureStore({ instruments, bars, fundamentals, label: `dir:${path10.basename(dir)}` });
214831
+ return createFixtureStore({ instruments, bars, fundamentals, label: `dir:${path11.basename(dir)}` });
213759
214832
  }
213760
214833
  var init_fixtureStore = __esm({
213761
214834
  "lib/perchMarketDesk/data/fixtureStore.ts"() {
@@ -218837,9 +219910,19 @@ async function executeToolBatch(toolCalls, ctx) {
218837
219910
  { type: "image_url", image_url: { url: execution.imageDataUrl } }
218838
219911
  ]
218839
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
+ });
218840
219921
  } else {
218841
- 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.`;
218842
- 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
+ });
218843
219926
  }
218844
219927
  }
218845
219928
  }
@@ -218860,6 +219943,26 @@ function parseImageDataUrlForTool(dataUrl) {
218860
219943
  base64
218861
219944
  };
218862
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
+ }
218863
219966
  function currentScreenVisionToolCall(input) {
218864
219967
  const { toolCall, imageDataUrl } = input;
218865
219968
  if (toolCall.name !== TOOL_NAMES.visionInspect) return { toolCall };
@@ -218918,11 +220021,12 @@ function updateConsecutiveRequiredArgFailures(counts, execution) {
218918
220021
  function now6() {
218919
220022
  return (/* @__PURE__ */ new Date()).toISOString();
218920
220023
  }
218921
- var MAX_PARALLEL_TOOL_CALLS, LOCAL_VISUAL_ATTACHMENT_MARKER;
220024
+ var MAX_PARALLEL_TOOL_CALLS, LOCAL_VISUAL_ATTACHMENT_MARKER, OPERATOR_VISION_OBSERVER_PROMPT;
218922
220025
  var init_toolBatchExecutor = __esm({
218923
220026
  "features/perchTerminal/runtime/toolLoop/toolBatchExecutor.ts"() {
218924
220027
  "use strict";
218925
220028
  init_modelRegistry();
220029
+ init_visionLaneCall();
218926
220030
  init_toolExecutor();
218927
220031
  init_fileDiff();
218928
220032
  init_toolNames();
@@ -218935,6 +220039,14 @@ var init_toolBatchExecutor = __esm({
218935
220039
  init_sourceReadPolicy();
218936
220040
  MAX_PARALLEL_TOOL_CALLS = 8;
218937
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(" ");
218938
220050
  }
218939
220051
  });
218940
220052
 
@@ -219379,8 +220491,11 @@ async function prepareLoopMessagesForSend(input) {
219379
220491
  toolDefinitionsJson,
219380
220492
  modelContext
219381
220493
  });
219382
- const contextLimitTokens = resolveEffectiveContextLimit(input.option);
219383
- 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);
219384
220499
  const postCompactTargetTokens = resolvePostCompactTargetTokens(contextLimitTokens);
219385
220500
  const recentTailTokenBudget = resolveRecentTailBudgetTokens({
219386
220501
  postCompactTargetTokens,
@@ -219406,11 +220521,6 @@ async function prepareLoopMessagesForSend(input) {
219406
220521
  toolDefinitionsJson,
219407
220522
  modelContext
219408
220523
  });
219409
- input.onEvent?.({
219410
- type: "activity_delta",
219411
- text: `Cleared ${microcompactedToolResults} old tool result(s) to free context.`,
219412
- ts: (/* @__PURE__ */ new Date()).toISOString()
219413
- });
219414
220524
  }
219415
220525
  }
219416
220526
  if (autoCompactEnabled && anchoredTokens(breakdown.total) >= compactThreshold) {
@@ -223197,6 +224307,7 @@ async function runFlockTurn(input, deps, options = {}) {
223197
224307
  emitWorkerUpdate(emit, plan, worker, "queued");
223198
224308
  }
223199
224309
  const spawnFn = options.spawnWorkerFn ?? spawnWorker;
224310
+ let turnInput = input;
223200
224311
  const outcomes = [];
223201
224312
  const revisionOutcomes = [];
223202
224313
  let revisionReport = null;
@@ -223245,6 +224356,7 @@ async function runFlockTurn(input, deps, options = {}) {
223245
224356
  toolCallsReserved += reservedToolCalls;
223246
224357
  emitWorkerUpdate(emit, plan, worker, "running");
223247
224358
  try {
224359
+ turnInput = await refreshFlockCliAuth(turnInput);
223248
224360
  const context = contextOverride ?? buildWorkerContext(worker, sharedContext, outputByFlockWorkerId, plan);
223249
224361
  const objective = worker.role === "worker" && contextContainsSources(context) ? `${worker.objective}
223250
224362
  ${FLOCK_GROUNDING_CONTRACT}` : worker.objective;
@@ -223256,18 +224368,18 @@ ${FLOCK_GROUNDING_CONTRACT}` : worker.objective;
223256
224368
  maxIterations: worker.maxIterations,
223257
224369
  maxToolCalls: reservedToolCalls
223258
224370
  },
223259
- buildSpawnContext(input, plan.flockId, flockRun.controller.signal, emit, worker)
224371
+ buildSpawnContext(turnInput, plan.flockId, flockRun.controller.signal, emit, worker)
223260
224372
  );
223261
224373
  toolCallsReserved -= reservedToolCalls;
223262
224374
  toolCallsUsed += Math.min(result2.toolCalls ?? 0, reservedToolCalls);
223263
- const ok = result2.ok;
224375
+ const status = flockWorkerStatusFromSpawnResult(result2);
223264
224376
  const outcome = {
223265
224377
  worker,
223266
224378
  result: result2,
223267
- status: ok ? "done" : "failed",
224379
+ status,
223268
224380
  detail: clampLine(result2.summary, 200)
223269
224381
  };
223270
- emitWorkerUpdate(emit, plan, worker, ok ? "done" : "failed", clampLine(result2.summary, 160));
224382
+ emitWorkerUpdate(emit, plan, worker, status, clampLine(result2.summary, 160));
223271
224383
  return outcome;
223272
224384
  } catch (error) {
223273
224385
  toolCallsReserved -= reservedToolCalls;
@@ -223367,8 +224479,10 @@ ${FLOCK_GROUNDING_CONTRACT}` : worker.objective;
223367
224479
  }
223368
224480
  const userCancelled = Boolean(deps.signal?.aborted);
223369
224481
  const workersDone = outcomes.filter((outcome) => outcome.status === "done").length;
223370
- 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;
223371
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." : "";
223372
224486
  const assistantText = [
223373
224487
  buildFlockSummary(
223374
224488
  plan,
@@ -223384,7 +224498,7 @@ ${FLOCK_GROUNDING_CONTRACT}` : worker.objective;
223384
224498
  ...modelOverrides.unavailable.map(
223385
224499
  (override) => `Requested model "${override.modelText}"${override.roleText ? ` for ${override.roleText}` : ""} is not available \u2014 that worker stayed on the default model path.`
223386
224500
  )
223387
- ].join("\n");
224501
+ ].join("\n") + permissionHint;
223388
224502
  emit({
223389
224503
  type: "flock_run_completed",
223390
224504
  flockId: plan.flockId,
@@ -223618,6 +224732,12 @@ function buildFlockSummary(plan, outcomes, status, toolCallsUsed, wallTimeHit) {
223618
224732
  function friendlyOutcomeDetail(detail) {
223619
224733
  const clean = clampLine(detail, 220);
223620
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
+ }
223621
224741
  if (/Reached maximum \d+ tool-call iterations/i.test(clean)) {
223622
224742
  return "finished from gathered evidence";
223623
224743
  }
@@ -223636,6 +224756,29 @@ function clampLine(text, max2) {
223636
224756
  function makeFlockRunId() {
223637
224757
  return `flock-run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
223638
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
+ }
223639
224782
  function now11() {
223640
224783
  return (/* @__PURE__ */ new Date()).toISOString();
223641
224784
  }
@@ -223646,6 +224789,8 @@ var init_runFlockTurn = __esm({
223646
224789
  init_operatorStream();
223647
224790
  init_runRegistry();
223648
224791
  init_spawnWorker2();
224792
+ init_cliAuthSession();
224793
+ init_cliAuthRefresh();
223649
224794
  init_registry();
223650
224795
  init_flockCommand();
223651
224796
  init_flockLimits();
@@ -231959,9 +233104,9 @@ var init_backgroundTaskTypes = __esm({
231959
233104
  });
231960
233105
 
231961
233106
  // features/perchTerminal/runtime/background/backgroundTaskRegistry.ts
231962
- import fs10 from "node:fs";
231963
- import os from "node:os";
231964
- import path11 from "node:path";
233107
+ import fs11 from "node:fs";
233108
+ import os2 from "node:os";
233109
+ import path12 from "node:path";
231965
233110
  function isTerminalStatus(status) {
231966
233111
  return status !== "running";
231967
233112
  }
@@ -232018,19 +233163,19 @@ function runtimeMs(record) {
232018
233163
  function readOutputPreview(outputPath, previewBytes = BACKGROUND_TASK_BOUNDS.previewBytes) {
232019
233164
  if (!outputPath) return { preview: "", truncated: false };
232020
233165
  try {
232021
- const stat2 = fs10.statSync(outputPath);
232022
- const fd = fs10.openSync(outputPath, "r");
233166
+ const stat2 = fs11.statSync(outputPath);
233167
+ const fd = fs11.openSync(outputPath, "r");
232023
233168
  try {
232024
233169
  if (stat2.size <= previewBytes) {
232025
233170
  const buf2 = Buffer.alloc(stat2.size);
232026
- fs10.readSync(fd, buf2, 0, stat2.size, 0);
233171
+ fs11.readSync(fd, buf2, 0, stat2.size, 0);
232027
233172
  return { preview: buf2.toString("utf8"), truncated: false };
232028
233173
  }
232029
233174
  const buf = Buffer.alloc(previewBytes);
232030
- fs10.readSync(fd, buf, 0, previewBytes, stat2.size - previewBytes);
233175
+ fs11.readSync(fd, buf, 0, previewBytes, stat2.size - previewBytes);
232031
233176
  return { preview: buf.toString("utf8"), truncated: true };
232032
233177
  } finally {
232033
- fs10.closeSync(fd);
233178
+ fs11.closeSync(fd);
232034
233179
  }
232035
233180
  } catch {
232036
233181
  return { preview: "", truncated: false };
@@ -232065,26 +233210,28 @@ function getBackgroundTaskStore() {
232065
233210
  var STORE_FILE, BackgroundTaskStore, singleton;
232066
233211
  var init_backgroundTaskRegistry = __esm({
232067
233212
  "features/perchTerminal/runtime/background/backgroundTaskRegistry.ts"() {
232068
- "use strict";
232069
233213
  init_backgroundTaskTypes();
232070
233214
  STORE_FILE = "background-tasks.json";
232071
233215
  BackgroundTaskStore = class {
233216
+ baseDir;
233217
+ storePath;
233218
+ outputDir;
233219
+ records = /* @__PURE__ */ new Map();
233220
+ loaded = false;
232072
233221
  constructor(options = {}) {
232073
- this.records = /* @__PURE__ */ new Map();
232074
- this.loaded = false;
232075
- this.baseDir = options.baseDir ?? path11.join(os.homedir(), ".perch");
232076
- this.storePath = path11.join(this.baseDir, STORE_FILE);
232077
- 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");
232078
233225
  }
232079
233226
  ensureDirs() {
232080
- fs10.mkdirSync(this.baseDir, { recursive: true });
232081
- fs10.mkdirSync(this.outputDir, { recursive: true });
233227
+ fs11.mkdirSync(this.baseDir, { recursive: true });
233228
+ fs11.mkdirSync(this.outputDir, { recursive: true });
232082
233229
  }
232083
233230
  load() {
232084
233231
  if (this.loaded) return;
232085
233232
  this.loaded = true;
232086
233233
  try {
232087
- const raw = fs10.readFileSync(this.storePath, "utf8");
233234
+ const raw = fs11.readFileSync(this.storePath, "utf8");
232088
233235
  const parsed = JSON.parse(raw);
232089
233236
  const tasks = Array.isArray(parsed.tasks) ? parsed.tasks : [];
232090
233237
  this.records = new Map(tasks.map((t) => [t.taskId, t]));
@@ -232096,11 +233243,11 @@ var init_backgroundTaskRegistry = __esm({
232096
233243
  this.ensureDirs();
232097
233244
  const tasks = Array.from(this.records.values());
232098
233245
  const tmp = `${this.storePath}.tmp`;
232099
- fs10.writeFileSync(tmp, JSON.stringify({ version: 1, tasks }, null, 2), "utf8");
232100
- fs10.renameSync(tmp, this.storePath);
233246
+ fs11.writeFileSync(tmp, JSON.stringify({ version: 1, tasks }, null, 2), "utf8");
233247
+ fs11.renameSync(tmp, this.storePath);
232101
233248
  }
232102
233249
  outputPathFor(taskId) {
232103
- return path11.join(this.outputDir, `${taskId}.log`);
233250
+ return path12.join(this.outputDir, `${taskId}.log`);
232104
233251
  }
232105
233252
  list() {
232106
233253
  this.load();
@@ -232262,14 +233409,14 @@ var init_backgroundWakeController = __esm({
232262
233409
 
232263
233410
  // features/perchTerminal/runtime/background/runBackgroundCommand.ts
232264
233411
  import { spawn as spawn2 } from "node:child_process";
232265
- import fs11 from "node:fs";
232266
- import path12 from "node:path";
233412
+ import fs12 from "node:fs";
233413
+ import path13 from "node:path";
232267
233414
  function newTaskId() {
232268
233415
  return `bg-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
232269
233416
  }
232270
233417
  function createCappedWriter(filePath, maxBytes) {
232271
- fs11.mkdirSync(path12.dirname(filePath), { recursive: true });
232272
- const stream = fs11.createWriteStream(filePath, { flags: "w" });
233418
+ fs12.mkdirSync(path13.dirname(filePath), { recursive: true });
233419
+ const stream = fs12.createWriteStream(filePath, { flags: "w" });
232273
233420
  let written = 0;
232274
233421
  let truncated = false;
232275
233422
  return {
@@ -232442,10 +233589,10 @@ var init_runBackgroundCommand = __esm({
232442
233589
 
232443
233590
  // features/perchTerminal/runtime/cliHost/nodeLocalBridge.ts
232444
233591
  import { spawn as spawn3 } from "node:child_process";
232445
- import fs12 from "node:fs";
233592
+ import fs13 from "node:fs";
232446
233593
  import fsp from "node:fs/promises";
232447
- import os2 from "node:os";
232448
- import path13 from "node:path";
233594
+ import os3 from "node:os";
233595
+ import path14 from "node:path";
232449
233596
  function installCliNodeLocalBridge(input) {
232450
233597
  const globalRef = globalThis;
232451
233598
  const previous = globalRef.__PERCH_TEST_DESKTOP_BRIDGE__;
@@ -232459,7 +233606,7 @@ function installCliNodeLocalBridge(input) {
232459
233606
  };
232460
233607
  }
232461
233608
  function readCliProjectMemoryState(workspaceRoot) {
232462
- const root2 = path13.resolve(expandHome4(workspaceRoot));
233609
+ const root2 = path14.resolve(expandHome4(workspaceRoot));
232463
233610
  if (!isCliProjectMemoryAvailable(root2)) {
232464
233611
  return {
232465
233612
  available: false,
@@ -232474,7 +233621,7 @@ function readCliProjectMemoryState(workspaceRoot) {
232474
233621
  };
232475
233622
  }
232476
233623
  function createCliNodeLocalBridge(input) {
232477
- const workspaceRoot = path13.resolve(input.workspaceRoot);
233624
+ const workspaceRoot = path14.resolve(input.workspaceRoot);
232478
233625
  const now23 = () => (/* @__PURE__ */ new Date()).toISOString();
232479
233626
  const sandboxHandlers = /* @__PURE__ */ new Map();
232480
233627
  const emitSandboxEvent = (event) => {
@@ -232488,7 +233635,7 @@ function createCliNodeLocalBridge(input) {
232488
233635
  cwd: workspaceRoot,
232489
233636
  projectRoot: workspaceRoot,
232490
233637
  shellPath: process.env.SHELL || "/bin/zsh",
232491
- shellName: path13.basename(process.env.SHELL || "zsh"),
233638
+ shellName: path14.basename(process.env.SHELL || "zsh"),
232492
233639
  path: process.env.PATH || "",
232493
233640
  status: "running",
232494
233641
  backend: "spawn-fallback",
@@ -232515,7 +233662,7 @@ function createCliNodeLocalBridge(input) {
232515
233662
  capabilities: ["local-files", "local-shell"]
232516
233663
  }),
232517
233664
  checkFsAccess: async (request) => {
232518
- const absolutePath = path13.resolve(expandHome4(request.absolutePath));
233665
+ const absolutePath = path14.resolve(expandHome4(request.absolutePath));
232519
233666
  return {
232520
233667
  decision: "allow",
232521
233668
  reason: "Allowed by Perch CLI local filesystem access.",
@@ -232533,7 +233680,7 @@ function createCliNodeLocalBridge(input) {
232533
233680
  ok: true,
232534
233681
  directory: workspaceRoot,
232535
233682
  fileName: request.fileName,
232536
- absolutePath: path13.join(workspaceRoot, request.fileName),
233683
+ absolutePath: path14.join(workspaceRoot, request.fileName),
232537
233684
  source: "approved_root"
232538
233685
  }),
232539
233686
  validateWorkspaceRoot: async () => ({
@@ -232622,7 +233769,7 @@ function createCliNodeLocalBridge(input) {
232622
233769
  writeWorkspaceFile: async (request) => {
232623
233770
  const baseRoot = resolveRequestRoot(workspaceRoot, request.workspaceRoot);
232624
233771
  const target = resolveWritePath(baseRoot, request.relativePath);
232625
- await fsp.mkdir(path13.dirname(target), { recursive: true });
233772
+ await fsp.mkdir(path14.dirname(target), { recursive: true });
232626
233773
  const flag = request.overwrite === true ? "w" : "wx";
232627
233774
  const payload = request.encoding === "base64" && typeof request.contentBase64 === "string" ? Buffer.from(request.contentBase64, "base64") : request.content ?? "";
232628
233775
  try {
@@ -232642,14 +233789,14 @@ function createCliNodeLocalBridge(input) {
232642
233789
  moveLocalFile: async (request) => {
232643
233790
  const src = resolveReadPath(workspaceRoot, request.src);
232644
233791
  const dest = resolveWritePath(workspaceRoot, request.dest);
232645
- await fsp.mkdir(path13.dirname(dest), { recursive: true });
233792
+ await fsp.mkdir(path14.dirname(dest), { recursive: true });
232646
233793
  await fsp.rename(src, dest);
232647
233794
  return { ok: true, fromPath: src, toPath: dest };
232648
233795
  },
232649
233796
  copyLocalFile: async (request) => {
232650
233797
  const src = resolveReadPath(workspaceRoot, request.src);
232651
233798
  const dest = resolveWritePath(workspaceRoot, request.dest);
232652
- await fsp.mkdir(path13.dirname(dest), { recursive: true });
233799
+ await fsp.mkdir(path14.dirname(dest), { recursive: true });
232653
233800
  await fsp.copyFile(src, dest);
232654
233801
  return { ok: true, fromPath: src, toPath: dest };
232655
233802
  },
@@ -232674,7 +233821,7 @@ function createCliNodeLocalBridge(input) {
232674
233821
  const pattern = request.pattern?.trim() || "**/*";
232675
233822
  const regex2 = globToRegex(pattern);
232676
233823
  const files = await collectFiles(root2, { maxVisits: Math.max(maxResults * 20, 1e3) });
232677
- 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);
232678
233825
  return {
232679
233826
  ok: true,
232680
233827
  matches,
@@ -232697,7 +233844,7 @@ function createCliNodeLocalBridge(input) {
232697
233844
  const files = await collectFiles(root2, { maxVisits: 5e3 });
232698
233845
  const matches = [];
232699
233846
  for (const file of files) {
232700
- const relative2 = path13.relative(root2, file).replace(/\\/g, "/");
233847
+ const relative2 = path14.relative(root2, file).replace(/\\/g, "/");
232701
233848
  if (includeRegex && !includeRegex.test(relative2)) continue;
232702
233849
  const text = await readTextFileIfReasonable(file);
232703
233850
  if (text === null) continue;
@@ -232736,7 +233883,7 @@ function createCliNodeLocalBridge(input) {
232736
233883
  const stat2 = await safeStat(target);
232737
233884
  return {
232738
233885
  ok: true,
232739
- 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) || ".",
232740
233887
  exists: Boolean(stat2),
232741
233888
  isFile: stat2?.isFile() ?? false,
232742
233889
  isDirectory: stat2?.isDirectory() ?? false,
@@ -232770,7 +233917,7 @@ function createCliNodeLocalBridge(input) {
232770
233917
  return {
232771
233918
  ok: true,
232772
233919
  data: buffer.toString("base64"),
232773
- fileName: path13.relative(workspaceRoot, target) || path13.basename(target),
233920
+ fileName: path14.relative(workspaceRoot, target) || path14.basename(target),
232774
233921
  mimeType: inferMimeType(target),
232775
233922
  sizeBytes: stat2.size,
232776
233923
  encoding: "base64"
@@ -232866,7 +234013,7 @@ async function runCliPrepareAPEvidence(workspaceRoot, request) {
232866
234013
  topCases: (artifact.cases ?? []).slice(0, 12),
232867
234014
  duplicateCases: result2.payload.duplicateCases,
232868
234015
  controlGraph: result2.payload.controlGraph,
232869
- relativeOutputDir: typeof artifact.outputDir === "string" ? path13.basename(artifact.outputDir) : null,
234016
+ relativeOutputDir: typeof artifact.outputDir === "string" ? path14.basename(artifact.outputDir) : null,
232870
234017
  approvedRootMatch: true
232871
234018
  },
232872
234019
  executionHost: "electron_desktop",
@@ -232955,7 +234102,7 @@ async function prepareCliSandboxInputs(workspaceRoot, request) {
232955
234102
  const jobId = request.jobId?.trim() || `cli-sandbox-${Date.now()}`;
232956
234103
  const maxFiles = Math.max(1, Math.min(request.maxFiles ?? 20, 100));
232957
234104
  const maxBytesPerFile = Math.max(1, Math.min(request.maxBytesPerFile ?? 20 * 1024 * 1024, 50 * 1024 * 1024));
232958
- const tempDir = await fsp.mkdtemp(path13.join(os2.tmpdir(), `${jobId}-`));
234105
+ const tempDir = await fsp.mkdtemp(path14.join(os3.tmpdir(), `${jobId}-`));
232959
234106
  const entries = [];
232960
234107
  const skipped = [];
232961
234108
  for (const localSourceId of request.localSourceIds.slice(0, maxFiles)) {
@@ -232969,13 +234116,13 @@ async function prepareCliSandboxInputs(workspaceRoot, request) {
232969
234116
  skipped.push({ localSourceId, reason: `File is too large: ${stat2.size} bytes.` });
232970
234117
  continue;
232971
234118
  }
232972
- const relativePath = safeSandboxRelativePath(path13.isAbsolute(localSourceId) || localSourceId.startsWith("~") ? path13.basename(sourcePath) : path13.relative(workspaceRoot, sourcePath));
232973
- const tempPath = path13.join(tempDir, relativePath);
232974
- 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 });
232975
234122
  await fsp.copyFile(sourcePath, tempPath);
232976
234123
  entries.push({
232977
234124
  localSourceId,
232978
- fileName: path13.basename(sourcePath),
234125
+ fileName: path14.basename(sourcePath),
232979
234126
  relativePath,
232980
234127
  tempPath,
232981
234128
  sizeBytes: stat2.size,
@@ -233013,11 +234160,11 @@ async function runCliSandboxCodeJob(workspaceRoot, request, onEvent) {
233013
234160
  getApprovedRoot: (rootId) => rootId === CLI_ROOT_ID ? { id: CLI_ROOT_ID, path: workspaceRoot, approvedAt: (/* @__PURE__ */ new Date()).toISOString() } : null,
233014
234161
  isPathSafe: (_rootPath, targetPath) => !targetPath.split(/[\\/]+/).includes(".."),
233015
234162
  isInsideApprovedRoot: (filePath) => {
233016
- const absolute = path13.resolve(expandHome4(filePath));
234163
+ const absolute = path14.resolve(expandHome4(filePath));
233017
234164
  if (isInside(workspaceRoot, absolute)) {
233018
234165
  return { id: CLI_ROOT_ID, path: workspaceRoot, approvedAt: (/* @__PURE__ */ new Date()).toISOString() };
233019
234166
  }
233020
- 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() };
233021
234168
  },
233022
234169
  shouldIgnore: (fileName) => IGNORED_DIRS.has(fileName),
233023
234170
  guessMimeType: inferMimeType,
@@ -233060,7 +234207,7 @@ function safeSandboxRelativePath(value) {
233060
234207
  return clean || "input";
233061
234208
  }
233062
234209
  function guessCliFileType(filePath) {
233063
- const ext = path13.extname(filePath).toLowerCase();
234210
+ const ext = path14.extname(filePath).toLowerCase();
233064
234211
  if ([".xlsx", ".xls"].includes(ext)) return "spreadsheet";
233065
234212
  if (ext === ".csv" || ext === ".tsv") return "csv";
233066
234213
  if (ext === ".pdf") return "pdf";
@@ -233079,7 +234226,7 @@ async function readCliProjectMemoryBridge(workspaceRoot) {
233079
234226
  return { ok: true, meta: state.meta };
233080
234227
  }
233081
234228
  async function writeCliProjectMemory(workspaceRoot, patch) {
233082
- const root2 = path13.resolve(expandHome4(workspaceRoot));
234229
+ const root2 = path14.resolve(expandHome4(workspaceRoot));
233083
234230
  if (!isCliProjectMemoryAvailable(root2)) {
233084
234231
  return {
233085
234232
  ok: false,
@@ -233095,12 +234242,12 @@ async function writeCliProjectMemory(workspaceRoot, patch) {
233095
234242
  lastOpenedThreadId: patch.lastOpenedThreadId !== void 0 ? patch.lastOpenedThreadId : current.lastOpenedThreadId,
233096
234243
  memorySummary: patch.memorySummary !== void 0 ? patch.memorySummary : current.memorySummary
233097
234244
  };
233098
- await fsp.mkdir(path13.join(root2, PERCH_DIR), { recursive: true });
234245
+ await fsp.mkdir(path14.join(root2, PERCH_DIR), { recursive: true });
233099
234246
  await atomicWriteCliUtf8(getCliProjectFilePath(root2), JSON.stringify(next, null, 2));
233100
234247
  return { ok: true, meta: enrichCliProjectMeta(root2, next) };
233101
234248
  }
233102
234249
  async function writeCliMemoryFile(workspaceRoot, request) {
233103
- const root2 = path13.resolve(expandHome4(workspaceRoot));
234250
+ const root2 = path14.resolve(expandHome4(workspaceRoot));
233104
234251
  if (!isCliProjectMemoryAvailable(root2)) {
233105
234252
  return {
233106
234253
  ok: false,
@@ -233112,8 +234259,8 @@ async function writeCliMemoryFile(workspaceRoot, request) {
233112
234259
  }
233113
234260
  const fileName = normalizeCliMemoryFileName(request.fileName);
233114
234261
  if (!fileName) return { ok: false, error: "Invalid memory file name." };
233115
- const relativePath = path13.join(PERCH_DIR, MEMORY_DIR, fileName);
233116
- const fullPath = path13.join(root2, relativePath);
234262
+ const relativePath = path14.join(PERCH_DIR, MEMORY_DIR, fileName);
234263
+ const fullPath = path14.join(root2, relativePath);
233117
234264
  const existing = await fsp.readFile(fullPath, "utf8").catch(() => "");
233118
234265
  let nextContent;
233119
234266
  try {
@@ -233126,7 +234273,7 @@ async function writeCliMemoryFile(workspaceRoot, request) {
233126
234273
  }
233127
234274
  const capError = assertCliMemoryWithinCap(nextContent);
233128
234275
  if (capError) return { ok: false, error: capError };
233129
- await fsp.mkdir(path13.dirname(fullPath), { recursive: true });
234276
+ await fsp.mkdir(path14.dirname(fullPath), { recursive: true });
233130
234277
  await atomicWriteCliUtf8(fullPath, nextContent);
233131
234278
  const updatedFile = readCliTextMemoryFile(root2, relativePath, MAX_MEMORY_BYTES);
233132
234279
  return {
@@ -233152,17 +234299,17 @@ function defaultCliProjectMeta() {
233152
234299
  }
233153
234300
  function isCliProjectMemoryAvailable(root2) {
233154
234301
  try {
233155
- return fs12.statSync(path13.join(root2, PERCH_DIR)).isDirectory();
234302
+ return fs13.statSync(path14.join(root2, PERCH_DIR)).isDirectory();
233156
234303
  } catch {
233157
234304
  return false;
233158
234305
  }
233159
234306
  }
233160
234307
  function getCliProjectFilePath(root2) {
233161
- return path13.join(root2, PERCH_DIR, PROJECT_FILE);
234308
+ return path14.join(root2, PERCH_DIR, PROJECT_FILE);
233162
234309
  }
233163
234310
  function loadCliStoredMeta(root2) {
233164
234311
  try {
233165
- const raw = fs12.readFileSync(getCliProjectFilePath(root2), "utf8");
234312
+ const raw = fs13.readFileSync(getCliProjectFilePath(root2), "utf8");
233166
234313
  return { ...defaultCliProjectMeta(), ...JSON.parse(raw) };
233167
234314
  } catch {
233168
234315
  return defaultCliProjectMeta();
@@ -233170,7 +234317,7 @@ function loadCliStoredMeta(root2) {
233170
234317
  }
233171
234318
  function enrichCliProjectMeta(root2, base) {
233172
234319
  const perchMd = readCliFirstExisting(root2, [
233173
- path13.join(PERCH_DIR, PERCH_INDEX_FILE),
234320
+ path14.join(PERCH_DIR, PERCH_INDEX_FILE),
233174
234321
  PERCH_INDEX_FILE
233175
234322
  ], MAX_TEXT_BYTES);
233176
234323
  const rules = readCliRules(root2);
@@ -233281,10 +234428,10 @@ function readCliFirstExisting(root2, relativePaths, maxBytes) {
233281
234428
  return null;
233282
234429
  }
233283
234430
  function readCliRules(root2) {
233284
- const dirPath = path13.join(root2, PERCH_DIR, RULES_DIR);
234431
+ const dirPath = path14.join(root2, PERCH_DIR, RULES_DIR);
233285
234432
  try {
233286
- 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) => ({
233287
- 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),
233288
234435
  relativePath: file.relativePath,
233289
234436
  content: file.content,
233290
234437
  sizeBytes: file.sizeBytes,
@@ -233295,15 +234442,15 @@ function readCliRules(root2) {
233295
234442
  }
233296
234443
  }
233297
234444
  function readCliMemoryFiles(root2) {
233298
- const memoryDir = path13.join(root2, PERCH_DIR, MEMORY_DIR);
234445
+ const memoryDir = path14.join(root2, PERCH_DIR, MEMORY_DIR);
233299
234446
  const discovered = /* @__PURE__ */ new Set();
233300
234447
  const memoryFiles = [];
233301
234448
  try {
233302
- for (const name of fs12.readdirSync(memoryDir).slice(0, MAX_MEMORY_FILES)) {
233303
- 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();
233304
234451
  if (ext !== ".md" && ext !== ".txt") continue;
233305
234452
  discovered.add(name);
233306
- 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));
233307
234454
  }
233308
234455
  } catch {
233309
234456
  }
@@ -233311,20 +234458,20 @@ function readCliMemoryFiles(root2) {
233311
234458
  memoryFiles,
233312
234459
  missingMemoryFiles: EXPECTED_MEMORY_FILES.filter((name) => !discovered.has(name)).map((name) => ({
233313
234460
  fileName: name,
233314
- relativePath: path13.join(PERCH_DIR, MEMORY_DIR, name),
234461
+ relativePath: path14.join(PERCH_DIR, MEMORY_DIR, name),
233315
234462
  reason: "expected project memory file was not found"
233316
234463
  }))
233317
234464
  };
233318
234465
  }
233319
234466
  function readCliTextMemoryFile(root2, relativePath, maxBytes) {
233320
- const fullPath = path13.join(root2, relativePath);
233321
- const fileName = path13.basename(relativePath);
234467
+ const fullPath = path14.join(root2, relativePath);
234468
+ const fileName = path14.basename(relativePath);
233322
234469
  try {
233323
- const stat2 = fs12.statSync(fullPath);
234470
+ const stat2 = fs13.statSync(fullPath);
233324
234471
  if (!stat2.isFile()) {
233325
234472
  return { fileName, relativePath, content: "", found: false, error: "not a regular file" };
233326
234473
  }
233327
- const content = fs12.readFileSync(fullPath, "utf8");
234474
+ const content = fs13.readFileSync(fullPath, "utf8");
233328
234475
  if (stat2.size > maxBytes) {
233329
234476
  return {
233330
234477
  fileName,
@@ -233446,7 +234593,7 @@ function terminateProcessGroup(child, signal) {
233446
234593
  }
233447
234594
  function resolveReadPath(root2, inputPath) {
233448
234595
  const expanded = expandHome4(inputPath || ".");
233449
- return path13.resolve(path13.isAbsolute(expanded) ? expanded : path13.join(root2, expanded));
234596
+ return path14.resolve(path14.isAbsolute(expanded) ? expanded : path14.join(root2, expanded));
233450
234597
  }
233451
234598
  function resolveRequestRoot(defaultRoot, requestRoot) {
233452
234599
  const trimmed = requestRoot?.trim();
@@ -233454,25 +234601,25 @@ function resolveRequestRoot(defaultRoot, requestRoot) {
233454
234601
  }
233455
234602
  function resolveWritePath(root2, inputPath) {
233456
234603
  const expanded = expandHome4(inputPath || ".");
233457
- return path13.resolve(path13.isAbsolute(expanded) ? expanded : path13.join(root2, expanded));
234604
+ return path14.resolve(path14.isAbsolute(expanded) ? expanded : path14.join(root2, expanded));
233458
234605
  }
233459
234606
  function displayPath(root2, target, requestedPath) {
233460
234607
  const expanded = expandHome4(requestedPath || ".");
233461
- if (path13.isAbsolute(expanded) || requestedPath.startsWith("~")) return target;
233462
- 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);
233463
234610
  }
233464
234611
  function resolveLocalSourceId(root2, localSourceId) {
233465
234612
  const raw = localSourceId.includes("::") ? localSourceId.split("::").slice(1).join("::") : localSourceId;
233466
234613
  return resolveReadPath(root2, raw);
233467
234614
  }
233468
234615
  function expandHome4(inputPath) {
233469
- if (inputPath === "~") return os2.homedir();
233470
- 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));
233471
234618
  return inputPath;
233472
234619
  }
233473
234620
  function isInside(root2, candidate) {
233474
- const relative2 = path13.relative(path13.resolve(root2), path13.resolve(candidate));
233475
- 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);
233476
234623
  }
233477
234624
  async function safeStat(target) {
233478
234625
  try {
@@ -233489,7 +234636,7 @@ async function collectFiles(root2, options) {
233489
234636
  for (const entry of entries) {
233490
234637
  if (result2.length >= options.maxVisits) return;
233491
234638
  if (entry.name.startsWith(".") && entry.name !== ".env") continue;
233492
- const absolute = path13.join(dir, entry.name);
234639
+ const absolute = path14.join(dir, entry.name);
233493
234640
  if (entry.isDirectory()) {
233494
234641
  if (!IGNORED_DIRS.has(entry.name)) await walk2(absolute);
233495
234642
  } else if (entry.isFile()) {
@@ -233545,14 +234692,14 @@ function sanitizeMaxResults(value) {
233545
234692
  return Math.max(1, Math.min(typeof value === "number" ? Math.floor(value) : DEFAULT_MAX_RESULTS, 1e3));
233546
234693
  }
233547
234694
  function localSourceEntry(root2, file, absoluteId = false) {
233548
- const stat2 = fs12.statSync(file);
233549
- const relativePath = path13.relative(root2, file);
233550
- 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();
233551
234698
  return {
233552
234699
  localSourceId: absoluteId ? file : `${CLI_ROOT_ID}::${relativePath}`,
233553
234700
  rootId: absoluteId ? "cli-absolute-root" : CLI_ROOT_ID,
233554
234701
  relativePath,
233555
- fileName: path13.basename(file),
234702
+ fileName: path14.basename(file),
233556
234703
  extension: extension2,
233557
234704
  sizeBytes: stat2.size,
233558
234705
  modifiedAt: stat2.mtime.toISOString(),
@@ -233561,7 +234708,7 @@ function localSourceEntry(root2, file, absoluteId = false) {
233561
234708
  };
233562
234709
  }
233563
234710
  function inferMimeType(file) {
233564
- const ext = path13.extname(file).toLowerCase();
234711
+ const ext = path14.extname(file).toLowerCase();
233565
234712
  if (ext === ".pdf") return "application/pdf";
233566
234713
  if (ext === ".json") return "application/json";
233567
234714
  if (ext === ".csv") return "text/csv";
@@ -233622,12 +234769,12 @@ var init_nodeLocalBridge = __esm({
233622
234769
  });
233623
234770
 
233624
234771
  // features/perchTerminal/runtime/cliHost/runCliTurn.ts
233625
- import path14 from "node:path";
234772
+ import path15 from "node:path";
233626
234773
  async function runPerchCliTurn(input, options = {}) {
233627
234774
  const prompt = input.prompt.trim();
233628
234775
  if (!prompt) throw new Error("Missing prompt for perch run");
233629
234776
  const now23 = options.now ?? (() => Date.now());
233630
- const cwd2 = path14.resolve(input.cwd ?? process.cwd());
234777
+ const cwd2 = path15.resolve(input.cwd ?? process.cwd());
233631
234778
  const threadId = input.threadId?.trim() || `cli-${now23()}`;
233632
234779
  const events = [];
233633
234780
  let latestContextSnapshot = null;
@@ -233928,495 +235075,6 @@ var init_cliHost = __esm({
233928
235075
  }
233929
235076
  });
233930
235077
 
233931
- // features/perchTerminal/runtime/publicModelDefault.ts
233932
- function parsePublicModelDefaultPayload(value) {
233933
- const raw = value && typeof value === "object" ? value : {};
233934
- const selection = sanitizeFounderModelSelection(
233935
- raw.selection ?? DEFAULT_FOUNDER_MODEL_SELECTION
233936
- );
233937
- return {
233938
- selection,
233939
- updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : null,
233940
- updatedBy: typeof raw.updatedBy === "string" ? raw.updatedBy : null
233941
- };
233942
- }
233943
- var init_publicModelDefault = __esm({
233944
- "features/perchTerminal/runtime/publicModelDefault.ts"() {
233945
- "use strict";
233946
- init_modelRegistry();
233947
- }
233948
- });
233949
-
233950
- // features/perchTerminal/runtime/cliHost/cliAuthSession.ts
233951
- import { execFile } from "node:child_process";
233952
- import fs13 from "node:fs/promises";
233953
- import os3 from "node:os";
233954
- import path15 from "node:path";
233955
- import { promisify } from "node:util";
233956
- async function readStoredCliAuthSession() {
233957
- const raw = shouldUseFallbackAuthFile() ? await readFallbackSecret().catch(() => null) : process.platform === "darwin" ? await readKeychainSecret().catch(() => null) : await readFallbackSecret().catch(() => null);
233958
- return parseStoredCliAuthSession(raw);
233959
- }
233960
- async function writeStoredCliAuthSession(session) {
233961
- const raw = JSON.stringify(session);
233962
- if (process.platform === "darwin" && !shouldUseFallbackAuthFile()) {
233963
- await execFileAsync("security", [
233964
- "add-generic-password",
233965
- "-U",
233966
- "-s",
233967
- KEYCHAIN_SERVICE,
233968
- "-a",
233969
- KEYCHAIN_ACCOUNT,
233970
- "-w",
233971
- raw
233972
- ]);
233973
- return;
233974
- }
233975
- await writeFallbackSecret(raw);
233976
- }
233977
- async function clearStoredCliAuthSession() {
233978
- if (process.platform === "darwin" && !shouldUseFallbackAuthFile()) {
233979
- await execFileAsync("security", [
233980
- "delete-generic-password",
233981
- "-s",
233982
- KEYCHAIN_SERVICE,
233983
- "-a",
233984
- KEYCHAIN_ACCOUNT
233985
- ]).catch(() => void 0);
233986
- return;
233987
- }
233988
- await fs13.rm(fallbackSessionPath(), { force: true }).catch(() => void 0);
233989
- }
233990
- function shouldUseFallbackAuthFile() {
233991
- return Boolean(process.env.PERCH_CLI_AUTH_DIR?.trim());
233992
- }
233993
- function isStoredCliAuthSessionUsable(session, nowSeconds = Math.floor(Date.now() / 1e3)) {
233994
- if (!session?.accessToken?.trim()) return false;
233995
- if (!session.appUrl?.trim()) return false;
233996
- if (!session.expiresAt) return true;
233997
- return session.expiresAt > nowSeconds + 90;
233998
- }
233999
- function parseStoredCliAuthSession(raw) {
234000
- if (!raw?.trim()) return null;
234001
- try {
234002
- const parsed = JSON.parse(raw);
234003
- if (parsed.version !== 1) return null;
234004
- if (!parsed.appUrl || !parsed.accessToken || !parsed.updatedAt) return null;
234005
- return {
234006
- version: 1,
234007
- appUrl: parsed.appUrl,
234008
- accessToken: parsed.accessToken,
234009
- refreshToken: parsed.refreshToken ?? null,
234010
- expiresAt: typeof parsed.expiresAt === "number" ? parsed.expiresAt : null,
234011
- userId: parsed.userId ?? null,
234012
- email: parsed.email ?? null,
234013
- updatedAt: parsed.updatedAt
234014
- };
234015
- } catch {
234016
- return null;
234017
- }
234018
- }
234019
- async function readKeychainSecret() {
234020
- const { stdout } = await execFileAsync("security", [
234021
- "find-generic-password",
234022
- "-s",
234023
- KEYCHAIN_SERVICE,
234024
- "-a",
234025
- KEYCHAIN_ACCOUNT,
234026
- "-w"
234027
- ]);
234028
- return stdout.trim() || null;
234029
- }
234030
- function fallbackSessionPath() {
234031
- const base = process.env.PERCH_CLI_AUTH_DIR?.trim() || path15.join(os3.homedir(), ".perch");
234032
- return path15.join(base, "cli-auth-session.json");
234033
- }
234034
- async function readFallbackSecret() {
234035
- return await fs13.readFile(fallbackSessionPath(), "utf8");
234036
- }
234037
- async function writeFallbackSecret(raw) {
234038
- const filePath = fallbackSessionPath();
234039
- await fs13.mkdir(path15.dirname(filePath), { recursive: true, mode: 448 });
234040
- await fs13.writeFile(filePath, raw, { mode: 384 });
234041
- }
234042
- var execFileAsync, KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT;
234043
- var init_cliAuthSession = __esm({
234044
- "features/perchTerminal/runtime/cliHost/cliAuthSession.ts"() {
234045
- "use strict";
234046
- execFileAsync = promisify(execFile);
234047
- KEYCHAIN_SERVICE = "app.perchai.cli-auth";
234048
- KEYCHAIN_ACCOUNT = "default";
234049
- }
234050
- });
234051
-
234052
- // features/perchTerminal/runtime/cliHost/cliStandaloneOAuth.ts
234053
- import { execFile as execFile2 } from "node:child_process";
234054
- import http from "node:http";
234055
- import { promisify as promisify2 } from "node:util";
234056
- async function runCliStandaloneOAuthLogin(input) {
234057
- const appUrl = resolveCliAppUrl(input.appUrl, null);
234058
- if (!appUrl) {
234059
- return { ok: false, code: "invalid_app_url", error: "Invalid Perch app URL." };
234060
- }
234061
- const fetchImpl = input.fetchImpl ?? globalThis.fetch;
234062
- if (typeof fetchImpl !== "function") {
234063
- return { ok: false, code: "fetch_unavailable", error: "This Node runtime does not expose fetch." };
234064
- }
234065
- const config = await fetchCliAuthConfig(appUrl, fetchImpl);
234066
- if (!config.ok || !config.supabaseUrl || !config.supabaseAnonKey) {
234067
- return {
234068
- ok: false,
234069
- code: "auth_config_unavailable",
234070
- error: "The Perch app did not return CLI auth configuration."
234071
- };
234072
- }
234073
- const provider = selectOAuthProvider(config.providers ?? [], input.provider);
234074
- if (!provider) {
234075
- return {
234076
- ok: false,
234077
- code: "oauth_provider_unavailable",
234078
- error: "No supported CLI OAuth provider is enabled for this Perch app."
234079
- };
234080
- }
234081
- const callback = await createOAuthCallbackServer({
234082
- host: config.redirectHost?.trim() || "127.0.0.1",
234083
- timeoutMs: input.timeoutMs ?? DEFAULT_LOGIN_TIMEOUT_MS
234084
- });
234085
- try {
234086
- const memoryStorage = createMemoryAuthStorage();
234087
- const supabase = createClient(config.supabaseUrl, config.supabaseAnonKey, {
234088
- auth: {
234089
- flowType: "pkce",
234090
- autoRefreshToken: false,
234091
- detectSessionInUrl: false,
234092
- persistSession: true,
234093
- storage: memoryStorage
234094
- }
234095
- });
234096
- const { data, error } = await supabase.auth.signInWithOAuth({
234097
- provider,
234098
- options: {
234099
- redirectTo: callback.redirectTo,
234100
- skipBrowserRedirect: true,
234101
- data: { signup_source: "cli" }
234102
- }
234103
- });
234104
- if (error || !data.url) {
234105
- return {
234106
- ok: false,
234107
- code: "oauth_start_failed",
234108
- error: error?.message ?? "Supabase did not return an OAuth URL."
234109
- };
234110
- }
234111
- await (input.openExternal ?? openExternal)(data.url);
234112
- const callbackResult = await callback.waitForCode();
234113
- if (!callbackResult.ok) {
234114
- return callbackResult;
234115
- }
234116
- const exchanged = await supabase.auth.exchangeCodeForSession(callbackResult.code);
234117
- if (exchanged.error || !exchanged.data.session) {
234118
- return {
234119
- ok: false,
234120
- code: "oauth_exchange_failed",
234121
- error: exchanged.error?.message ?? "Supabase did not return a CLI session."
234122
- };
234123
- }
234124
- const session = storedSessionFromSupabaseSession({
234125
- appUrl,
234126
- accessToken: exchanged.data.session.access_token,
234127
- refreshToken: exchanged.data.session.refresh_token,
234128
- expiresAt: exchanged.data.session.expires_at ?? null,
234129
- userId: exchanged.data.session.user.id,
234130
- email: exchanged.data.session.user.email ?? null
234131
- });
234132
- await writeStoredCliAuthSession(session);
234133
- return { ok: true, session };
234134
- } finally {
234135
- await callback.close();
234136
- }
234137
- }
234138
- async function fetchCliAuthConfig(appUrl, fetchImpl = globalThis.fetch) {
234139
- const response = await fetchImpl(`${appUrl}/api/perch-terminal/cli-auth/config`, {
234140
- method: "GET",
234141
- headers: { Accept: "application/json" }
234142
- });
234143
- if (!response.ok) return { ok: false };
234144
- return await response.json();
234145
- }
234146
- function selectOAuthProvider(providers, preferred) {
234147
- const available = new Set(providers);
234148
- if (preferred && available.has(preferred)) return preferred;
234149
- if (available.has("google")) return "google";
234150
- if (available.has("github")) return "github";
234151
- return null;
234152
- }
234153
- function storedSessionFromSupabaseSession(input) {
234154
- return {
234155
- version: 1,
234156
- appUrl: input.appUrl,
234157
- accessToken: input.accessToken,
234158
- refreshToken: input.refreshToken ?? null,
234159
- expiresAt: input.expiresAt ?? null,
234160
- userId: input.userId ?? null,
234161
- email: input.email ?? null,
234162
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
234163
- };
234164
- }
234165
- function createMemoryAuthStorage() {
234166
- const values2 = /* @__PURE__ */ new Map();
234167
- return {
234168
- getItem: (key) => values2.get(key) ?? null,
234169
- setItem: (key, value) => {
234170
- values2.set(key, value);
234171
- },
234172
- removeItem: (key) => {
234173
- values2.delete(key);
234174
- }
234175
- };
234176
- }
234177
- async function createOAuthCallbackServer(input) {
234178
- let resolveResult = null;
234179
- const resultPromise = new Promise((resolve5) => {
234180
- resolveResult = resolve5;
234181
- });
234182
- const server = http.createServer((request, response) => {
234183
- const requestUrl = new URL(request.url ?? "/", `http://${input.host}`);
234184
- if (requestUrl.pathname !== CALLBACK_PATH) {
234185
- response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
234186
- response.end("Not found");
234187
- return;
234188
- }
234189
- const code = requestUrl.searchParams.get("code");
234190
- const error = requestUrl.searchParams.get("error_description") ?? requestUrl.searchParams.get("error");
234191
- if (!code) {
234192
- response.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
234193
- response.end(renderCallbackHtml(false));
234194
- resolveResult?.({
234195
- ok: false,
234196
- code: "oauth_callback_missing_code",
234197
- error: error ?? "OAuth callback did not include a code."
234198
- });
234199
- resolveResult = null;
234200
- return;
234201
- }
234202
- response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
234203
- response.end(renderCallbackHtml(true));
234204
- resolveResult?.({ ok: true, code });
234205
- resolveResult = null;
234206
- });
234207
- await new Promise((resolve5, reject2) => {
234208
- server.once("error", reject2);
234209
- server.listen(0, input.host, () => {
234210
- server.off("error", reject2);
234211
- resolve5();
234212
- });
234213
- });
234214
- const address = server.address();
234215
- const redirectTo = `http://${input.host}:${address.port}${CALLBACK_PATH}`;
234216
- const timeout = setTimeout(() => {
234217
- resolveResult?.({
234218
- ok: false,
234219
- code: "oauth_timeout",
234220
- error: "Timed out waiting for browser sign-in to complete."
234221
- });
234222
- resolveResult = null;
234223
- }, input.timeoutMs);
234224
- return {
234225
- redirectTo,
234226
- waitForCode: async () => resultPromise,
234227
- close: async () => {
234228
- clearTimeout(timeout);
234229
- await new Promise((resolve5) => server.close(() => resolve5()));
234230
- }
234231
- };
234232
- }
234233
- function renderCallbackHtml(ok) {
234234
- const title = ok ? "Perch CLI signed in" : "Perch CLI sign-in failed";
234235
- const message = ok ? "You can close this tab and return to your terminal." : "Return to your terminal and try again.";
234236
- 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>`;
234237
- }
234238
- async function openExternal(url) {
234239
- if (process.platform === "darwin") {
234240
- await execFileAsync2("open", [url]);
234241
- } else if (process.platform === "win32") {
234242
- await execFileAsync2("cmd", ["/c", "start", "", url]);
234243
- } else {
234244
- await execFileAsync2("xdg-open", [url]);
234245
- }
234246
- }
234247
- var execFileAsync2, DEFAULT_LOGIN_TIMEOUT_MS, CALLBACK_PATH;
234248
- var init_cliStandaloneOAuth = __esm({
234249
- "features/perchTerminal/runtime/cliHost/cliStandaloneOAuth.ts"() {
234250
- "use strict";
234251
- init_dist4();
234252
- init_modelConnection();
234253
- init_cliAuthSession();
234254
- execFileAsync2 = promisify2(execFile2);
234255
- DEFAULT_LOGIN_TIMEOUT_MS = 12e4;
234256
- CALLBACK_PATH = "/callback";
234257
- }
234258
- });
234259
-
234260
- // features/perchTerminal/runtime/cliHost/cliAuthRefresh.ts
234261
- async function ensureFreshCliAuthSession(input) {
234262
- const session = input.session;
234263
- if (!session?.accessToken?.trim()) return null;
234264
- const nowSeconds = Math.floor(Date.now() / 1e3);
234265
- const stillUsable = !session.expiresAt || session.expiresAt > nowSeconds + 90;
234266
- if (stillUsable) return session;
234267
- if (!session.refreshToken?.trim()) return null;
234268
- const fetchImpl = input.fetchImpl ?? globalThis.fetch;
234269
- if (typeof fetchImpl !== "function") return null;
234270
- const appUrl = session.appUrl?.trim();
234271
- if (!appUrl) return null;
234272
- const config = await fetchCliAuthConfig(appUrl, fetchImpl).catch(() => ({ ok: false }));
234273
- if (!config.ok || !config.supabaseUrl || !config.supabaseAnonKey) return null;
234274
- const supabase = createClient(config.supabaseUrl, config.supabaseAnonKey, {
234275
- auth: {
234276
- autoRefreshToken: false,
234277
- detectSessionInUrl: false,
234278
- persistSession: false
234279
- }
234280
- });
234281
- try {
234282
- const { data, error } = await supabase.auth.refreshSession({
234283
- refresh_token: session.refreshToken
234284
- });
234285
- if (error || !data.session) return null;
234286
- const refreshed = {
234287
- version: 1,
234288
- appUrl: session.appUrl,
234289
- accessToken: data.session.access_token,
234290
- refreshToken: data.session.refresh_token ?? session.refreshToken,
234291
- expiresAt: data.session.expires_at ?? null,
234292
- userId: data.session.user?.id ?? session.userId ?? null,
234293
- email: data.session.user?.email ?? session.email ?? null,
234294
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
234295
- };
234296
- await writeStoredCliAuthSession(refreshed);
234297
- return refreshed;
234298
- } catch {
234299
- return null;
234300
- }
234301
- }
234302
- var init_cliAuthRefresh = __esm({
234303
- "features/perchTerminal/runtime/cliHost/cliAuthRefresh.ts"() {
234304
- "use strict";
234305
- init_dist4();
234306
- init_cliStandaloneOAuth();
234307
- init_cliAuthSession();
234308
- }
234309
- });
234310
-
234311
- // features/perchTerminal/runtime/cliHost/modelConnection.ts
234312
- async function connectCliModelProxy(input = {}) {
234313
- const storedSession = input.storedSession === void 0 ? await readStoredCliAuthSession() : input.storedSession;
234314
- const usableStoredSession = await ensureFreshCliAuthSession({
234315
- session: storedSession,
234316
- fetchImpl: input.fetchImpl
234317
- });
234318
- const appUrl = resolveCliAppUrl(input.appUrl, usableStoredSession?.appUrl ?? null);
234319
- if (!appUrl) return noCliModelConnection();
234320
- if (!usableStoredSession?.accessToken) return noCliModelConnection(appUrl);
234321
- const fetchImpl = input.fetchImpl ?? globalThis.fetch;
234322
- if (typeof fetchImpl !== "function") return noCliModelConnection(appUrl);
234323
- const selection = await fetchPublicModelDefaultSelection(
234324
- appUrl,
234325
- fetchImpl,
234326
- usableStoredSession?.accessToken ?? null
234327
- );
234328
- if (!selection) return noCliModelConnection(appUrl);
234329
- const priorProxy = process.env[MODEL_PROXY_ENV];
234330
- const priorToken = process.env[MODEL_PROXY_TOKEN_ENV];
234331
- process.env[MODEL_PROXY_ENV] = appUrl;
234332
- if (usableStoredSession?.accessToken) {
234333
- process.env[MODEL_PROXY_TOKEN_ENV] = usableStoredSession.accessToken;
234334
- }
234335
- return {
234336
- appUrl,
234337
- authenticated: !!usableStoredSession?.accessToken,
234338
- userId: usableStoredSession?.userId ?? null,
234339
- email: usableStoredSession?.email ?? null,
234340
- founderModelSelection: selection,
234341
- restore: () => {
234342
- if (priorProxy === void 0) {
234343
- delete process.env[MODEL_PROXY_ENV];
234344
- } else {
234345
- process.env[MODEL_PROXY_ENV] = priorProxy;
234346
- }
234347
- if (priorToken === void 0) {
234348
- delete process.env[MODEL_PROXY_TOKEN_ENV];
234349
- } else {
234350
- process.env[MODEL_PROXY_TOKEN_ENV] = priorToken;
234351
- }
234352
- }
234353
- };
234354
- }
234355
- function resolveCliAppUrl(explicit, stored) {
234356
- const raw = explicit?.trim() || process.env[CLI_APP_URL_ENV]?.trim() || process.env[FALLBACK_APP_URL_ENV]?.trim() || stored?.trim() || DEFAULT_CLI_APP_URL;
234357
- if (!raw) return null;
234358
- const withProtocol = /^https?:\/\//i.test(raw) ? raw : `http://${raw}`;
234359
- try {
234360
- const url = new URL(withProtocol);
234361
- url.pathname = url.pathname.replace(/\/+$/, "");
234362
- url.search = "";
234363
- url.hash = "";
234364
- return url.toString().replace(/\/+$/, "");
234365
- } catch {
234366
- return null;
234367
- }
234368
- }
234369
- async function fetchPublicModelDefaultSelection(appUrl, fetchImpl, accessToken) {
234370
- const url = `${appUrl}/api/perch-terminal/public-model-default`;
234371
- const controller = new AbortController();
234372
- const timeout = setTimeout(() => controller.abort(), 3500);
234373
- try {
234374
- const response = await fetchImpl(url, {
234375
- method: "GET",
234376
- headers: {
234377
- Accept: "application/json",
234378
- ...accessToken ? { Authorization: `Bearer ${accessToken}` } : {}
234379
- },
234380
- signal: controller.signal
234381
- });
234382
- if (!response.ok) return null;
234383
- const body = await response.json();
234384
- if (!body || typeof body !== "object") return null;
234385
- const record = body;
234386
- if (record.ok !== true) return null;
234387
- return parsePublicModelDefaultPayload({ selection: record.selection }).selection;
234388
- } catch {
234389
- return null;
234390
- } finally {
234391
- clearTimeout(timeout);
234392
- }
234393
- }
234394
- function noCliModelConnection(appUrl = null) {
234395
- return {
234396
- appUrl,
234397
- authenticated: false,
234398
- userId: null,
234399
- email: null,
234400
- founderModelSelection: null,
234401
- restore: () => {
234402
- }
234403
- };
234404
- }
234405
- var MODEL_PROXY_ENV, MODEL_PROXY_TOKEN_ENV, CLI_APP_URL_ENV, FALLBACK_APP_URL_ENV, DEFAULT_CLI_APP_URL;
234406
- var init_modelConnection = __esm({
234407
- "features/perchTerminal/runtime/cliHost/modelConnection.ts"() {
234408
- "use strict";
234409
- init_publicModelDefault();
234410
- init_cliAuthSession();
234411
- init_cliAuthRefresh();
234412
- MODEL_PROXY_ENV = "PERCH_MODEL_CALL_PROXY_URL";
234413
- MODEL_PROXY_TOKEN_ENV = "PERCH_MODEL_CALL_PROXY_TOKEN";
234414
- CLI_APP_URL_ENV = "PERCH_CLI_APP_URL";
234415
- FALLBACK_APP_URL_ENV = "PERCH_APP_URL";
234416
- DEFAULT_CLI_APP_URL = "https://app.perchai.app";
234417
- }
234418
- });
234419
-
234420
235078
  // lib/perchTerminal/statusLabels.ts
234421
235079
  function sectionLabel(sectionKey) {
234422
235080
  if (STATUS_LABELS[sectionKey]) return STATUS_LABELS[sectionKey];
@@ -234937,6 +235595,17 @@ function claimFirstRun(dir = stateDir()) {
234937
235595
  return false;
234938
235596
  }
234939
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
+ }
234940
235609
  async function postCapture(event, properties) {
234941
235610
  const controller = new AbortController();
234942
235611
  const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS2);
@@ -234963,6 +235632,17 @@ function captureCliFirstRun(version4) {
234963
235632
  if (!claimFirstRun()) return;
234964
235633
  void postCapture("cli_first_run", { version: version4, platform: process.platform });
234965
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
+ }
234966
235646
  var POSTHOG_KEY, POSTHOG_HOST, FETCH_TIMEOUT_MS2;
234967
235647
  var init_cliAnalytics = __esm({
234968
235648
  "features/perchTerminal/runtime/cliHost/cliAnalytics.ts"() {
@@ -289269,7 +289949,7 @@ function flockEventToCliRow(event) {
289269
289949
  return {
289270
289950
  id: `flock-${event.flockId}-${event.flockWorkerId}`,
289271
289951
  text: `${event.displayName} \xB7 ${event.nickname} \xB7 ${FLOCK_STATUS_LABELS[event.status] ?? event.status}`,
289272
- tone: event.status === "done" ? "success" : event.status === "failed" ? "danger" : "muted"
289952
+ tone: event.status === "done" ? "success" : event.status === "failed" || event.status === "blocked" ? "danger" : "muted"
289273
289953
  };
289274
289954
  case "flock_run_completed":
289275
289955
  return {
@@ -289390,6 +290070,7 @@ ${HELP_TEXT}`);
289390
290070
  recentMessages: nextRecentMessages,
289391
290071
  contextSnapshot: result3.contextSnapshot ?? persisted?.contextSnapshot ?? null
289392
290072
  });
290073
+ captureCliMessageSent();
289393
290074
  return cliExitCodeForTurn(result3);
289394
290075
  } finally {
289395
290076
  connection.restore();
@@ -289690,6 +290371,7 @@ async function runReadlineInteractivePerchCli(writer, deps, options) {
289690
290371
  terminal: true
289691
290372
  });
289692
290373
  const state = createInteractiveCliState(options);
290374
+ captureCliSessionStarted();
289693
290375
  const connectModelProxy = deps.connectModelProxy ?? connectCliModelProxy;
289694
290376
  let connection = await connectModelProxy({ appUrl: state.appUrl });
289695
290377
  if (!state.appUrl && connection.appUrl) state.appUrl = connection.appUrl;
@@ -289750,6 +290432,7 @@ async function runReadlineInteractivePerchCli(writer, deps, options) {
289750
290432
  });
289751
290433
  if (!opts.systemInitiated) {
289752
290434
  appendRecentMessage(state.recentMessages, "user", turnPrompt);
290435
+ captureCliMessageSent();
289753
290436
  }
289754
290437
  if (result2.assistantText.trim()) {
289755
290438
  appendRecentMessage(state.recentMessages, "assistant", result2.assistantText.trim());
@@ -289842,6 +290525,7 @@ async function runInkInteractivePerchCli(writer, deps, options) {
289842
290525
  const runTurn = deps.runCliTurn ?? runPerchCliTurn;
289843
290526
  const updateNotice = resolveUpdateNotice(CLI_PACKAGE_VERSION);
289844
290527
  captureCliFirstRun(CLI_PACKAGE_VERSION);
290528
+ captureCliSessionStarted();
289845
290529
  const instance = Ink2.render(
289846
290530
  React11.createElement(function PerchInkApp() {
289847
290531
  const app = Ink2.useApp();
@@ -290463,6 +291147,7 @@ async function runInkInteractivePerchCli(writer, deps, options) {
290463
291147
  }
290464
291148
  if (!systemInitiated) {
290465
291149
  appendRecentMessage(state.recentMessages, "user", prompt);
291150
+ captureCliMessageSent();
290466
291151
  }
290467
291152
  if (assistantText) appendRecentMessage(state.recentMessages, "assistant", assistantText);
290468
291153
  trimRecentMessages(state.recentMessages);