perchai-cli 2.4.48 → 2.4.50

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 +1222 -502
  2. package/package.json +1 -1
package/dist/perch.mjs CHANGED
@@ -75566,7 +75566,6 @@ var init_payroll = __esm({
75566
75566
  // lib/perchBusinessTools/index.ts
75567
75567
  var init_perchBusinessTools = __esm({
75568
75568
  "lib/perchBusinessTools/index.ts"() {
75569
- "use strict";
75570
75569
  init_generateAPAuditPacket();
75571
75570
  init_inventoryFolder();
75572
75571
  init_loadBusinessTables();
@@ -83486,6 +83485,8 @@ Browser Operator skill contract:
83486
83485
  - 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
83486
  - Use semantic browser tools for ordinary controls with text, role, or aria labels.
83488
83487
  - 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.
83488
+ - 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.
83489
+ - 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
83490
  - 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
83491
  - 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
83492
  - Never claim completion from intent alone; require visible proof, save/sent status, URL, receipt, or screenshot evidence.
@@ -83591,17 +83592,20 @@ Rules:
83591
83592
  name: "Browser Operator",
83592
83593
  description: "Operates a logged-in browser turn-by-turn for Quill delivery and web tasks.",
83593
83594
  // 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.
83595
+ // screenshots directly; for text-only models the loop automatically runs each
83596
+ // post-action screenshot through the vision sidecar and injects the readout,
83597
+ // so the operator is always sighted. visionInspect({ currentScreen: true })
83598
+ // remains available for targeted follow-up questions.
83596
83599
  lane: "chat",
83597
83600
  systemPrompt: `You are Quill's Browser Operator. You operate the user's logged-in browser like a careful human.
83598
83601
 
83599
83602
  Rules:
83600
83603
  - 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
83604
  - 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.
83605
+ - 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.
83606
+ - 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
83607
  - 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"}.
83608
+ - 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
83609
  - 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
83610
  - Do not claim completion unless the browser shows a confirmation, URL, saved state, sent state, or other visible receipt.
83607
83611
 
@@ -86236,7 +86240,8 @@ var init_desktopContextSection = __esm({
86236
86240
  BROWSER_EXECUTION_GUIDANCE = `## Browser execution \u2014 logged-in web surface
86237
86241
  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
86242
  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.
86243
+ 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.
86244
+ 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
86245
  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
86246
  }
86242
86247
  });
@@ -87398,7 +87403,7 @@ var init_threadLedger = __esm({
87398
87403
  });
87399
87404
 
87400
87405
  // 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;
87406
+ var VISIBLE_OUTPUT_STYLE_GUIDANCE, PERCH_OPERATOR_CAPABILITIES, SAFFRON_STYLE_CONTRACT, QUILL_STYLE_CONTRACT, SAFFRON_CORE_IDENTITY, QUILL_CORE_IDENTITY, PERCH_MEMORY_GUIDANCE;
87402
87407
  var init_perchMemoryGuidance = __esm({
87403
87408
  "features/perchTerminal/runtime/perchMemoryGuidance.ts"() {
87404
87409
  "use strict";
@@ -87413,6 +87418,36 @@ Avoid stock AI phrases: "delve," "nuanced," "robust," "seamless,"
87413
87418
  Be concise when the user is moving fast.
87414
87419
  Preserve the persona: Quill is warm, literate, and direct; Saffron is sharp,
87415
87420
  practical, and confident.
87421
+ `.trim();
87422
+ PERCH_OPERATOR_CAPABILITIES = `
87423
+ ## What you can do (and you can do a lot)
87424
+
87425
+ You run on the user's Mac through Perch Desktop. You are a real operator, not a
87426
+ chat box, and you have the tools to act. When someone asks what you can do, or
87427
+ whether you can control their computer, answer with specifics and offer to show
87428
+ them rather than listing what you cannot do. You can:
87429
+
87430
+ - Run shell commands in folders the user approves, including installing tools and
87431
+ packages, running scripts and software, and starting long jobs in the background.
87432
+ - Work across their files: read, write, edit, search with glob and grep, move,
87433
+ and organize anything inside approved folders.
87434
+ - Run real analysis in a sealed sandbox: write and execute Python or Node over
87435
+ their actual data.
87436
+ - Drive a real browser: navigate, click, type, fill forms, research the web, and
87437
+ pull sources.
87438
+ - Deliver finished work: draft and send Gmail, create Google Docs and Sheets, and
87439
+ add calendar events.
87440
+ - Do forensic and financial work: AP audits, reconciliations, statement and
87441
+ earnings reviews, and multi-step suites.
87442
+
87443
+ Lead with what you can do and offer to prove it, for example "want me to run
87444
+ something to show you?". Do not talk yourself down or open with a list of limits.
87445
+
87446
+ State the real limits once, and only when they matter: file and command access is
87447
+ scoped to the folders the user approves, and commands run under their permission
87448
+ settings. You work through the shell, their files, the sandbox, and the browser,
87449
+ not through arbitrary macOS system settings or other desktop apps. That is already
87450
+ a lot. Show it.
87416
87451
  `.trim();
87417
87452
  SAFFRON_STYLE_CONTRACT = `
87418
87453
  ## Voice contract \u2014 Saffron (visible output only)
@@ -87449,6 +87484,8 @@ You genuinely enjoy finding something interesting buried in a mess.
87449
87484
 
87450
87485
  You're not an assistant. You're the operator who gets the work done.
87451
87486
 
87487
+ ${PERCH_OPERATOR_CAPABILITIES}
87488
+
87452
87489
  ## How you work
87453
87490
 
87454
87491
  Lead with the finding. Skip the preamble. "Seven duplicate payments, same vendor,
@@ -87579,6 +87616,8 @@ the user needs analysis, files, data, an audit, or delivery, you do that work
87579
87616
  yourself with the same tools \u2014 in your own voice. You never punt real work to
87580
87617
  another persona.
87581
87618
 
87619
+ ${PERCH_OPERATOR_CAPABILITIES}
87620
+
87582
87621
  ## How you work
87583
87622
 
87584
87623
  Read first. Before you draft a single sentence, you know what the piece is for,
@@ -91110,6 +91149,7 @@ Final answers lead with findings, name artifacts or delivery status, and give on
91110
91149
  var MARKET_DESK_TOOL_NAMES;
91111
91150
  var init_marketDeskAccess = __esm({
91112
91151
  "features/perchTerminal/runtime/marketDesk/marketDeskAccess.ts"() {
91152
+ "use strict";
91113
91153
  init_toolNames();
91114
91154
  MARKET_DESK_TOOL_NAMES = /* @__PURE__ */ new Set([
91115
91155
  TOOL_NAMES.getMarketSignal,
@@ -91789,6 +91829,7 @@ function listFinancialPlaybooks() {
91789
91829
  var AP_AUDIT_PACKET_DEF, PLAYBOOKS;
91790
91830
  var init_registry2 = __esm({
91791
91831
  "features/perchTerminal/runtime/financialPlaybooks/registry.ts"() {
91832
+ "use strict";
91792
91833
  init_managedWorkflowRegistry2();
91793
91834
  init_toolNames();
91794
91835
  AP_AUDIT_PACKET_DEF = {
@@ -92416,6 +92457,297 @@ var init_coreSystemPromptLanes = __esm({
92416
92457
  }
92417
92458
  });
92418
92459
 
92460
+ // lib/perchTerminal/appHostGate.ts
92461
+ var PERCH_CHAT_WEB_URL, PERCH_MARKETING_SITE_URL, PERCH_MARKETING_DESKTOP_URL, PERCH_MARKETING_CLI_URL, PERCH_CLI_INSTALL_COMMAND;
92462
+ var init_appHostGate = __esm({
92463
+ "lib/perchTerminal/appHostGate.ts"() {
92464
+ PERCH_CHAT_WEB_URL = process.env.NEXT_PUBLIC_PERCHAI_WEB_URL?.trim() || "https://chat.perchai.app";
92465
+ PERCH_MARKETING_SITE_URL = process.env.NEXT_PUBLIC_MARKETING_SITE_URL?.trim() || "https://perchai.app";
92466
+ PERCH_MARKETING_DESKTOP_URL = `${PERCH_MARKETING_SITE_URL}/desktop`;
92467
+ PERCH_MARKETING_CLI_URL = `${PERCH_MARKETING_SITE_URL}/cli`;
92468
+ PERCH_CLI_INSTALL_COMMAND = "npm install -g perchai-cli";
92469
+ }
92470
+ });
92471
+
92472
+ // lib/perch-ai/plan-catalog.ts
92473
+ var PERCH_AI_PLAN_CATALOG;
92474
+ var init_plan_catalog = __esm({
92475
+ "lib/perch-ai/plan-catalog.ts"() {
92476
+ PERCH_AI_PLAN_CATALOG = [
92477
+ {
92478
+ code: "internal",
92479
+ name: "Internal",
92480
+ description: "Full Perch AI internal/admin access for Perch Terminal operators.",
92481
+ entitlements: {
92482
+ "shell.preview": { enabled: true },
92483
+ "auth.access_codes.redeem": { enabled: true },
92484
+ "desktop.runtime": { enabled: true },
92485
+ "coding.workspace": { enabled: true },
92486
+ "ap.audit": { enabled: true },
92487
+ "writing.studio": { enabled: true },
92488
+ "writing.studio.web": { enabled: true },
92489
+ "admin.console": { enabled: true },
92490
+ "limits.daily_runs": { limit: null },
92491
+ "limits.weekly_runs": { limit: null },
92492
+ "limits.max_context_tokens": { limit: 2e5 },
92493
+ "limits.allowed_models": { models: ["*"] }
92494
+ }
92495
+ },
92496
+ {
92497
+ code: "pilot",
92498
+ name: "Pilot",
92499
+ description: "Self-serve pilot access with full product features.",
92500
+ entitlements: {
92501
+ "shell.preview": { enabled: true },
92502
+ "auth.access_codes.redeem": { enabled: true },
92503
+ "desktop.runtime": { enabled: true },
92504
+ "coding.workspace": { enabled: true },
92505
+ "ap.audit": { enabled: true },
92506
+ "writing.studio": { enabled: true },
92507
+ "writing.studio.web": { enabled: true },
92508
+ // Admin console stays off for self-serve signups (it grants operator/admin
92509
+ // tooling, not a product feature). Flip to true only for trusted accounts.
92510
+ "admin.console": { enabled: false },
92511
+ "limits.daily_runs": { limit: null },
92512
+ "limits.weekly_runs": { limit: null },
92513
+ "limits.max_context_tokens": { limit: 2e5 },
92514
+ "limits.allowed_models": { models: ["*"] }
92515
+ }
92516
+ },
92517
+ {
92518
+ code: "demo",
92519
+ name: "Demo",
92520
+ description: "Restricted demo access for guided evaluations.",
92521
+ entitlements: {
92522
+ "shell.preview": { enabled: true },
92523
+ "auth.access_codes.redeem": { enabled: true },
92524
+ "desktop.runtime": { enabled: false },
92525
+ "coding.workspace": { enabled: false },
92526
+ "ap.audit": { enabled: false },
92527
+ "writing.studio": { enabled: false },
92528
+ "writing.studio.web": { enabled: false },
92529
+ "admin.console": { enabled: false },
92530
+ "limits.daily_runs": { limit: 10 },
92531
+ "limits.weekly_runs": { limit: 30 },
92532
+ "limits.max_context_tokens": { limit: 32e3 },
92533
+ "limits.allowed_models": { models: ["gpt-4o-mini"] }
92534
+ }
92535
+ }
92536
+ ];
92537
+ }
92538
+ });
92539
+
92540
+ // features/perchTerminal/runtime/roost.ts
92541
+ function normalizeRoostTier(value) {
92542
+ if (value === "max") return "pro_max";
92543
+ return isRoostTier(value) ? value : null;
92544
+ }
92545
+ function normalizeRoostModelChoice(value) {
92546
+ if (value === "internal") return "internal";
92547
+ return normalizeRoostTier(value);
92548
+ }
92549
+ function isRoostTier(value) {
92550
+ return value === "standard" || value === "standard_max" || value === "pro" || value === "pro_max";
92551
+ }
92552
+ var ROOST_TIERS, ROOST_TIER_LABELS, ROOST_TIER_CONTEXT_LABELS;
92553
+ var init_roost = __esm({
92554
+ "features/perchTerminal/runtime/roost.ts"() {
92555
+ "use strict";
92556
+ init_modelRegistry();
92557
+ ROOST_TIERS = ["standard", "standard_max", "pro", "pro_max"];
92558
+ ROOST_TIER_LABELS = {
92559
+ standard: "Standard",
92560
+ standard_max: "Standard Max",
92561
+ pro: "Pro",
92562
+ pro_max: "Pro Max"
92563
+ };
92564
+ ROOST_TIER_CONTEXT_LABELS = {
92565
+ standard: "200K context",
92566
+ standard_max: "1M context",
92567
+ pro: "200K context",
92568
+ pro_max: "1M context"
92569
+ };
92570
+ }
92571
+ });
92572
+
92573
+ // lib/perch/perchFactsCore.ts
92574
+ function getPilotEntitlements() {
92575
+ const pilot = PERCH_AI_PLAN_CATALOG.find((p) => p.code === "pilot");
92576
+ if (!pilot) return null;
92577
+ return pilot.entitlements;
92578
+ }
92579
+ function formatUsageLine() {
92580
+ const ent = getPilotEntitlements();
92581
+ if (!ent) return "Usage limits are configured per plan.";
92582
+ const daily = ent["limits.daily_runs"]?.limit;
92583
+ const weekly = ent["limits.weekly_runs"]?.limit;
92584
+ if (daily === null && weekly === null) {
92585
+ 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.";
92586
+ }
92587
+ const parts = [];
92588
+ if (daily !== null && daily !== void 0) parts.push(`${daily} runs per day`);
92589
+ if (weekly !== null && weekly !== void 0) parts.push(`${weekly} runs per week`);
92590
+ return `Usage is capped at ${parts.join(" and ")} on this plan. No credit card is required.`;
92591
+ }
92592
+ function buildRoostTierSummary() {
92593
+ const lines = ROOST_TIERS.map(
92594
+ (tier) => `- ${ROOST_TIER_LABELS[tier]} (${ROOST_TIER_CONTEXT_LABELS[tier]})`
92595
+ );
92596
+ return lines.join("\n");
92597
+ }
92598
+ function buildWebCapabilityList() {
92599
+ return [
92600
+ "- Grounded answers with real inline citations from your uploaded documents.",
92601
+ "- Upload and ask: PDF, spreadsheet, image, and document analysis.",
92602
+ "- Web search with cited results.",
92603
+ "- Draft citation check: marks each claim verified or unverified.",
92604
+ "",
92605
+ "The web chat does not run shell commands, access your local files, control a browser,",
92606
+ `or operate your computer. Those capabilities live in Perch Terminal (Desktop and CLI).`,
92607
+ `Get Perch Terminal at ${PERCH_MARKETING_DESKTOP_URL}.`
92608
+ ].join("\n");
92609
+ }
92610
+ function buildTerminalCapabilityList() {
92611
+ const skillLines = DEDUPLICATED_SKILLS.map(
92612
+ (s) => `- ${s.id}: ${s.description}`
92613
+ ).join("\n");
92614
+ return [
92615
+ "Core operator capabilities (available in Desktop and CLI):",
92616
+ "- Run shell commands in folders the user approves.",
92617
+ "- Read, write, edit, search, and organize files across approved folders.",
92618
+ "- Run real analysis in a sealed sandbox: Python and Node over actual data.",
92619
+ "- Drive a real browser: navigate, click, fill forms, research the web.",
92620
+ "- Deliver finished work: draft and send Gmail, create Google Docs and Sheets, add calendar events.",
92621
+ "- Financial and forensic work: AP audits, reconciliations, statement and earnings reviews, multi-step suites.",
92622
+ "",
92623
+ "Specialized skills (activated per task from the skill registry):",
92624
+ skillLines
92625
+ ].join("\n");
92626
+ }
92627
+ function buildPerchFactsCore(surface) {
92628
+ const capabilitySection = surface === "web" ? `### What this web chat can do
92629
+
92630
+ ${buildWebCapabilityList()}` : `### What Perch Terminal can do
92631
+
92632
+ ${buildTerminalCapabilityList()}`;
92633
+ const tierTwoNote = surface === "terminal" ? `
92634
+ 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.` : "";
92635
+ return [
92636
+ INSTRUCTION_PREAMBLE,
92637
+ "",
92638
+ WHAT_IS_PERCH,
92639
+ "",
92640
+ ACCESS_AND_PRICING,
92641
+ "",
92642
+ MODEL_STORY,
92643
+ "",
92644
+ capabilitySection,
92645
+ tierTwoNote
92646
+ ].join("\n").trim();
92647
+ }
92648
+ var PERCH_SKILL_REGISTRY, DEDUPLICATED_SKILLS, INSTRUCTION_PREAMBLE, WHAT_IS_PERCH, ACCESS_AND_PRICING, MODEL_STORY;
92649
+ var init_perchFactsCore = __esm({
92650
+ "lib/perch/perchFactsCore.ts"() {
92651
+ init_appHostGate();
92652
+ init_plan_catalog();
92653
+ init_roost();
92654
+ PERCH_SKILL_REGISTRY = [
92655
+ {
92656
+ id: "workspace",
92657
+ description: "Navigate, inspect, search, summarize, and safely edit local workspaces, attached folders, source inventories, project files, and codebases with exact file-grounded evidence."
92658
+ },
92659
+ {
92660
+ id: "browser-operator",
92661
+ description: "Operate Perch's logged-in browser surface using visual state, semantic DOM targets, and coordinate actions when available."
92662
+ },
92663
+ {
92664
+ id: "research",
92665
+ description: "Gather, compare, rank, and cite evidence from public web sources, scholarly sources, workspace files, source packets, and attached local folders."
92666
+ },
92667
+ {
92668
+ id: "documents",
92669
+ description: "Create, edit, verify, and deliver professional document artifacts such as DOCX, Google Docs, memos, letters, reports, redlines, and formal written deliverables."
92670
+ },
92671
+ {
92672
+ id: "spreadsheets",
92673
+ description: "Create, inspect, clean, calculate, format, and verify spreadsheet artifacts such as XLSX, CSV, TSV, Google Sheets, formulas, pivots, charts, and reconciliation workbooks."
92674
+ },
92675
+ {
92676
+ id: "pdf",
92677
+ description: "Extract, summarize, cite, split, merge, create, inspect, and verify PDF content, including text PDFs, scanned PDFs, tables, forms, page citations, and generated reports."
92678
+ },
92679
+ {
92680
+ id: "presentations",
92681
+ 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."
92682
+ },
92683
+ {
92684
+ id: "data-quality",
92685
+ description: "Profile messy data, find anomalies, reconcile systems, explain exceptions, score risk, and produce audit-ready evidence through deterministic math before narrative."
92686
+ },
92687
+ {
92688
+ id: "diagrams",
92689
+ description: "Create, edit, and verify diagrams, graphs, workflows, org charts, entity networks, control graphs, architecture maps, and evidence-backed visual structures."
92690
+ },
92691
+ {
92692
+ id: "github",
92693
+ description: "Inspect GitHub and git repository state, review commits and pull requests, debug checks, and publish local changes with clear permission boundaries."
92694
+ },
92695
+ {
92696
+ id: "wait-operator",
92697
+ 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."
92698
+ },
92699
+ {
92700
+ id: "data-quality",
92701
+ description: "Profile messy data, find anomalies, reconcile systems, explain exceptions, score risk, and produce audit-ready evidence through deterministic math before narrative."
92702
+ }
92703
+ ];
92704
+ DEDUPLICATED_SKILLS = Array.from(
92705
+ new Map(PERCH_SKILL_REGISTRY.map((s) => [s.id, s])).values()
92706
+ );
92707
+ INSTRUCTION_PREAMBLE = `## Perch product facts -- authoritative reference
92708
+
92709
+ For any question about Perch (what it is, whether it is free, what you can do,
92710
+ what model you are, do I need an account, etc.), answer only from the facts
92711
+ below. If something is not covered here, say you are not certain rather than
92712
+ guessing.`.trim();
92713
+ WHAT_IS_PERCH = `### What Perch is
92714
+
92715
+ Perch is an AI platform built around verifiable AI: every claim is sourced and
92716
+ checkable. The goal is to make AI output trustworthy, not just fluent.
92717
+
92718
+ Perch has three surfaces:
92719
+ - Perch AI Web (chat.perchai.app): a warm, accessible web chat for grounded
92720
+ answers, document upload, web search, and citation checking.
92721
+ - Perch Terminal Desktop: a local operator that runs on your Mac, controls
92722
+ your files, runs code in a sandbox, drives a real browser, and delivers
92723
+ finished work. Download at ${PERCH_MARKETING_DESKTOP_URL}.
92724
+ - Perch Terminal CLI: the same operator power from your terminal.
92725
+ Install with: ${PERCH_CLI_INSTALL_COMMAND}.`.trim();
92726
+ ACCESS_AND_PRICING = `### Access and pricing
92727
+
92728
+ All three surfaces are free during the current pilot. No credit card is
92729
+ required to sign up.
92730
+
92731
+ ${formatUsageLine()}
92732
+
92733
+ A Perch account is required to use the web chat. Sign up at ${PERCH_CHAT_WEB_URL}.
92734
+ Perch Terminal Desktop and CLI are also free; sign in with your Perch account.`.trim();
92735
+ MODEL_STORY = `### The model -- Roost
92736
+
92737
+ Perch does not expose raw model names. The model layer is called Roost.
92738
+ Roost picks the best available model for each task and locks it for that task.
92739
+ You are powered by Roost.
92740
+
92741
+ Roost has four tiers:
92742
+ ${buildRoostTierSummary()}
92743
+
92744
+ Standard and Pro use a 200K context window. Standard Max and Pro Max use a 1M
92745
+ context window (Max = long context). Tier selection is available in the
92746
+ composer. When someone asks what model you are, say: "I run on Roost, Perch's
92747
+ model layer. Perch does not surface the underlying model names."`.trim();
92748
+ }
92749
+ });
92750
+
92419
92751
  // features/perchTerminal/runtime/context/coreSystemSection.ts
92420
92752
  function buildCoreSystemSection(input) {
92421
92753
  const enabledToolNames = Array.from(new Set(input.enabledToolNames ?? []));
@@ -92442,6 +92774,7 @@ function buildCoreSystemSection(input) {
92442
92774
  });
92443
92775
  const lines = splitRenderedLaneText(renderedCoreLanes);
92444
92776
  const permissionModeHandled = corePromptPermissionModeHandled(renderedCoreLanes);
92777
+ lines.push("", PERCH_FACTS_CORE_TERMINAL);
92445
92778
  if (!permissionModeHandled && input.chatMode === "plan") {
92446
92779
  if (planGateRejected) {
92447
92780
  lines.push(
@@ -92699,7 +93032,7 @@ function buildCoordinatorNotificationSynthesisInstructions(notifications) {
92699
93032
  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
93033
  ].join("\n");
92701
93034
  }
92702
- var PLATFORM_DELIVERY_GUIDANCE, LIVE_NARRATION_GUIDANCE;
93035
+ var PERCH_FACTS_CORE_TERMINAL, PLATFORM_DELIVERY_GUIDANCE, LIVE_NARRATION_GUIDANCE;
92703
93036
  var init_coreSystemSection = __esm({
92704
93037
  "features/perchTerminal/runtime/context/coreSystemSection.ts"() {
92705
93038
  "use strict";
@@ -92715,6 +93048,8 @@ var init_coreSystemSection = __esm({
92715
93048
  init_operatingCaseService();
92716
93049
  init_planModeStateMachine();
92717
93050
  init_coreSystemPromptLanes();
93051
+ init_perchFactsCore();
93052
+ PERCH_FACTS_CORE_TERMINAL = buildPerchFactsCore("terminal");
92718
93053
  PLATFORM_DELIVERY_GUIDANCE = `## Platform delivery \u2014 use the verified shortcut, do not hand off
92719
93054
  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
93055
 
@@ -92756,6 +93091,45 @@ var init_agentSkillRegistry = __esm({
92756
93091
  "generated/agentSkillRegistry.ts"() {
92757
93092
  "use strict";
92758
93093
  AGENT_SKILL_REGISTRY = [
93094
+ {
93095
+ "id": "about-perch",
93096
+ "name": "about-perch",
93097
+ "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.",
93098
+ "path": "features/perchTerminal/agentPlatform/skills/about-perch/SKILL.md",
93099
+ "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.",
93100
+ "surface": {
93101
+ "gui": "supported",
93102
+ "cli": "supported"
93103
+ },
93104
+ "triggerHints": [
93105
+ "what is perch",
93106
+ "what can you do",
93107
+ "what model are you",
93108
+ "is this free",
93109
+ "is perch free",
93110
+ "is terminal free",
93111
+ "is terminal paid",
93112
+ "do i need an account",
93113
+ "roost",
93114
+ "roost tier",
93115
+ "standard max",
93116
+ "pro max",
93117
+ "perch terminal",
93118
+ "perch desktop",
93119
+ "perch cli",
93120
+ "perch vs",
93121
+ "web vs terminal",
93122
+ "difference between perch",
93123
+ "how does perch work",
93124
+ "citation verifier",
93125
+ "what is roost"
93126
+ ],
93127
+ "companionSkills": [
93128
+ "research",
93129
+ "workspace"
93130
+ ],
93131
+ "preferredTools": []
93132
+ },
92759
93133
  {
92760
93134
  "id": "browser-operator",
92761
93135
  "name": "browser-operator",
@@ -93407,7 +93781,8 @@ var init_skillSelector = __esm({
93407
93781
  "diagrams",
93408
93782
  "workspace",
93409
93783
  "browser-operator",
93410
- "skill-creator"
93784
+ "skill-creator",
93785
+ "about-perch"
93411
93786
  ];
93412
93787
  INTENT_PATTERNS = {
93413
93788
  "browser-operator": [
@@ -93459,6 +93834,10 @@ var init_skillSelector = __esm({
93459
93834
  ],
93460
93835
  "skill-creator": [
93461
93836
  /\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
93837
+ ],
93838
+ "about-perch": [
93839
+ /\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,
93840
+ /\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
93841
  ]
93463
93842
  };
93464
93843
  }
@@ -119131,29 +119510,18 @@ var init_browserSession = __esm({
119131
119510
  }
119132
119511
  });
119133
119512
 
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);
119513
+ // features/perchTerminal/runtime/cliAuthRefreshRegistry.ts
119514
+ function setCliModelProxyAuthRefresher(fn) {
119515
+ refresher = fn;
119142
119516
  }
119143
- function isRoostTier(value) {
119144
- return value === "standard" || value === "standard_max" || value === "pro" || value === "pro_max";
119517
+ function refreshCliModelProxyAuth() {
119518
+ return refresher ? refresher() : Promise.resolve(null);
119145
119519
  }
119146
- var ROOST_TIER_LABELS;
119147
- var init_roost = __esm({
119148
- "features/perchTerminal/runtime/roost.ts"() {
119520
+ var refresher;
119521
+ var init_cliAuthRefreshRegistry = __esm({
119522
+ "features/perchTerminal/runtime/cliAuthRefreshRegistry.ts"() {
119149
119523
  "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
- };
119524
+ refresher = null;
119157
119525
  }
119158
119526
  });
119159
119527
 
@@ -119488,19 +119856,36 @@ async function callModelRouterViaServer(request, opts, endpoint = "/api/perch-te
119488
119856
  durationMs: 0
119489
119857
  };
119490
119858
  }
119859
+ async function syncModelProxyAuthAfter401() {
119860
+ if (typeof globalThis.window !== "undefined") {
119861
+ await syncBrowserAuthSession();
119862
+ return null;
119863
+ }
119864
+ return refreshCliModelProxyAuth();
119865
+ }
119866
+ function withModelProxyAuthHeader(init, token) {
119867
+ if (!token?.trim()) return init;
119868
+ const headers = new Headers(init.headers);
119869
+ headers.set("Authorization", `Bearer ${token}`);
119870
+ return { ...init, headers };
119871
+ }
119491
119872
  async function fetchModelProxyWithRetry(endpoint, init, options) {
119492
119873
  const maxAttempts = Math.max(1, options?.maxAttempts ?? MODEL_PROXY_MAX_ATTEMPTS);
119493
119874
  let lastError = null;
119494
119875
  let authSynced = false;
119876
+ let requestInit = init;
119495
119877
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
119496
119878
  try {
119497
- const response = await fetch(endpoint, init);
119498
- if (response.status === 401 && !authSynced && typeof globalThis.window !== "undefined") {
119879
+ const response = await fetch(endpoint, requestInit);
119880
+ if (response.status === 401 && !authSynced) {
119499
119881
  authSynced = true;
119500
- await syncBrowserAuthSession();
119882
+ const refreshedToken = await syncModelProxyAuthAfter401();
119883
+ if (refreshedToken) {
119884
+ requestInit = withModelProxyAuthHeader(requestInit, refreshedToken);
119885
+ }
119501
119886
  options?.onRetry?.(attempt, "HTTP 401 (session sync)");
119502
119887
  await response.body?.cancel().catch(() => void 0);
119503
- await waitForModelProxyRetry(attempt, init.signal);
119888
+ await waitForModelProxyRetry(attempt, requestInit.signal);
119504
119889
  continue;
119505
119890
  }
119506
119891
  if (!isRetriableModelProxyStatus(response.status) || attempt === maxAttempts - 1) {
@@ -119508,12 +119893,12 @@ async function fetchModelProxyWithRetry(endpoint, init, options) {
119508
119893
  }
119509
119894
  options?.onRetry?.(attempt, `HTTP ${response.status}`);
119510
119895
  await response.body?.cancel().catch(() => void 0);
119511
- await waitForModelProxyRetry(attempt, init.signal);
119896
+ await waitForModelProxyRetry(attempt, requestInit.signal);
119512
119897
  } catch (error) {
119513
119898
  if (isAbortLikeError(error) || attempt === maxAttempts - 1) throw error;
119514
119899
  lastError = error;
119515
119900
  options?.onRetry?.(attempt, error instanceof Error ? error.message : String(error));
119516
- await waitForModelProxyRetry(attempt, init.signal);
119901
+ await waitForModelProxyRetry(attempt, requestInit.signal);
119517
119902
  }
119518
119903
  }
119519
119904
  throw lastError instanceof Error ? lastError : new Error(String(lastError ?? "Model proxy request failed"));
@@ -119637,6 +120022,7 @@ var init_modelRouter = __esm({
119637
120022
  init_contextMeter();
119638
120023
  init_inferenceUsageRecorder();
119639
120024
  init_browserSession();
120025
+ init_cliAuthRefreshRegistry();
119640
120026
  init_roostUserSelectionClient();
119641
120027
  init_providerModelStep();
119642
120028
  init_toolCallNormalizer();
@@ -197350,7 +197736,6 @@ function recordDispatchBatchResult(tasks, result2, ctx, opts = {}) {
197350
197736
  var lifecycleByRun, MAX_LIFECYCLE_RUNS;
197351
197737
  var init_workerLifecycle = __esm({
197352
197738
  "features/perchTerminal/runtime/workers/workerLifecycle.ts"() {
197353
- "use strict";
197354
197739
  init_workerManifest();
197355
197740
  lifecycleByRun = /* @__PURE__ */ new Map();
197356
197741
  MAX_LIFECYCLE_RUNS = 200;
@@ -198995,10 +199380,10 @@ function evaluateBrowserAction(action, observation, context = {}) {
198995
199380
  if (action.kind === "click" && !label && !("role" in action && action.role)) {
198996
199381
  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
199382
  }
198998
- if ((action.kind === "click" || action.kind === "visual_click" || action.kind === "type") && context.recentFailedClickSignatures?.includes(clickSignature(action))) {
199383
+ if ((action.kind === "click" || action.kind === "visual_click") && context.recentFailedClickSignatures?.includes(clickSignature(action))) {
198999
199384
  return block(
199000
199385
  "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.`
199386
+ `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
199387
  );
199003
199388
  }
199004
199389
  if (action.kind === "click" && (observation.app === "google_docs" || observation.app === "google_sheets")) {
@@ -199363,6 +199748,17 @@ async function safeBrowserAction(input, runtime = {}) {
199363
199748
  if (target.kind === "ambiguous") {
199364
199749
  lastFailure = fail("selector_ambiguous", target.message, startMs, latestSnapshot.text, latestSnapshot.url, target.candidates);
199365
199750
  } else if (target.kind === "missing") {
199751
+ const escalated = await escalateMissingClickToCoordinate({
199752
+ input,
199753
+ snapshot: latestSnapshot,
199754
+ callTool,
199755
+ startMs,
199756
+ runtime
199757
+ });
199758
+ if (escalated) {
199759
+ clearPolicyClickFailure(input, runtime, latestSnapshot);
199760
+ return escalated;
199761
+ }
199366
199762
  lastFailure = fail("selector_not_found", target.message, startMs, latestSnapshot.text, latestSnapshot.url);
199367
199763
  } else {
199368
199764
  const actionResult = await executeBrowserAction(input, target.target, callTool);
@@ -199499,7 +199895,7 @@ function enforceActionPolicy(input, preSnapshot, runtime, startMs) {
199499
199895
  }
199500
199896
  function recordPolicyClickFailure(input, runtime, snapshot) {
199501
199897
  if (!runtime.policy) return;
199502
- if (input.intent !== "click" && input.intent !== "visual_click" && input.intent !== "type") return;
199898
+ if (input.intent !== "click" && input.intent !== "visual_click") return;
199503
199899
  const key = policySessionKey(runtime);
199504
199900
  const signature = `${policySurfaceFingerprint(snapshot)}::${clickSignature(inputToProposedAction(input))}`;
199505
199901
  const existing = failedClickMemory.get(key) ?? [];
@@ -199611,6 +200007,7 @@ async function clickGmailComposeSendButton(callTool) {
199611
200007
  };
199612
200008
  });
199613
200009
  if (!target || target.result !== "target") return JSON.stringify(target || { result: "missing" });
200010
+ await page.evaluate(${PERCH_CURSOR_OVERLAY_SNIPPET}, { x: target.x, y: target.y });
199614
200011
  await page.mouse.click(target.x, target.y);
199615
200012
  return JSON.stringify({ result: "clicked", label: target.label, x: target.x, y: target.y, bounds: target.bounds, viewport: target.viewport });
199616
200013
  }
@@ -199671,7 +200068,8 @@ async function dispatchSyntheticPaste(payload, runtime) {
199671
200068
  () => {
199672
200069
  const html = ${JSON.stringify(payload.html)};
199673
200070
  const plain = ${JSON.stringify(payload.plain)};
199674
- const active = document.activeElement;
200071
+ const deepActive = () => { let n = document.activeElement; while (n && n.shadowRoot && n.shadowRoot.activeElement) n = n.shadowRoot.activeElement; return n; };
200072
+ const active = deepActive();
199675
200073
  if (!active) return false;
199676
200074
  const isLongBody = plain.length > ${TYPE_DIRECT_MAX_CHARS} || plain.includes("\\n");
199677
200075
  if (isLongBody && active.tagName === "INPUT") return false;
@@ -199989,7 +200387,26 @@ async function resolveTargetForAction(input, snapshot, callTool) {
199989
200387
  }
199990
200388
  const resolved = resolveTarget(snapshot.text, input.targetDescriptor);
199991
200389
  if (resolved.kind === "found") return resolved;
199992
- if (input.intent === "type") return { kind: "found", target: null };
200390
+ if (input.intent === "type") {
200391
+ const desc = input.targetDescriptor;
200392
+ const named = (desc?.text ?? "").trim() || (desc?.ariaLabel ?? "").trim();
200393
+ if (named) {
200394
+ return {
200395
+ kind: "found",
200396
+ target: {
200397
+ kind: "semantic",
200398
+ descriptor: {
200399
+ ...desc?.text ? { text: desc.text } : {},
200400
+ ...desc?.ariaLabel ? { ariaLabel: desc.ariaLabel } : {},
200401
+ ...desc?.role ? { role: desc.role } : {},
200402
+ nth: normalizeNth(desc?.nth)
200403
+ },
200404
+ matched: desc?.text ?? desc?.ariaLabel ?? "target"
200405
+ }
200406
+ };
200407
+ }
200408
+ return { kind: "found", target: null };
200409
+ }
199993
200410
  return resolved;
199994
200411
  }
199995
200412
  function resolveTarget(snapshot, descriptor) {
@@ -200016,7 +200433,7 @@ function resolveTarget(snapshot, descriptor) {
200016
200433
  if (!match) {
200017
200434
  return {
200018
200435
  kind: "missing",
200019
- message: `No browser element matched "${desc.text ?? desc.ariaLabel ?? desc.role ?? "target"}" at nth=${nth}.`
200436
+ 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
200437
  };
200021
200438
  }
200022
200439
  if (matches.length > 1 && desc.nth === void 0) {
@@ -200048,7 +200465,7 @@ function resolveTarget(snapshot, descriptor) {
200048
200465
  }
200049
200466
  return {
200050
200467
  kind: "missing",
200051
- message: `No browser element matched "${desc.text ?? desc.ariaLabel ?? desc.role ?? "target"}".`
200468
+ 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
200469
  };
200053
200470
  }
200054
200471
  function liveResolverSource() {
@@ -200097,17 +200514,50 @@ function liveResolverSource() {
200097
200514
  el.textContent,
200098
200515
  ].filter(Boolean).map((v) => String(v).trim()).find(Boolean) || "";
200099
200516
  };
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);
200517
+ const candidates = () => {
200518
+ const INTERACTIVE_SELECTOR = [
200519
+ "button",
200520
+ "a[href]",
200521
+ "input",
200522
+ "textarea",
200523
+ "select",
200524
+ "[role]",
200525
+ "[aria-label]",
200526
+ "[data-tooltip]",
200527
+ "[contenteditable='true']"
200528
+ ].join(",");
200529
+ // Collect open shadow roots recursively so the resolver reaches controls
200530
+ // inside web components (e.g. Reddit/shreddit). Shadow-DOM elements share
200531
+ // the top document's viewport coordinate space, so getBoundingClientRect
200532
+ // (used for the coordinate click) stays correct. Same-origin iframes are
200533
+ // intentionally skipped: their rects are in the frame's own space and
200534
+ // would misplace the click.
200535
+ const roots = [document];
200536
+ const seenRoots = new Set();
200537
+ const walk = (root) => {
200538
+ let hosts;
200539
+ try { hosts = root.querySelectorAll("*"); } catch (e) { return; }
200540
+ for (const host of hosts) {
200541
+ const sr = host.shadowRoot;
200542
+ if (sr && !seenRoots.has(sr)) {
200543
+ seenRoots.add(sr);
200544
+ roots.push(sr);
200545
+ walk(sr);
200546
+ }
200547
+ }
200548
+ };
200549
+ walk(document);
200550
+ const out = [];
200551
+ const seenEl = new Set();
200552
+ for (const root of roots) {
200553
+ let found;
200554
+ try { found = root.querySelectorAll(INTERACTIVE_SELECTOR); } catch (e) { continue; }
200555
+ for (const el of found) {
200556
+ if (!seenEl.has(el) && visible(el)) { seenEl.add(el); out.push(el); }
200557
+ }
200558
+ }
200559
+ return out;
200560
+ };
200111
200561
  const rankMatch = (el, descriptor) => {
200112
200562
  const label = labelFor(el);
200113
200563
  const aria = el.getAttribute("aria-label") || "";
@@ -200145,6 +200595,32 @@ function liveResolverSource() {
200145
200595
  };
200146
200596
  `;
200147
200597
  }
200598
+ async function escalateMissingClickToCoordinate(args) {
200599
+ const { input, snapshot, callTool, startMs, runtime } = args;
200600
+ if (input.intent !== "click") return null;
200601
+ const descriptor = input.targetDescriptor;
200602
+ const label = descriptor?.text ?? descriptor?.ariaLabel ?? descriptor?.role;
200603
+ if (!descriptor || !label) return null;
200604
+ const coordinateTarget = { kind: "semantic", descriptor, matched: label };
200605
+ const clicked = await clickTargetTrusted(callTool, coordinateTarget, label);
200606
+ if (!clicked.ok) return null;
200607
+ const outcome = await waitForExpectedOutcome({
200608
+ input,
200609
+ callTool,
200610
+ preSnapshot: snapshot,
200611
+ startMs,
200612
+ signal: runtime.signal
200613
+ });
200614
+ if (!outcome.ok) return null;
200615
+ return {
200616
+ ...outcome,
200617
+ evidence: {
200618
+ ...outcome.evidence,
200619
+ matched: `${clicked.matched} \xB7 escalated to coordinate (CUA) click`,
200620
+ ...clicked.actionGeometry ? { actionGeometry: clicked.actionGeometry } : {}
200621
+ }
200622
+ };
200623
+ }
200148
200624
  async function executeBrowserAction(input, target, callTool) {
200149
200625
  if (input.intent === "snapshot" || input.intent === "extract") return { ok: true, matched: input.intent };
200150
200626
  if (input.intent === "navigate") {
@@ -200173,6 +200649,8 @@ async function executeBrowserAction(input, target, callTool) {
200173
200649
  if (!clicked.ok) return clicked;
200174
200650
  const typed = await typeViaRealKeys(callTool, input.text ?? "", clicked.matched);
200175
200651
  if (typed.ok) {
200652
+ const verified = await verifyTypedTextLanded(callTool, input.text ?? "", clicked.matched);
200653
+ if (!verified.ok) return verified;
200176
200654
  return {
200177
200655
  ok: true,
200178
200656
  matched: `${clicked.matched} then typed into focused surface`,
@@ -200182,7 +200660,10 @@ async function executeBrowserAction(input, target, callTool) {
200182
200660
  return typed;
200183
200661
  }
200184
200662
  if (input.intent === "type") {
200185
- return enterText(callTool, target, input.text ?? "");
200663
+ const typed = await enterText(callTool, target, input.text ?? "");
200664
+ if (!typed.ok) return typed;
200665
+ const verified = await verifyTypedTextLanded(callTool, input.text ?? "", typed.matched);
200666
+ return verified.ok ? typed : verified;
200186
200667
  }
200187
200668
  if (!target) return { ok: false, error: "target could not be resolved" };
200188
200669
  if (target.kind === "selector") {
@@ -200193,8 +200674,24 @@ async function executeBrowserAction(input, target, callTool) {
200193
200674
  }
200194
200675
  return { ok: false, error: `Unsupported browser intent: ${input.intent}` };
200195
200676
  }
200677
+ function labelGroundingVariants(label) {
200678
+ const trimmed = (label ?? "").trim();
200679
+ if (!trimmed || trimmed.toLowerCase() === "visual target") return [];
200680
+ const variants = [trimmed];
200681
+ 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();
200682
+ if (stripped && stripped.toLowerCase() !== trimmed.toLowerCase()) variants.push(stripped);
200683
+ return variants;
200684
+ }
200196
200685
  async function clickVisualTarget(callTool, visualTarget) {
200197
200686
  if (!visualTarget) return { ok: false, error: "visual target requires x and y" };
200687
+ for (const candidate of labelGroundingVariants(visualTarget.label)) {
200688
+ const grounded = await clickTargetTrusted(
200689
+ callTool,
200690
+ { kind: "semantic", descriptor: { text: candidate }, matched: candidate },
200691
+ candidate
200692
+ );
200693
+ if (grounded.ok) return grounded;
200694
+ }
200198
200695
  const x = Number(visualTarget.x);
200199
200696
  const y = Number(visualTarget.y);
200200
200697
  if (!Number.isFinite(x) || !Number.isFinite(y)) {
@@ -200212,6 +200709,7 @@ async function clickVisualTarget(callTool, visualTarget) {
200212
200709
  const py = coordinateSpace === "viewport_pixels" ? rawY : rawY * viewport.height;
200213
200710
  const safeX = Math.max(1, Math.min(viewport.width - 1, px));
200214
200711
  const safeY = Math.max(1, Math.min(viewport.height - 1, py));
200712
+ await page.evaluate(${PERCH_CURSOR_OVERLAY_SNIPPET}, { x: safeX, y: safeY });
200215
200713
  await page.mouse.click(safeX, safeY);
200216
200714
  return JSON.stringify({
200217
200715
  result: "clicked",
@@ -200235,17 +200733,78 @@ async function clickVisualTarget(callTool, visualTarget) {
200235
200733
  }
200236
200734
  return { ok: false, error: text || "visual click failed" };
200237
200735
  }
200736
+ async function verifyTypedTextLanded(callTool, text, matched) {
200737
+ if (!text.trim()) return { ok: true };
200738
+ const expr = `() => {
200739
+ const url = String(location.href || "");
200740
+ if (/docs\\.google\\.com\\/(document|spreadsheets|presentation)\\//i.test(url)) return "PERCH_DOCS_BLIND";
200741
+ const deepActive = () => { let n = document.activeElement; while (n && n.shadowRoot && n.shadowRoot.activeElement) n = n.shadowRoot.activeElement; return n; };
200742
+ const el = deepActive();
200743
+ if (!el) return "PERCH_UNKNOWN";
200744
+ const tag = (el.tagName || "").toLowerCase();
200745
+ const readableEditable =
200746
+ el.isContentEditable ||
200747
+ tag === "textarea" ||
200748
+ (tag === "input") ||
200749
+ ("value" in el && typeof el.value === "string");
200750
+ if (!readableEditable) return "PERCH_UNKNOWN";
200751
+ const read = (n) => {
200752
+ if ("value" in n && typeof n.value === "string") return n.value;
200753
+ if (n.isContentEditable) return n.textContent || "";
200754
+ return "";
200755
+ };
200756
+ const val = String(read(el) || "").replace(/\\s+/g, " ").trim();
200757
+ return val ? "PERCH_LANDED" : "PERCH_EMPTY";
200758
+ }`;
200759
+ const result2 = await callTool({
200760
+ serverName: "playwright",
200761
+ toolName: "browser_evaluate",
200762
+ args: { function: expr }
200763
+ });
200764
+ if (!result2.ok) return { ok: true };
200765
+ if (firstText(result2.content).trim() !== "PERCH_EMPTY") return { ok: true };
200766
+ return {
200767
+ ok: false,
200768
+ 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.`
200769
+ };
200770
+ }
200771
+ async function hasEditableFocus(callTool) {
200772
+ const expr = `() => {
200773
+ const deepActive = () => { let n = document.activeElement; while (n && n.shadowRoot && n.shadowRoot.activeElement) n = n.shadowRoot.activeElement; return n; };
200774
+ const el = deepActive();
200775
+ if (!el) return false;
200776
+ if (el.isContentEditable) return true;
200777
+ const tag = (el.tagName || "").toLowerCase();
200778
+ if (tag === "textarea") return true;
200779
+ if (tag === "input") {
200780
+ const t = (el.getAttribute("type") || "text").toLowerCase();
200781
+ return !["button","submit","reset","checkbox","radio","range","file","image","color"].includes(t);
200782
+ }
200783
+ if ("value" in el && typeof el.value === "string") return true;
200784
+ return false;
200785
+ }`;
200786
+ const result2 = await callTool({
200787
+ serverName: "playwright",
200788
+ toolName: "browser_evaluate",
200789
+ args: { function: expr }
200790
+ });
200791
+ if (!result2.ok) return false;
200792
+ return /^\s*true\s*$/i.test(firstText(result2.content));
200793
+ }
200238
200794
  async function enterText(callTool, target, text) {
200239
200795
  const matched = target?.matched ?? "focused field";
200240
200796
  const isLong = text.length > TYPE_DIRECT_MAX_CHARS || text.includes("\n");
200797
+ const alreadyFocused = await hasEditableFocus(callTool);
200241
200798
  if (!isLong) {
200242
- if (target) {
200799
+ let focusGeometry;
200800
+ if (!alreadyFocused && target) {
200243
200801
  const focused = await focusTarget(callTool, target, matched);
200244
- if (!focused.ok) return focused;
200802
+ if (focused.ok) focusGeometry = focused.actionGeometry;
200245
200803
  }
200246
- return typeViaRealKeys(callTool, text, matched);
200804
+ const typed = await typeViaRealKeys(callTool, text, matched);
200805
+ return typed.ok && focusGeometry ? { ...typed, actionGeometry: focusGeometry } : typed;
200247
200806
  }
200248
- if (target) {
200807
+ if (!alreadyFocused && target) {
200249
200808
  const focused = await focusTarget(callTool, target, matched);
200250
200809
  if (focused.ok) {
200251
200810
  const pasted2 = await dispatchSyntheticPaste({ html: null, plain: text }, { callTool });
@@ -200273,7 +200832,8 @@ function shouldRefuseRealKeys(error) {
200273
200832
  async function typeIntoActiveElement(callTool, text) {
200274
200833
  const json = JSON.stringify(text);
200275
200834
  const expr = `() => {
200276
- const el = document.activeElement;
200835
+ const deepActive = () => { let n = document.activeElement; while (n && n.shadowRoot && n.shadowRoot.activeElement) n = n.shadowRoot.activeElement; return n; };
200836
+ const el = deepActive();
200277
200837
  if (!el) throw new Error("no active element to type into");
200278
200838
  const isLongBody = ${JSON.stringify(text.length > TYPE_DIRECT_MAX_CHARS || text.includes("\n"))};
200279
200839
  if (isLongBody && el.tagName === "INPUT") {
@@ -200359,6 +200919,7 @@ async function clickTargetTrusted(callTool, target, matched) {
200359
200919
  };
200360
200920
  }, { descriptor: ${JSON.stringify(descriptor)}, selector: ${JSON.stringify(selector)} });
200361
200921
  if (!target || target.result !== "target") return JSON.stringify(target || { result: "missing" });
200922
+ await page.evaluate(${PERCH_CURSOR_OVERLAY_SNIPPET}, { x: target.x, y: target.y });
200362
200923
  await page.mouse.click(target.x, target.y);
200363
200924
  return JSON.stringify({ result: "clicked", label: target.label, x: target.x, y: target.y, bounds: target.bounds, viewport: target.viewport });
200364
200925
  }`;
@@ -200686,7 +201247,7 @@ function delay(ms, signal) {
200686
201247
  );
200687
201248
  });
200688
201249
  }
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;
201250
+ 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
201251
  var init_safeBrowserAction = __esm({
200691
201252
  "features/perchTerminal/runtime/playwright/safeBrowserAction.ts"() {
200692
201253
  "use strict";
@@ -200713,6 +201274,26 @@ var init_safeBrowserAction = __esm({
200713
201274
  ];
200714
201275
  failedClickMemory = /* @__PURE__ */ new Map();
200715
201276
  FAILED_CLICK_MEMORY_MAX = 12;
201277
+ PERCH_CURSOR_OVERLAY_SNIPPET = `(coord) => {
201278
+ try {
201279
+ const id = "__perch_operator_cursor__";
201280
+ let el = document.getElementById(id);
201281
+ if (!el) {
201282
+ el = document.createElement("div");
201283
+ el.id = id;
201284
+ el.setAttribute("aria-hidden", "true");
201285
+ 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));";
201286
+ 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>';
201287
+ (document.body || document.documentElement).appendChild(el);
201288
+ }
201289
+ // The arrow tip is at the SVG origin (0,0), so left/top place the tip on the point.
201290
+ el.style.left = coord.x + "px";
201291
+ el.style.top = coord.y + "px";
201292
+ el.style.opacity = "1";
201293
+ if (el.__perchHide) clearTimeout(el.__perchHide);
201294
+ el.__perchHide = setTimeout(function () { el.style.opacity = "0.6"; }, 1500);
201295
+ } catch (e) {}
201296
+ }`;
200716
201297
  TYPE_DIRECT_MAX_CHARS = 200;
200717
201298
  PRESS_KEY_LOOP_MAX_CHARS = 4e3;
200718
201299
  }
@@ -218837,9 +219418,19 @@ async function executeToolBatch(toolCalls, ctx) {
218837
219418
  { type: "image_url", image_url: { url: execution.imageDataUrl } }
218838
219419
  ]
218839
219420
  });
219421
+ } else if (isOperatorScreenshot) {
219422
+ const readout = await describeOperatorScreenshotViaVision(execution.imageDataUrl, ctx);
219423
+ ctx.messages.push({
219424
+ role: "user",
219425
+ 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.
219426
+
219427
+ ${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.`
219428
+ });
218840
219429
  } 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 });
219430
+ ctx.messages.push({
219431
+ role: "user",
219432
+ 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.`
219433
+ });
218843
219434
  }
218844
219435
  }
218845
219436
  }
@@ -218860,6 +219451,26 @@ function parseImageDataUrlForTool(dataUrl) {
218860
219451
  base64
218861
219452
  };
218862
219453
  }
219454
+ async function describeOperatorScreenshotViaVision(dataUrl, ctx) {
219455
+ if (ctx.signal?.aborted) return null;
219456
+ const parsed = parseImageDataUrlForTool(dataUrl);
219457
+ if (!parsed) return null;
219458
+ try {
219459
+ const result2 = await callVisionLaneModel({
219460
+ founderModelSelection: ctx.founderModelSelection,
219461
+ imageBase64: parsed.base64,
219462
+ mimeType: parsed.mimeType,
219463
+ userPrompt: OPERATOR_VISION_OBSERVER_PROMPT,
219464
+ signal: ctx.signal ?? void 0,
219465
+ userId: ctx.userId ?? null,
219466
+ workspaceId: ctx.workspaceId ?? null,
219467
+ runId: ctx.runId ?? null
219468
+ });
219469
+ if (result2.ok && result2.text.trim()) return result2.text.trim();
219470
+ } catch {
219471
+ }
219472
+ return null;
219473
+ }
218863
219474
  function currentScreenVisionToolCall(input) {
218864
219475
  const { toolCall, imageDataUrl } = input;
218865
219476
  if (toolCall.name !== TOOL_NAMES.visionInspect) return { toolCall };
@@ -218918,11 +219529,12 @@ function updateConsecutiveRequiredArgFailures(counts, execution) {
218918
219529
  function now6() {
218919
219530
  return (/* @__PURE__ */ new Date()).toISOString();
218920
219531
  }
218921
- var MAX_PARALLEL_TOOL_CALLS, LOCAL_VISUAL_ATTACHMENT_MARKER;
219532
+ var MAX_PARALLEL_TOOL_CALLS, LOCAL_VISUAL_ATTACHMENT_MARKER, OPERATOR_VISION_OBSERVER_PROMPT;
218922
219533
  var init_toolBatchExecutor = __esm({
218923
219534
  "features/perchTerminal/runtime/toolLoop/toolBatchExecutor.ts"() {
218924
219535
  "use strict";
218925
219536
  init_modelRegistry();
219537
+ init_visionLaneCall();
218926
219538
  init_toolExecutor();
218927
219539
  init_fileDiff();
218928
219540
  init_toolNames();
@@ -218935,6 +219547,14 @@ var init_toolBatchExecutor = __esm({
218935
219547
  init_sourceReadPolicy();
218936
219548
  MAX_PARALLEL_TOOL_CALLS = 8;
218937
219549
  LOCAL_VISUAL_ATTACHMENT_MARKER = "[local-visual]";
219550
+ OPERATOR_VISION_OBSERVER_PROMPT = [
219551
+ "You are the eyes of a browser operator that cannot see this screen.",
219552
+ "Describe the current browser screen so the operator can act precisely.",
219553
+ "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.",
219554
+ "State which field or control is focused or selected, the values already entered in visible fields, and any error, confirmation, or toast text.",
219555
+ "If a crosshair or marker is drawn on the image, say which element it is pointing at.",
219556
+ "Plain text only. Do not suggest or decide the next action."
219557
+ ].join(" ");
218938
219558
  }
218939
219559
  });
218940
219560
 
@@ -219379,8 +219999,11 @@ async function prepareLoopMessagesForSend(input) {
219379
219999
  toolDefinitionsJson,
219380
220000
  modelContext
219381
220001
  });
219382
- const contextLimitTokens = resolveEffectiveContextLimit(input.option);
219383
- const compactThreshold = getAutoCompactThreshold(input.option);
220002
+ const budgetModel = typeof input.effectiveContextLimitTokens === "number" && input.effectiveContextLimitTokens > 0 ? { ...input.option, effectiveLimitTokens: input.effectiveContextLimitTokens } : input.option;
220003
+ const contextLimitTokens = resolveEffectiveContextLimit(
220004
+ budgetModel
220005
+ );
220006
+ const compactThreshold = getAutoCompactThreshold(budgetModel);
219384
220007
  const postCompactTargetTokens = resolvePostCompactTargetTokens(contextLimitTokens);
219385
220008
  const recentTailTokenBudget = resolveRecentTailBudgetTokens({
219386
220009
  postCompactTargetTokens,
@@ -219406,11 +220029,6 @@ async function prepareLoopMessagesForSend(input) {
219406
220029
  toolDefinitionsJson,
219407
220030
  modelContext
219408
220031
  });
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
220032
  }
219415
220033
  }
219416
220034
  if (autoCompactEnabled && anchoredTokens(breakdown.total) >= compactThreshold) {
@@ -223197,6 +223815,7 @@ async function runFlockTurn(input, deps, options = {}) {
223197
223815
  emitWorkerUpdate(emit, plan, worker, "queued");
223198
223816
  }
223199
223817
  const spawnFn = options.spawnWorkerFn ?? spawnWorker;
223818
+ let turnInput = input;
223200
223819
  const outcomes = [];
223201
223820
  const revisionOutcomes = [];
223202
223821
  let revisionReport = null;
@@ -223245,6 +223864,7 @@ async function runFlockTurn(input, deps, options = {}) {
223245
223864
  toolCallsReserved += reservedToolCalls;
223246
223865
  emitWorkerUpdate(emit, plan, worker, "running");
223247
223866
  try {
223867
+ turnInput = deps.refreshTurnInput ? await deps.refreshTurnInput(turnInput) : turnInput;
223248
223868
  const context = contextOverride ?? buildWorkerContext(worker, sharedContext, outputByFlockWorkerId, plan);
223249
223869
  const objective = worker.role === "worker" && contextContainsSources(context) ? `${worker.objective}
223250
223870
  ${FLOCK_GROUNDING_CONTRACT}` : worker.objective;
@@ -223256,18 +223876,18 @@ ${FLOCK_GROUNDING_CONTRACT}` : worker.objective;
223256
223876
  maxIterations: worker.maxIterations,
223257
223877
  maxToolCalls: reservedToolCalls
223258
223878
  },
223259
- buildSpawnContext(input, plan.flockId, flockRun.controller.signal, emit, worker)
223879
+ buildSpawnContext(turnInput, plan.flockId, flockRun.controller.signal, emit, worker)
223260
223880
  );
223261
223881
  toolCallsReserved -= reservedToolCalls;
223262
223882
  toolCallsUsed += Math.min(result2.toolCalls ?? 0, reservedToolCalls);
223263
- const ok = result2.ok;
223883
+ const status = flockWorkerStatusFromSpawnResult(result2);
223264
223884
  const outcome = {
223265
223885
  worker,
223266
223886
  result: result2,
223267
- status: ok ? "done" : "failed",
223887
+ status,
223268
223888
  detail: clampLine(result2.summary, 200)
223269
223889
  };
223270
- emitWorkerUpdate(emit, plan, worker, ok ? "done" : "failed", clampLine(result2.summary, 160));
223890
+ emitWorkerUpdate(emit, plan, worker, status, clampLine(result2.summary, 160));
223271
223891
  return outcome;
223272
223892
  } catch (error) {
223273
223893
  toolCallsReserved -= reservedToolCalls;
@@ -223367,8 +223987,10 @@ ${FLOCK_GROUNDING_CONTRACT}` : worker.objective;
223367
223987
  }
223368
223988
  const userCancelled = Boolean(deps.signal?.aborted);
223369
223989
  const workersDone = outcomes.filter((outcome) => outcome.status === "done").length;
223370
- const workersFailed = outcomes.filter((outcome) => outcome.status === "failed").length;
223990
+ const workersBlocked = outcomes.filter((outcome) => outcome.status === "blocked").length;
223991
+ const workersFailed = outcomes.filter((outcome) => outcome.status === "failed").length + workersBlocked;
223371
223992
  const flockStatus = userCancelled || wallTimeHit && workersDone === 0 ? "cancelled" : workersFailed === 0 && workersDone === plan.workers.length ? "completed" : workersDone > 0 ? "partial" : "failed";
223993
+ 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
223994
  const assistantText = [
223373
223995
  buildFlockSummary(
223374
223996
  plan,
@@ -223384,7 +224006,7 @@ ${FLOCK_GROUNDING_CONTRACT}` : worker.objective;
223384
224006
  ...modelOverrides.unavailable.map(
223385
224007
  (override) => `Requested model "${override.modelText}"${override.roleText ? ` for ${override.roleText}` : ""} is not available \u2014 that worker stayed on the default model path.`
223386
224008
  )
223387
- ].join("\n");
224009
+ ].join("\n") + permissionHint;
223388
224010
  emit({
223389
224011
  type: "flock_run_completed",
223390
224012
  flockId: plan.flockId,
@@ -223618,6 +224240,12 @@ function buildFlockSummary(plan, outcomes, status, toolCallsUsed, wallTimeHit) {
223618
224240
  function friendlyOutcomeDetail(detail) {
223619
224241
  const clean = clampLine(detail, 220);
223620
224242
  if (!clean) return "";
224243
+ if (/approval gate|requires approval|Blocked at an approval/i.test(clean)) {
224244
+ return clean;
224245
+ }
224246
+ if (/No usable model response|Model proxy error|not_authenticated|Unauthorized/i.test(clean)) {
224247
+ return clean;
224248
+ }
223621
224249
  if (/Reached maximum \d+ tool-call iterations/i.test(clean)) {
223622
224250
  return "finished from gathered evidence";
223623
224251
  }
@@ -223636,6 +224264,11 @@ function clampLine(text, max2) {
223636
224264
  function makeFlockRunId() {
223637
224265
  return `flock-run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
223638
224266
  }
224267
+ function flockWorkerStatusFromSpawnResult(result2) {
224268
+ if (!result2.ok || result2.usabilityStatus === "failed") return "failed";
224269
+ if (result2.usabilityStatus === "needs_user_input") return "blocked";
224270
+ return "done";
224271
+ }
223639
224272
  function now11() {
223640
224273
  return (/* @__PURE__ */ new Date()).toISOString();
223641
224274
  }
@@ -232065,13 +232698,15 @@ function getBackgroundTaskStore() {
232065
232698
  var STORE_FILE, BackgroundTaskStore, singleton;
232066
232699
  var init_backgroundTaskRegistry = __esm({
232067
232700
  "features/perchTerminal/runtime/background/backgroundTaskRegistry.ts"() {
232068
- "use strict";
232069
232701
  init_backgroundTaskTypes();
232070
232702
  STORE_FILE = "background-tasks.json";
232071
232703
  BackgroundTaskStore = class {
232704
+ baseDir;
232705
+ storePath;
232706
+ outputDir;
232707
+ records = /* @__PURE__ */ new Map();
232708
+ loaded = false;
232072
232709
  constructor(options = {}) {
232073
- this.records = /* @__PURE__ */ new Map();
232074
- this.loaded = false;
232075
232710
  this.baseDir = options.baseDir ?? path11.join(os.homedir(), ".perch");
232076
232711
  this.storePath = path11.join(this.baseDir, STORE_FILE);
232077
232712
  this.outputDir = path11.join(this.baseDir, "background-output");
@@ -233621,337 +234256,11 @@ var init_nodeLocalBridge = __esm({
233621
234256
  }
233622
234257
  });
233623
234258
 
233624
- // features/perchTerminal/runtime/cliHost/runCliTurn.ts
233625
- import path14 from "node:path";
233626
- async function runPerchCliTurn(input, options = {}) {
233627
- const prompt = input.prompt.trim();
233628
- if (!prompt) throw new Error("Missing prompt for perch run");
233629
- const now23 = options.now ?? (() => Date.now());
233630
- const cwd2 = path14.resolve(input.cwd ?? process.cwd());
233631
- const threadId = input.threadId?.trim() || `cli-${now23()}`;
233632
- const events = [];
233633
- let latestContextSnapshot = null;
233634
- const turnRunner = resolveTurnRunnerForPrompt(prompt) ?? options.turnRunner ?? runOperatorTurn;
233635
- const turnInput = buildCliTurnInput(input, {
233636
- prompt,
233637
- cwd: cwd2,
233638
- threadId
233639
- });
233640
- const deps = buildCliTurnDeps({
233641
- signal: input.signal ?? null,
233642
- events,
233643
- onEvent: input.onEvent,
233644
- onContextSnapshot: (snapshot) => {
233645
- latestContextSnapshot = snapshot;
233646
- }
233647
- });
233648
- const restoreLocalBridge = input.cliLocalTools === false ? null : installCliNodeLocalBridge({ workspaceRoot: cwd2 });
233649
- try {
233650
- const result2 = await turnRunner(turnInput, deps);
233651
- return buildCliTurnResult({
233652
- prompt,
233653
- cwd: cwd2,
233654
- threadId,
233655
- events,
233656
- contextSnapshot: latestContextSnapshot,
233657
- result: result2,
233658
- error: null
233659
- });
233660
- } catch (error) {
233661
- const message = error instanceof Error ? error.message : String(error);
233662
- const runId = findRunId(events) ?? threadId;
233663
- return buildCliTurnResult({
233664
- prompt,
233665
- cwd: cwd2,
233666
- threadId,
233667
- events,
233668
- contextSnapshot: latestContextSnapshot,
233669
- result: {
233670
- status: "failed",
233671
- error: message,
233672
- runId
233673
- },
233674
- error: message
233675
- });
233676
- } finally {
233677
- restoreLocalBridge?.();
233678
- }
233679
- }
233680
- function buildCliTurnInput(input, resolved) {
233681
- const workspaceId = input.workspaceId ?? null;
233682
- const userId = input.userId ?? null;
233683
- return {
233684
- trimmedInput: resolved.prompt,
233685
- clientRunId: input.clientRunId ?? null,
233686
- chatMode: input.chatMode ?? "agents",
233687
- threadId: resolved.threadId,
233688
- personaId: input.personaId ?? DEFAULT_PERSONA_ID,
233689
- workspaceId,
233690
- userId,
233691
- selectedSourceId: input.selectedSourceId ?? null,
233692
- recentMessages: input.recentMessages ?? [],
233693
- usageRunIds: input.usageRunIds ?? [],
233694
- sessionContext: emptyPerchTerminalSessionContext({
233695
- userId,
233696
- workspaceId
233697
- }),
233698
- desktopConnected: input.desktopConnected ?? false,
233699
- cliLocalTools: input.cliLocalTools !== false,
233700
- activeRootPath: input.activeRootPath ?? resolved.cwd,
233701
- localSources: [],
233702
- localSourcesMeta: null,
233703
- projectMeta: readCliProjectMemoryState(resolved.cwd).meta,
233704
- permanentMemories: input.permanentMemories ?? [],
233705
- userMemories: input.userMemories ?? [],
233706
- supabase: null,
233707
- supabaseConfigured: false,
233708
- cliServerAppUrl: input.cliServerAppUrl ?? input.marketDeskProxyAppUrl ?? input.appUrl ?? null,
233709
- cliServerAccessToken: input.cliServerAccessToken ?? input.marketDeskProxyAccessToken ?? null,
233710
- marketDeskProxyAppUrl: input.marketDeskProxyAppUrl ?? input.appUrl ?? null,
233711
- marketDeskProxyAccessToken: input.marketDeskProxyAccessToken ?? null,
233712
- permissionMode: normalizePermissionMode(input.permissionMode ?? "default"),
233713
- founderModelSelection: input.founderModelSelection ?? null,
233714
- threadEvidence: [],
233715
- workspaceFileResolution: null,
233716
- folderIndexSummary: null,
233717
- mcpTools: [],
233718
- coordinatorMode: false,
233719
- systemNotifications: [],
233720
- workflowState: null,
233721
- skipUserMessagePersistence: false,
233722
- systemInitiated: input.systemInitiated === true
233723
- };
233724
- }
233725
- function buildCliTurnDeps(input) {
233726
- return {
233727
- signal: input.signal,
233728
- onEvent: (event) => {
233729
- input.events.push(event);
233730
- input.onEvent?.(event);
233731
- },
233732
- onContextAssembled: input.onContextSnapshot,
233733
- persistUserMessage: async () => null,
233734
- persistAssistantMessage: async () => null,
233735
- createWorkflowRun: async () => null,
233736
- finishWorkflowRun: async () => {
233737
- },
233738
- persistToolCall: async () => {
233739
- },
233740
- persistSandboxExecution: async () => null,
233741
- persistContextSnapshot: async () => null,
233742
- createManagedArtifact: async () => null,
233743
- createManagedReport: async () => null,
233744
- logProductAuditEvent: async () => {
233745
- }
233746
- };
233747
- }
233748
- function buildCliTurnResult(input) {
233749
- const segments = buildTranscriptSegments({
233750
- events: input.events,
233751
- complete: true,
233752
- error: input.error
233753
- });
233754
- const status = input.result.status;
233755
- const assistantText = "assistantText" in input.result ? input.result.assistantText : "";
233756
- const durationMs = "durationMs" in input.result ? input.result.durationMs : null;
233757
- const error = input.error ?? ("error" in input.result ? input.result.error : void 0) ?? ("reason" in input.result ? input.result.reason : void 0);
233758
- return {
233759
- ok: status === "completed",
233760
- status,
233761
- prompt: input.prompt,
233762
- cwd: input.cwd,
233763
- threadId: input.threadId,
233764
- runId: input.result.runId,
233765
- durationMs,
233766
- assistantText,
233767
- events: input.events,
233768
- segments,
233769
- contextSnapshot: input.contextSnapshot,
233770
- ...error ? { error } : {}
233771
- };
233772
- }
233773
- function findRunId(events) {
233774
- const started = events.find((event) => event.type === "turn_started");
233775
- return started?.type === "turn_started" ? started.runId : null;
233776
- }
233777
- var init_runCliTurn = __esm({
233778
- "features/perchTerminal/runtime/cliHost/runCliTurn.ts"() {
233779
- "use strict";
233780
- init_session();
233781
- init_sharedSlashCommands();
233782
- init_operatorStream();
233783
- init_permissionModes();
233784
- init_personaRegistry();
233785
- init_runOperatorTurn();
233786
- init_nodeLocalBridge();
233787
- init_nodeLocalBridge();
233788
- }
233789
- });
233790
-
233791
- // features/perchTerminal/runtime/cliHost/renderCliTurn.ts
233792
- function renderCliTurnText(result2, options = {}) {
233793
- const lines = [];
233794
- const seen = /* @__PURE__ */ new Set();
233795
- for (const event of result2.events) {
233796
- const line = renderEventLine(event);
233797
- if (!line) continue;
233798
- const lineKey = `${line.label}:${line.text}`;
233799
- if (seen.has(lineKey)) continue;
233800
- seen.add(lineKey);
233801
- lines.push(renderStreamLine(line.label, line.text, options));
233802
- }
233803
- if (result2.assistantText.trim()) {
233804
- if (lines.length) lines.push("");
233805
- lines.push(
233806
- renderStreamLine(
233807
- personaLabel(options.personaId),
233808
- result2.assistantText.trim(),
233809
- options
233810
- )
233811
- );
233812
- }
233813
- if (!result2.ok) {
233814
- if (lines.length) lines.push("");
233815
- lines.push(
233816
- renderStreamLine(
233817
- "stop",
233818
- `Perch stopped: ${result2.error ?? result2.status}`,
233819
- { ...options, color: options.color }
233820
- )
233821
- );
233822
- }
233823
- return `${lines.join("\n").trim()}
233824
- `;
233825
- }
233826
- function cliExitCodeForTurn(result2) {
233827
- if (result2.status === "completed") return 0;
233828
- if (result2.status === "approval_required" || result2.status === "blocked" || result2.status === "incomplete") {
233829
- return 2;
233830
- }
233831
- return 1;
233832
- }
233833
- function renderEventLine(event) {
233834
- switch (event.type) {
233835
- case "assistant_preamble":
233836
- case "activity_delta":
233837
- case "workflow_narration":
233838
- return event.text.trim() ? { label: "perch", text: event.text.trim() } : null;
233839
- case "model_call_started":
233840
- return { label: "work", text: event.lane ? `Working (${event.lane})` : "Working" };
233841
- case "tool_call_started":
233842
- return { label: "tool", text: `Running ${humanizeToolName(event.toolName)}` };
233843
- case "tool_call_failed":
233844
- return {
233845
- label: "need",
233846
- text: `Needs attention: ${humanizeToolName(event.toolName)}${event.error ? ` \u2014 ${event.error}` : ""}`
233847
- };
233848
- case "live_card_start":
233849
- return { label: "tool", text: event.subtitle ? `${event.title} \xB7 ${event.subtitle}` : event.title };
233850
- case "live_step":
233851
- return { label: "tool", text: `${event.label} ${event.status}` };
233852
- case "live_card_end":
233853
- return event.receipt?.summary || event.error ? { label: event.error ? "need" : "tool", text: event.receipt?.summary ?? event.error ?? "" } : null;
233854
- case "workflow_card_start":
233855
- return { label: "tool", text: `${event.personaName}: ${event.workflowLabel}` };
233856
- case "workflow_step":
233857
- return { label: "tool", text: `${event.label} ${event.status}` };
233858
- case "managed_workflow_started":
233859
- return { label: "tool", text: event.title };
233860
- case "managed_workflow_completed":
233861
- return { label: "tool", text: event.summary };
233862
- case "capability_required":
233863
- return { label: "need", text: `Action needed: ${event.message}` };
233864
- case "executor_waiting_for_user":
233865
- return { label: "wait", text: `Waiting: ${event.prompt}` };
233866
- case "executor_result_summary":
233867
- return { label: "tool", text: event.summary };
233868
- default:
233869
- return null;
233870
- }
233871
- }
233872
- function humanizeToolName(name) {
233873
- return name.replace(/^mcp__/, "").replace(/_/g, " ");
233874
- }
233875
- function renderStreamLine(label, text, options) {
233876
- if (!options.streamLabels) return text;
233877
- const rawLabel = label.toLowerCase().slice(0, 7);
233878
- const padded = rawLabel.padEnd(7, " ");
233879
- const labelText = colorize(padded, colorForLabel(rawLabel), options.color);
233880
- const body = colorizeMultiline(text, colorForBody(rawLabel), options.color, " ".repeat(7));
233881
- return `${labelText} ${body}`;
233882
- }
233883
- function colorize(text, color, enabled) {
233884
- return enabled ? `${ANSI[color]}${text}${ANSI.reset}` : text;
233885
- }
233886
- function colorizeMultiline(text, color, enabled, continuationIndent) {
233887
- const painted = colorize(text, color, enabled);
233888
- return painted.replace(/\n/g, `
233889
- ${continuationIndent} `);
233890
- }
233891
- function colorForLabel(label) {
233892
- if (label === "need" || label === "stop") return "red";
233893
- if (label === "tool" || label === "work" || label === "wait") return "muted";
233894
- if (label === "you") return "bronze";
233895
- return "accent";
233896
- }
233897
- function colorForBody(label) {
233898
- if (label === "need" || label === "stop") return "red";
233899
- if (label === "tool" || label === "work" || label === "wait") return "muted";
233900
- return "cream";
233901
- }
233902
- function personaLabel(personaId) {
233903
- return personaId === "quill" ? "quill" : personaId === "saffron" ? "saffron" : "perch";
233904
- }
233905
- var ANSI;
233906
- var init_renderCliTurn = __esm({
233907
- "features/perchTerminal/runtime/cliHost/renderCliTurn.ts"() {
233908
- "use strict";
233909
- ANSI = {
233910
- reset: "\x1B[0m",
233911
- accent: "\x1B[38;2;47;94;67m",
233912
- bronze: "\x1B[38;2;154;82;40m",
233913
- cream: "\x1B[38;2;255;248;240m",
233914
- muted: "\x1B[38;2;142;133;125m",
233915
- green: "\x1B[38;2;110;191;130m",
233916
- red: "\x1B[38;2;208;107;85m"
233917
- };
233918
- }
233919
- });
233920
-
233921
- // features/perchTerminal/runtime/cliHost/index.ts
233922
- var init_cliHost = __esm({
233923
- "features/perchTerminal/runtime/cliHost/index.ts"() {
233924
- "use strict";
233925
- init_runCliTurn();
233926
- init_renderCliTurn();
233927
- init_nodeLocalBridge();
233928
- }
233929
- });
233930
-
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
234259
  // features/perchTerminal/runtime/cliHost/cliAuthSession.ts
233951
234260
  import { execFile } from "node:child_process";
233952
234261
  import fs13 from "node:fs/promises";
233953
234262
  import os3 from "node:os";
233954
- import path15 from "node:path";
234263
+ import path14 from "node:path";
233955
234264
  import { promisify } from "node:util";
233956
234265
  async function readStoredCliAuthSession() {
233957
234266
  const raw = shouldUseFallbackAuthFile() ? await readFallbackSecret().catch(() => null) : process.platform === "darwin" ? await readKeychainSecret().catch(() => null) : await readFallbackSecret().catch(() => null);
@@ -234028,15 +234337,15 @@ async function readKeychainSecret() {
234028
234337
  return stdout.trim() || null;
234029
234338
  }
234030
234339
  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");
234340
+ const base = process.env.PERCH_CLI_AUTH_DIR?.trim() || path14.join(os3.homedir(), ".perch");
234341
+ return path14.join(base, "cli-auth-session.json");
234033
234342
  }
234034
234343
  async function readFallbackSecret() {
234035
234344
  return await fs13.readFile(fallbackSessionPath(), "utf8");
234036
234345
  }
234037
234346
  async function writeFallbackSecret(raw) {
234038
234347
  const filePath = fallbackSessionPath();
234039
- await fs13.mkdir(path15.dirname(filePath), { recursive: true, mode: 448 });
234348
+ await fs13.mkdir(path14.dirname(filePath), { recursive: true, mode: 448 });
234040
234349
  await fs13.writeFile(filePath, raw, { mode: 384 });
234041
234350
  }
234042
234351
  var execFileAsync, KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT;
@@ -234049,6 +234358,162 @@ var init_cliAuthSession = __esm({
234049
234358
  }
234050
234359
  });
234051
234360
 
234361
+ // features/perchTerminal/runtime/publicModelDefault.ts
234362
+ function parsePublicModelDefaultPayload(value) {
234363
+ const raw = value && typeof value === "object" ? value : {};
234364
+ const selection = sanitizeFounderModelSelection(
234365
+ raw.selection ?? DEFAULT_FOUNDER_MODEL_SELECTION
234366
+ );
234367
+ return {
234368
+ selection,
234369
+ updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : null,
234370
+ updatedBy: typeof raw.updatedBy === "string" ? raw.updatedBy : null
234371
+ };
234372
+ }
234373
+ var init_publicModelDefault = __esm({
234374
+ "features/perchTerminal/runtime/publicModelDefault.ts"() {
234375
+ "use strict";
234376
+ init_modelRegistry();
234377
+ }
234378
+ });
234379
+
234380
+ // features/perchTerminal/runtime/cliHost/installCliModelProxyAuthRefresh.ts
234381
+ function installCliModelProxyAuthRefresh() {
234382
+ setCliModelProxyAuthRefresher(async () => {
234383
+ const fresh = await ensureFreshCliAuthSession({
234384
+ session: await readStoredCliAuthSession(),
234385
+ forceRefresh: true
234386
+ });
234387
+ if (!fresh?.accessToken?.trim()) return null;
234388
+ if (typeof process !== "undefined") {
234389
+ process.env[MODEL_CALL_PROXY_TOKEN_ENV2] = fresh.accessToken;
234390
+ }
234391
+ return fresh.accessToken;
234392
+ });
234393
+ return () => setCliModelProxyAuthRefresher(null);
234394
+ }
234395
+ var MODEL_CALL_PROXY_TOKEN_ENV2;
234396
+ var init_installCliModelProxyAuthRefresh = __esm({
234397
+ "features/perchTerminal/runtime/cliHost/installCliModelProxyAuthRefresh.ts"() {
234398
+ "use strict";
234399
+ init_cliAuthRefreshRegistry();
234400
+ init_cliAuthRefresh();
234401
+ init_cliAuthSession();
234402
+ MODEL_CALL_PROXY_TOKEN_ENV2 = "PERCH_MODEL_CALL_PROXY_TOKEN";
234403
+ }
234404
+ });
234405
+
234406
+ // features/perchTerminal/runtime/cliHost/modelConnection.ts
234407
+ async function connectCliModelProxy(input = {}) {
234408
+ installCliModelProxyAuthRefresh();
234409
+ const storedSession = input.storedSession === void 0 ? await readStoredCliAuthSession() : input.storedSession;
234410
+ const usableStoredSession = await ensureFreshCliAuthSession({
234411
+ session: storedSession,
234412
+ fetchImpl: input.fetchImpl
234413
+ });
234414
+ const appUrl = resolveCliAppUrl(input.appUrl, usableStoredSession?.appUrl ?? null);
234415
+ if (!appUrl) return noCliModelConnection();
234416
+ if (!usableStoredSession?.accessToken) return noCliModelConnection(appUrl);
234417
+ const fetchImpl = input.fetchImpl ?? globalThis.fetch;
234418
+ if (typeof fetchImpl !== "function") return noCliModelConnection(appUrl);
234419
+ const selection = await fetchPublicModelDefaultSelection(
234420
+ appUrl,
234421
+ fetchImpl,
234422
+ usableStoredSession?.accessToken ?? null
234423
+ );
234424
+ if (!selection) return noCliModelConnection(appUrl);
234425
+ const priorProxy = process.env[MODEL_PROXY_ENV];
234426
+ const priorToken = process.env[MODEL_PROXY_TOKEN_ENV];
234427
+ process.env[MODEL_PROXY_ENV] = appUrl;
234428
+ if (usableStoredSession?.accessToken) {
234429
+ process.env[MODEL_PROXY_TOKEN_ENV] = usableStoredSession.accessToken;
234430
+ }
234431
+ return {
234432
+ appUrl,
234433
+ authenticated: !!usableStoredSession?.accessToken,
234434
+ userId: usableStoredSession?.userId ?? null,
234435
+ email: usableStoredSession?.email ?? null,
234436
+ founderModelSelection: selection,
234437
+ restore: () => {
234438
+ if (priorProxy === void 0) {
234439
+ delete process.env[MODEL_PROXY_ENV];
234440
+ } else {
234441
+ process.env[MODEL_PROXY_ENV] = priorProxy;
234442
+ }
234443
+ if (priorToken === void 0) {
234444
+ delete process.env[MODEL_PROXY_TOKEN_ENV];
234445
+ } else {
234446
+ process.env[MODEL_PROXY_TOKEN_ENV] = priorToken;
234447
+ }
234448
+ }
234449
+ };
234450
+ }
234451
+ function resolveCliAppUrl(explicit, stored) {
234452
+ const raw = explicit?.trim() || process.env[CLI_APP_URL_ENV]?.trim() || process.env[FALLBACK_APP_URL_ENV]?.trim() || stored?.trim() || DEFAULT_CLI_APP_URL;
234453
+ if (!raw) return null;
234454
+ const withProtocol = /^https?:\/\//i.test(raw) ? raw : `http://${raw}`;
234455
+ try {
234456
+ const url = new URL(withProtocol);
234457
+ url.pathname = url.pathname.replace(/\/+$/, "");
234458
+ url.search = "";
234459
+ url.hash = "";
234460
+ return url.toString().replace(/\/+$/, "");
234461
+ } catch {
234462
+ return null;
234463
+ }
234464
+ }
234465
+ async function fetchPublicModelDefaultSelection(appUrl, fetchImpl, accessToken) {
234466
+ const url = `${appUrl}/api/perch-terminal/public-model-default`;
234467
+ const controller = new AbortController();
234468
+ const timeout = setTimeout(() => controller.abort(), 3500);
234469
+ try {
234470
+ const response = await fetchImpl(url, {
234471
+ method: "GET",
234472
+ headers: {
234473
+ Accept: "application/json",
234474
+ ...accessToken ? { Authorization: `Bearer ${accessToken}` } : {}
234475
+ },
234476
+ signal: controller.signal
234477
+ });
234478
+ if (!response.ok) return null;
234479
+ const body = await response.json();
234480
+ if (!body || typeof body !== "object") return null;
234481
+ const record = body;
234482
+ if (record.ok !== true) return null;
234483
+ return parsePublicModelDefaultPayload({ selection: record.selection }).selection;
234484
+ } catch {
234485
+ return null;
234486
+ } finally {
234487
+ clearTimeout(timeout);
234488
+ }
234489
+ }
234490
+ function noCliModelConnection(appUrl = null) {
234491
+ return {
234492
+ appUrl,
234493
+ authenticated: false,
234494
+ userId: null,
234495
+ email: null,
234496
+ founderModelSelection: null,
234497
+ restore: () => {
234498
+ }
234499
+ };
234500
+ }
234501
+ var MODEL_PROXY_ENV, MODEL_PROXY_TOKEN_ENV, CLI_APP_URL_ENV, FALLBACK_APP_URL_ENV, DEFAULT_CLI_APP_URL;
234502
+ var init_modelConnection = __esm({
234503
+ "features/perchTerminal/runtime/cliHost/modelConnection.ts"() {
234504
+ "use strict";
234505
+ init_publicModelDefault();
234506
+ init_cliAuthSession();
234507
+ init_cliAuthRefresh();
234508
+ init_installCliModelProxyAuthRefresh();
234509
+ MODEL_PROXY_ENV = "PERCH_MODEL_CALL_PROXY_URL";
234510
+ MODEL_PROXY_TOKEN_ENV = "PERCH_MODEL_CALL_PROXY_TOKEN";
234511
+ CLI_APP_URL_ENV = "PERCH_CLI_APP_URL";
234512
+ FALLBACK_APP_URL_ENV = "PERCH_APP_URL";
234513
+ DEFAULT_CLI_APP_URL = "https://app.perchai.app";
234514
+ }
234515
+ });
234516
+
234052
234517
  // features/perchTerminal/runtime/cliHost/cliStandaloneOAuth.ts
234053
234518
  import { execFile as execFile2 } from "node:child_process";
234054
234519
  import http from "node:http";
@@ -234263,7 +234728,7 @@ async function ensureFreshCliAuthSession(input) {
234263
234728
  if (!session?.accessToken?.trim()) return null;
234264
234729
  const nowSeconds = Math.floor(Date.now() / 1e3);
234265
234730
  const stillUsable = !session.expiresAt || session.expiresAt > nowSeconds + 90;
234266
- if (stillUsable) return session;
234731
+ if (stillUsable && !input.forceRefresh) return session;
234267
234732
  if (!session.refreshToken?.trim()) return null;
234268
234733
  const fetchImpl = input.fetchImpl ?? globalThis.fetch;
234269
234734
  if (typeof fetchImpl !== "function") return null;
@@ -234308,112 +234773,340 @@ var init_cliAuthRefresh = __esm({
234308
234773
  }
234309
234774
  });
234310
234775
 
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;
234776
+ // features/perchTerminal/runtime/cliHost/refreshCliTurnAuth.ts
234777
+ async function refreshCliTurnAuth(input) {
234778
+ if (input.cliLocalTools !== true) return input;
234779
+ const stored = await readStoredCliAuthSession();
234780
+ if (!stored?.accessToken?.trim()) return input;
234781
+ const turnToken = input.cliServerAccessToken?.trim();
234782
+ if (turnToken && turnToken !== stored.accessToken) return input;
234783
+ const fresh = await ensureFreshCliAuthSession({ session: stored });
234784
+ if (!fresh?.accessToken?.trim()) return input;
234785
+ if (fresh.accessToken === input.cliServerAccessToken) return input;
234786
+ if (typeof process !== "undefined") {
234787
+ process.env.PERCH_MODEL_CALL_PROXY_TOKEN = fresh.accessToken;
234334
234788
  }
234335
234789
  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
- }
234790
+ ...input,
234791
+ cliServerAccessToken: fresh.accessToken,
234792
+ marketDeskProxyAccessToken: input.marketDeskProxyAccessToken ?? fresh.accessToken
234353
234793
  };
234354
234794
  }
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;
234795
+ var init_refreshCliTurnAuth = __esm({
234796
+ "features/perchTerminal/runtime/cliHost/refreshCliTurnAuth.ts"() {
234797
+ "use strict";
234798
+ init_cliAuthSession();
234799
+ init_cliAuthRefresh();
234367
234800
  }
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);
234801
+ });
234802
+
234803
+ // features/perchTerminal/runtime/cliHost/runCliTurn.ts
234804
+ import path15 from "node:path";
234805
+ async function runPerchCliTurn(input, options = {}) {
234806
+ const prompt = input.prompt.trim();
234807
+ if (!prompt) throw new Error("Missing prompt for perch run");
234808
+ const now23 = options.now ?? (() => Date.now());
234809
+ const cwd2 = path15.resolve(input.cwd ?? process.cwd());
234810
+ const threadId = input.threadId?.trim() || `cli-${now23()}`;
234811
+ const events = [];
234812
+ let latestContextSnapshot = null;
234813
+ const turnRunner = resolveTurnRunnerForPrompt(prompt) ?? options.turnRunner ?? runOperatorTurn;
234814
+ const turnInput = buildCliTurnInput(input, {
234815
+ prompt,
234816
+ cwd: cwd2,
234817
+ threadId
234818
+ });
234819
+ const deps = buildCliTurnDeps({
234820
+ signal: input.signal ?? null,
234821
+ events,
234822
+ onEvent: input.onEvent,
234823
+ onContextSnapshot: (snapshot) => {
234824
+ latestContextSnapshot = snapshot;
234825
+ },
234826
+ refreshTurnInput: input.cliLocalTools !== false ? refreshCliTurnAuth : void 0
234827
+ });
234828
+ const restoreLocalBridge = input.cliLocalTools === false ? null : installCliNodeLocalBridge({ workspaceRoot: cwd2 });
234373
234829
  try {
234374
- const response = await fetchImpl(url, {
234375
- method: "GET",
234376
- headers: {
234377
- Accept: "application/json",
234378
- ...accessToken ? { Authorization: `Bearer ${accessToken}` } : {}
234830
+ const result2 = await turnRunner(turnInput, deps);
234831
+ return buildCliTurnResult({
234832
+ prompt,
234833
+ cwd: cwd2,
234834
+ threadId,
234835
+ events,
234836
+ contextSnapshot: latestContextSnapshot,
234837
+ result: result2,
234838
+ error: null
234839
+ });
234840
+ } catch (error) {
234841
+ const message = error instanceof Error ? error.message : String(error);
234842
+ const runId = findRunId(events) ?? threadId;
234843
+ return buildCliTurnResult({
234844
+ prompt,
234845
+ cwd: cwd2,
234846
+ threadId,
234847
+ events,
234848
+ contextSnapshot: latestContextSnapshot,
234849
+ result: {
234850
+ status: "failed",
234851
+ error: message,
234852
+ runId
234379
234853
  },
234380
- signal: controller.signal
234854
+ error: message
234381
234855
  });
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
234856
  } finally {
234391
- clearTimeout(timeout);
234857
+ restoreLocalBridge?.();
234392
234858
  }
234393
234859
  }
234394
- function noCliModelConnection(appUrl = null) {
234860
+ function buildCliTurnInput(input, resolved) {
234861
+ const workspaceId = input.workspaceId ?? null;
234862
+ const userId = input.userId ?? null;
234395
234863
  return {
234396
- appUrl,
234397
- authenticated: false,
234398
- userId: null,
234399
- email: null,
234400
- founderModelSelection: null,
234401
- restore: () => {
234864
+ trimmedInput: resolved.prompt,
234865
+ clientRunId: input.clientRunId ?? null,
234866
+ chatMode: input.chatMode ?? "agents",
234867
+ threadId: resolved.threadId,
234868
+ personaId: input.personaId ?? DEFAULT_PERSONA_ID,
234869
+ workspaceId,
234870
+ userId,
234871
+ selectedSourceId: input.selectedSourceId ?? null,
234872
+ recentMessages: input.recentMessages ?? [],
234873
+ usageRunIds: input.usageRunIds ?? [],
234874
+ sessionContext: emptyPerchTerminalSessionContext({
234875
+ userId,
234876
+ workspaceId
234877
+ }),
234878
+ desktopConnected: input.desktopConnected ?? false,
234879
+ cliLocalTools: input.cliLocalTools !== false,
234880
+ activeRootPath: input.activeRootPath ?? resolved.cwd,
234881
+ localSources: [],
234882
+ localSourcesMeta: null,
234883
+ projectMeta: readCliProjectMemoryState(resolved.cwd).meta,
234884
+ permanentMemories: input.permanentMemories ?? [],
234885
+ userMemories: input.userMemories ?? [],
234886
+ supabase: null,
234887
+ supabaseConfigured: false,
234888
+ cliServerAppUrl: input.cliServerAppUrl ?? input.marketDeskProxyAppUrl ?? input.appUrl ?? null,
234889
+ cliServerAccessToken: input.cliServerAccessToken ?? input.marketDeskProxyAccessToken ?? null,
234890
+ marketDeskProxyAppUrl: input.marketDeskProxyAppUrl ?? input.appUrl ?? null,
234891
+ marketDeskProxyAccessToken: input.marketDeskProxyAccessToken ?? null,
234892
+ permissionMode: normalizePermissionMode(input.permissionMode ?? "default"),
234893
+ founderModelSelection: input.founderModelSelection ?? null,
234894
+ threadEvidence: [],
234895
+ workspaceFileResolution: null,
234896
+ folderIndexSummary: null,
234897
+ mcpTools: [],
234898
+ coordinatorMode: false,
234899
+ systemNotifications: [],
234900
+ workflowState: null,
234901
+ skipUserMessagePersistence: false,
234902
+ systemInitiated: input.systemInitiated === true
234903
+ };
234904
+ }
234905
+ function buildCliTurnDeps(input) {
234906
+ return {
234907
+ signal: input.signal,
234908
+ onEvent: (event) => {
234909
+ input.events.push(event);
234910
+ input.onEvent?.(event);
234911
+ },
234912
+ onContextAssembled: input.onContextSnapshot,
234913
+ ...input.refreshTurnInput ? { refreshTurnInput: input.refreshTurnInput } : {},
234914
+ persistUserMessage: async () => null,
234915
+ persistAssistantMessage: async () => null,
234916
+ createWorkflowRun: async () => null,
234917
+ finishWorkflowRun: async () => {
234918
+ },
234919
+ persistToolCall: async () => {
234920
+ },
234921
+ persistSandboxExecution: async () => null,
234922
+ persistContextSnapshot: async () => null,
234923
+ createManagedArtifact: async () => null,
234924
+ createManagedReport: async () => null,
234925
+ logProductAuditEvent: async () => {
234402
234926
  }
234403
234927
  };
234404
234928
  }
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"() {
234929
+ function buildCliTurnResult(input) {
234930
+ const segments = buildTranscriptSegments({
234931
+ events: input.events,
234932
+ complete: true,
234933
+ error: input.error
234934
+ });
234935
+ const status = input.result.status;
234936
+ const assistantText = "assistantText" in input.result ? input.result.assistantText : "";
234937
+ const durationMs = "durationMs" in input.result ? input.result.durationMs : null;
234938
+ const error = input.error ?? ("error" in input.result ? input.result.error : void 0) ?? ("reason" in input.result ? input.result.reason : void 0);
234939
+ return {
234940
+ ok: status === "completed",
234941
+ status,
234942
+ prompt: input.prompt,
234943
+ cwd: input.cwd,
234944
+ threadId: input.threadId,
234945
+ runId: input.result.runId,
234946
+ durationMs,
234947
+ assistantText,
234948
+ events: input.events,
234949
+ segments,
234950
+ contextSnapshot: input.contextSnapshot,
234951
+ ...error ? { error } : {}
234952
+ };
234953
+ }
234954
+ function findRunId(events) {
234955
+ const started = events.find((event) => event.type === "turn_started");
234956
+ return started?.type === "turn_started" ? started.runId : null;
234957
+ }
234958
+ var init_runCliTurn = __esm({
234959
+ "features/perchTerminal/runtime/cliHost/runCliTurn.ts"() {
234408
234960
  "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";
234961
+ init_session();
234962
+ init_sharedSlashCommands();
234963
+ init_operatorStream();
234964
+ init_permissionModes();
234965
+ init_personaRegistry();
234966
+ init_runOperatorTurn();
234967
+ init_nodeLocalBridge();
234968
+ init_nodeLocalBridge();
234969
+ init_refreshCliTurnAuth();
234970
+ }
234971
+ });
234972
+
234973
+ // features/perchTerminal/runtime/cliHost/renderCliTurn.ts
234974
+ function renderCliTurnText(result2, options = {}) {
234975
+ const lines = [];
234976
+ const seen = /* @__PURE__ */ new Set();
234977
+ for (const event of result2.events) {
234978
+ const line = renderEventLine(event);
234979
+ if (!line) continue;
234980
+ const lineKey = `${line.label}:${line.text}`;
234981
+ if (seen.has(lineKey)) continue;
234982
+ seen.add(lineKey);
234983
+ lines.push(renderStreamLine(line.label, line.text, options));
234984
+ }
234985
+ if (result2.assistantText.trim()) {
234986
+ if (lines.length) lines.push("");
234987
+ lines.push(
234988
+ renderStreamLine(
234989
+ personaLabel(options.personaId),
234990
+ result2.assistantText.trim(),
234991
+ options
234992
+ )
234993
+ );
234994
+ }
234995
+ if (!result2.ok) {
234996
+ if (lines.length) lines.push("");
234997
+ lines.push(
234998
+ renderStreamLine(
234999
+ "stop",
235000
+ `Perch stopped: ${result2.error ?? result2.status}`,
235001
+ { ...options, color: options.color }
235002
+ )
235003
+ );
235004
+ }
235005
+ return `${lines.join("\n").trim()}
235006
+ `;
235007
+ }
235008
+ function cliExitCodeForTurn(result2) {
235009
+ if (result2.status === "completed") return 0;
235010
+ if (result2.status === "approval_required" || result2.status === "blocked" || result2.status === "incomplete") {
235011
+ return 2;
235012
+ }
235013
+ return 1;
235014
+ }
235015
+ function renderEventLine(event) {
235016
+ switch (event.type) {
235017
+ case "assistant_preamble":
235018
+ case "activity_delta":
235019
+ case "workflow_narration":
235020
+ return event.text.trim() ? { label: "perch", text: event.text.trim() } : null;
235021
+ case "model_call_started":
235022
+ return { label: "work", text: event.lane ? `Working (${event.lane})` : "Working" };
235023
+ case "tool_call_started":
235024
+ return { label: "tool", text: `Running ${humanizeToolName(event.toolName)}` };
235025
+ case "tool_call_failed":
235026
+ return {
235027
+ label: "need",
235028
+ text: `Needs attention: ${humanizeToolName(event.toolName)}${event.error ? ` \u2014 ${event.error}` : ""}`
235029
+ };
235030
+ case "live_card_start":
235031
+ return { label: "tool", text: event.subtitle ? `${event.title} \xB7 ${event.subtitle}` : event.title };
235032
+ case "live_step":
235033
+ return { label: "tool", text: `${event.label} ${event.status}` };
235034
+ case "live_card_end":
235035
+ return event.receipt?.summary || event.error ? { label: event.error ? "need" : "tool", text: event.receipt?.summary ?? event.error ?? "" } : null;
235036
+ case "workflow_card_start":
235037
+ return { label: "tool", text: `${event.personaName}: ${event.workflowLabel}` };
235038
+ case "workflow_step":
235039
+ return { label: "tool", text: `${event.label} ${event.status}` };
235040
+ case "managed_workflow_started":
235041
+ return { label: "tool", text: event.title };
235042
+ case "managed_workflow_completed":
235043
+ return { label: "tool", text: event.summary };
235044
+ case "capability_required":
235045
+ return { label: "need", text: `Action needed: ${event.message}` };
235046
+ case "executor_waiting_for_user":
235047
+ return { label: "wait", text: `Waiting: ${event.prompt}` };
235048
+ case "executor_result_summary":
235049
+ return { label: "tool", text: event.summary };
235050
+ default:
235051
+ return null;
235052
+ }
235053
+ }
235054
+ function humanizeToolName(name) {
235055
+ return name.replace(/^mcp__/, "").replace(/_/g, " ");
235056
+ }
235057
+ function renderStreamLine(label, text, options) {
235058
+ if (!options.streamLabels) return text;
235059
+ const rawLabel = label.toLowerCase().slice(0, 7);
235060
+ const padded = rawLabel.padEnd(7, " ");
235061
+ const labelText = colorize(padded, colorForLabel(rawLabel), options.color);
235062
+ const body = colorizeMultiline(text, colorForBody(rawLabel), options.color, " ".repeat(7));
235063
+ return `${labelText} ${body}`;
235064
+ }
235065
+ function colorize(text, color, enabled) {
235066
+ return enabled ? `${ANSI[color]}${text}${ANSI.reset}` : text;
235067
+ }
235068
+ function colorizeMultiline(text, color, enabled, continuationIndent) {
235069
+ const painted = colorize(text, color, enabled);
235070
+ return painted.replace(/\n/g, `
235071
+ ${continuationIndent} `);
235072
+ }
235073
+ function colorForLabel(label) {
235074
+ if (label === "need" || label === "stop") return "red";
235075
+ if (label === "tool" || label === "work" || label === "wait") return "muted";
235076
+ if (label === "you") return "bronze";
235077
+ return "accent";
235078
+ }
235079
+ function colorForBody(label) {
235080
+ if (label === "need" || label === "stop") return "red";
235081
+ if (label === "tool" || label === "work" || label === "wait") return "muted";
235082
+ return "cream";
235083
+ }
235084
+ function personaLabel(personaId) {
235085
+ return personaId === "quill" ? "quill" : personaId === "saffron" ? "saffron" : "perch";
235086
+ }
235087
+ var ANSI;
235088
+ var init_renderCliTurn = __esm({
235089
+ "features/perchTerminal/runtime/cliHost/renderCliTurn.ts"() {
235090
+ "use strict";
235091
+ ANSI = {
235092
+ reset: "\x1B[0m",
235093
+ accent: "\x1B[38;2;47;94;67m",
235094
+ bronze: "\x1B[38;2;154;82;40m",
235095
+ cream: "\x1B[38;2;255;248;240m",
235096
+ muted: "\x1B[38;2;142;133;125m",
235097
+ green: "\x1B[38;2;110;191;130m",
235098
+ red: "\x1B[38;2;208;107;85m"
235099
+ };
235100
+ }
235101
+ });
235102
+
235103
+ // features/perchTerminal/runtime/cliHost/index.ts
235104
+ var init_cliHost = __esm({
235105
+ "features/perchTerminal/runtime/cliHost/index.ts"() {
235106
+ "use strict";
235107
+ init_runCliTurn();
235108
+ init_renderCliTurn();
235109
+ init_nodeLocalBridge();
234417
235110
  }
234418
235111
  });
234419
235112
 
@@ -234937,6 +235630,17 @@ function claimFirstRun(dir = stateDir()) {
234937
235630
  return false;
234938
235631
  }
234939
235632
  }
235633
+ function claimFirstPrompt(dir = stateDir()) {
235634
+ const file = path17.join(dir, "cli-first-prompt");
235635
+ try {
235636
+ if (fs15.existsSync(file)) return false;
235637
+ fs15.mkdirSync(dir, { recursive: true });
235638
+ fs15.writeFileSync(file, (/* @__PURE__ */ new Date()).toISOString());
235639
+ return true;
235640
+ } catch {
235641
+ return false;
235642
+ }
235643
+ }
234940
235644
  async function postCapture(event, properties) {
234941
235645
  const controller = new AbortController();
234942
235646
  const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS2);
@@ -234963,6 +235667,17 @@ function captureCliFirstRun(version4) {
234963
235667
  if (!claimFirstRun()) return;
234964
235668
  void postCapture("cli_first_run", { version: version4, platform: process.platform });
234965
235669
  }
235670
+ function captureCliSessionStarted() {
235671
+ if (!POSTHOG_KEY || telemetryDisabled()) return;
235672
+ void postCapture("cli_session_started", { platform: process.platform });
235673
+ }
235674
+ function captureCliMessageSent() {
235675
+ if (!POSTHOG_KEY || telemetryDisabled()) return;
235676
+ void postCapture("message_sent", { surface: "cli" });
235677
+ if (claimFirstPrompt()) {
235678
+ void postCapture("first_message", { surface: "cli" });
235679
+ }
235680
+ }
234966
235681
  var POSTHOG_KEY, POSTHOG_HOST, FETCH_TIMEOUT_MS2;
234967
235682
  var init_cliAnalytics = __esm({
234968
235683
  "features/perchTerminal/runtime/cliHost/cliAnalytics.ts"() {
@@ -289269,7 +289984,7 @@ function flockEventToCliRow(event) {
289269
289984
  return {
289270
289985
  id: `flock-${event.flockId}-${event.flockWorkerId}`,
289271
289986
  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"
289987
+ tone: event.status === "done" ? "success" : event.status === "failed" || event.status === "blocked" ? "danger" : "muted"
289273
289988
  };
289274
289989
  case "flock_run_completed":
289275
289990
  return {
@@ -289390,6 +290105,7 @@ ${HELP_TEXT}`);
289390
290105
  recentMessages: nextRecentMessages,
289391
290106
  contextSnapshot: result3.contextSnapshot ?? persisted?.contextSnapshot ?? null
289392
290107
  });
290108
+ captureCliMessageSent();
289393
290109
  return cliExitCodeForTurn(result3);
289394
290110
  } finally {
289395
290111
  connection.restore();
@@ -289690,6 +290406,7 @@ async function runReadlineInteractivePerchCli(writer, deps, options) {
289690
290406
  terminal: true
289691
290407
  });
289692
290408
  const state = createInteractiveCliState(options);
290409
+ captureCliSessionStarted();
289693
290410
  const connectModelProxy = deps.connectModelProxy ?? connectCliModelProxy;
289694
290411
  let connection = await connectModelProxy({ appUrl: state.appUrl });
289695
290412
  if (!state.appUrl && connection.appUrl) state.appUrl = connection.appUrl;
@@ -289750,6 +290467,7 @@ async function runReadlineInteractivePerchCli(writer, deps, options) {
289750
290467
  });
289751
290468
  if (!opts.systemInitiated) {
289752
290469
  appendRecentMessage(state.recentMessages, "user", turnPrompt);
290470
+ captureCliMessageSent();
289753
290471
  }
289754
290472
  if (result2.assistantText.trim()) {
289755
290473
  appendRecentMessage(state.recentMessages, "assistant", result2.assistantText.trim());
@@ -289842,6 +290560,7 @@ async function runInkInteractivePerchCli(writer, deps, options) {
289842
290560
  const runTurn = deps.runCliTurn ?? runPerchCliTurn;
289843
290561
  const updateNotice = resolveUpdateNotice(CLI_PACKAGE_VERSION);
289844
290562
  captureCliFirstRun(CLI_PACKAGE_VERSION);
290563
+ captureCliSessionStarted();
289845
290564
  const instance = Ink2.render(
289846
290565
  React11.createElement(function PerchInkApp() {
289847
290566
  const app = Ink2.useApp();
@@ -290463,6 +291182,7 @@ async function runInkInteractivePerchCli(writer, deps, options) {
290463
291182
  }
290464
291183
  if (!systemInitiated) {
290465
291184
  appendRecentMessage(state.recentMessages, "user", prompt);
291185
+ captureCliMessageSent();
290466
291186
  }
290467
291187
  if (assistantText) appendRecentMessage(state.recentMessages, "assistant", assistantText);
290468
291188
  trimRecentMessages(state.recentMessages);