perchai-cli 2.4.19 → 2.4.21

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 +1059 -213
  2. package/package.json +1 -1
package/dist/perch.mjs CHANGED
@@ -76200,6 +76200,7 @@ function getToolDisplayName(toolName) {
76200
76200
  var NON_MODULE_TOOL_OWNERS, TOOL_RISK, TOOL_DISPLAY_NAMES;
76201
76201
  var init_catalog = __esm({
76202
76202
  "features/perchTerminal/runtime/toolSystem/catalog.ts"() {
76203
+ "use strict";
76203
76204
  init_toolNames();
76204
76205
  NON_MODULE_TOOL_OWNERS = {
76205
76206
  [TOOL_NAMES.listSources]: "lane",
@@ -78259,7 +78260,7 @@ var init_permissionModes = __esm({
78259
78260
  });
78260
78261
 
78261
78262
  // features/perchTerminal/runtime/perchMemoryGuidance.ts
78262
- var VISIBLE_OUTPUT_STYLE_GUIDANCE, SAFFRON_CORE_IDENTITY, QUILL_CORE_IDENTITY, PERCH_MEMORY_GUIDANCE;
78263
+ var VISIBLE_OUTPUT_STYLE_GUIDANCE, SAFFRON_STYLE_CONTRACT, QUILL_STYLE_CONTRACT, SAFFRON_CORE_IDENTITY, QUILL_CORE_IDENTITY, PERCH_MEMORY_GUIDANCE;
78263
78264
  var init_perchMemoryGuidance = __esm({
78264
78265
  "features/perchTerminal/runtime/perchMemoryGuidance.ts"() {
78265
78266
  "use strict";
@@ -78274,6 +78275,31 @@ Avoid stock AI phrases: "delve," "nuanced," "robust," "seamless,"
78274
78275
  Be concise when the user is moving fast.
78275
78276
  Preserve the persona: Quill is warm, literate, and direct; Saffron is sharp,
78276
78277
  practical, and confident.
78278
+ `.trim();
78279
+ SAFFRON_STYLE_CONTRACT = `
78280
+ ## Voice contract \u2014 Saffron (visible output only)
78281
+
78282
+ This contract shapes how your replies read. It never changes which tools you
78283
+ use, which tasks you take on, or how the work itself is done.
78284
+
78285
+ - Direct, sharp, practical, operator-like, confident.
78286
+ - Lead with the finding or the action. Decoration is at most a dry aside.
78287
+ - Memory texture: use remembered facts operationally ("your close lands on the
78288
+ 5th, so this ships before it") \u2014 state the fact, use it, move on.
78289
+ - Concise. Confidence reads as economy, not volume.
78290
+ `.trim();
78291
+ QUILL_STYLE_CONTRACT = `
78292
+ ## Voice contract \u2014 Quill (visible output only)
78293
+
78294
+ This contract shapes how your replies read. It never changes which tools you
78295
+ use, which tasks you take on, or how the work itself is done.
78296
+
78297
+ - Warm, personable, literate, more emotionally present than a status report.
78298
+ - Lightly humorous when it arises naturally; never forced, never a bit.
78299
+ - Memory texture: when relevant personal memories are in context, weave them in
78300
+ naturally ("you mentioned the board reads these, so I kept it tight").
78301
+ - Still concise and useful. Warmth lives in the engagement, not the word count.
78302
+ - Hard limits: not verbose, not therapeutic, not flirty, not over-familiar.
78277
78303
  `.trim();
78278
78304
  SAFFRON_CORE_IDENTITY = `
78279
78305
  You are Saffron \u2014 Perch's workspace operator.
@@ -78410,6 +78436,11 @@ working through a paper. Founders tightening a pitch. Anyone who wants their
78410
78436
  words to land. You don't try to write *for* people; you help them write
78411
78437
  *better*. You know the difference, and you respect the difference.
78412
78438
 
78439
+ Writing is your craft home, not your fence. You're a full Perch operator: when
78440
+ the user needs analysis, files, data, an audit, or delivery, you do that work
78441
+ yourself with the same tools \u2014 in your own voice. You never punt real work to
78442
+ another persona.
78443
+
78413
78444
  ## How you work
78414
78445
 
78415
78446
  Read first. Before you draft a single sentence, you know what the piece is for,
@@ -80877,6 +80908,7 @@ var init_personaRegistry = __esm({
80877
80908
  label: "Saffron",
80878
80909
  shortDescription: "Operator. Direct, sharp, finds the thing.",
80879
80910
  identityPrompt: SAFFRON_CORE_IDENTITY,
80911
+ styleContract: SAFFRON_STYLE_CONTRACT,
80880
80912
  suggestedPrompts: [
80881
80913
  "Audit the AP folder for duplicates and anomalies",
80882
80914
  "Reconcile the GL against last month's subledger",
@@ -80900,6 +80932,7 @@ var init_personaRegistry = __esm({
80900
80932
  label: "Quill",
80901
80933
  shortDescription: "Writing companion. Warm, craft-aware, reads first.",
80902
80934
  identityPrompt: QUILL_CORE_IDENTITY,
80935
+ styleContract: QUILL_STYLE_CONTRACT,
80903
80936
  suggestedPrompts: [
80904
80937
  "Draft a memo on personal jurisdiction",
80905
80938
  "Brief Pennoyer v. Neff",
@@ -80931,12 +80964,14 @@ function buildDesktopContextSection(input) {
80931
80964
  "grep",
80932
80965
  "statPath",
80933
80966
  "readLocalFile",
80967
+ "readProjectMemory",
80934
80968
  "listLocalSources",
80935
80969
  "readLocalSourceFile"
80936
80970
  ].filter((name) => enabledToolSet2.has(name));
80937
80971
  const localWriteTools = [
80938
80972
  "writeLocalFile",
80939
80973
  "editLocalFile",
80974
+ "saveToMemory",
80940
80975
  "bash",
80941
80976
  "runBashTerminalCommand",
80942
80977
  "generateAPAuditPacket",
@@ -80948,16 +80983,16 @@ function buildDesktopContextSection(input) {
80948
80983
  return {
80949
80984
  id: "desktop-context",
80950
80985
  lane: "desktop",
80951
- label: "CLI workspace",
80986
+ label: "Terminal workspace",
80952
80987
  content: [
80953
- "## CLI local workspace",
80954
- "Perch is running from a terminal. Local filesystem, shell, sandbox-code, and AP evidence tools are available through CLI local workspace access.",
80988
+ "## Terminal workspace",
80989
+ "Perch is running from a terminal. Files, shell, sandbox-code, and AP evidence tools are available for the current workspace.",
80955
80990
  input.activeRootPath?.trim() ? `Workspace root: ${input.activeRootPath}. Treat relative paths as relative to this root.` : "Workspace root: current terminal directory.",
80956
- "GUI-only services are not connected here: embedded browser, Google/Gmail/Calendar delivery, Desktop RAG indexing, MCP Desktop servers, and project memory writes are unavailable unless the desktop app is running.",
80991
+ "App-only services are not connected here: embedded browser, Google/Gmail/Calendar delivery, attached-folder semantic source indexing, and connected desktop integrations. CLI memory can still use signed-in durable memory or local .perch project memory when available.",
80957
80992
  localReadTools.length > 0 ? `Read tools: ${localReadTools.join(", ")}.` : "Read tools: none exposed for this turn.",
80958
80993
  localWriteTools.length > 0 ? `Write/command tools: ${localWriteTools.join(", ")}. These are governed by the selected permission mode and command policy.` : "Write/command tools: none exposed for this turn."
80959
80994
  ].join("\n"),
80960
- reason: "Terminal local workspace access and CLI tool availability for this turn.",
80995
+ reason: "Terminal workspace and tool availability for this turn.",
80961
80996
  sourcePath: input.activeRootPath,
80962
80997
  metadata: {
80963
80998
  desktopConnected: false,
@@ -81016,8 +81051,14 @@ function buildDesktopContextSection(input) {
81016
81051
  (file) => `- ${file.relativePath} (${file.matchReason})`
81017
81052
  ) : [];
81018
81053
  const hasVisionSupport = input.selectedModelVisionSupport === true;
81019
- const searchHints = input.folderIndexSummary && input.folderIndexSummary.retrievalReadyFiles > 0 ? [
81020
- "Prefer retrieveContext for questions that span multiple files (cross-file evidence, duplicates, totals).",
81054
+ const hasIndexedSourceSearch = Boolean(
81055
+ input.folderIndexSummary && (input.folderIndexSummary.sourceChunkCount > 0 || input.folderIndexSummary.retrievalChunkCount > 0)
81056
+ );
81057
+ const hasVectorSearch = Boolean(
81058
+ input.folderIndexSummary && (input.folderIndexSummary.embeddedChunkCount > 0 || input.folderIndexSummary.vectorReadyFiles > 0)
81059
+ );
81060
+ const searchHints = hasIndexedSourceSearch ? [
81061
+ hasVectorSearch ? "Prefer retrieveContext for questions that span multiple files (cross-file evidence, duplicates, totals)." : "Keyword/source search is available through retrieveContext; semantic/vector retrieval is pending until embedded chunks are created.",
81021
81062
  "Use glob for named files like *Screenshot* or exact extensions like *.png.",
81022
81063
  "Use statPath or glob before claiming a file does not exist.",
81023
81064
  hasVisionSupport ? "For screenshots/images/PDF pages, open the file with readLocalSourceFile/readLocalFile and read the attached visual preview directly." : "For screenshots/images/PDF pages, open the file with readLocalSourceFile/readLocalFile; if visual details matter and no usable text was extracted, call visionInspect."
@@ -81026,8 +81067,16 @@ function buildDesktopContextSection(input) {
81026
81067
  "Use statPath or glob before claiming a file does not exist.",
81027
81068
  hasVisionSupport ? "For screenshots/images/PDF pages, open the file with readLocalSourceFile/readLocalFile and read the attached visual preview directly." : "For screenshots/images/PDF pages, open the file with readLocalSourceFile/readLocalFile; if visual details matter and no usable text was extracted, call visionInspect."
81028
81069
  ];
81029
- const folderIndexLine = input.folderIndexSummary ? `Folder index: ${input.folderIndexSummary.indexedFiles}/${input.folderIndexSummary.totalFiles} indexed \xB7 ${input.folderIndexSummary.retrievalReadyFiles} retrieval-ready \xB7 ${input.folderIndexSummary.filesNeedingOcr} need OCR.` : "Folder index: not loaded for this turn.";
81030
- const retrievalGuidance = input.folderIndexSummary && input.folderIndexSummary.retrievalReadyFiles > 20 ? `This folder has ${input.folderIndexSummary.retrievalReadyFiles} files with semantic embeddings. Use retrieveContext for cross-file queries instead of serial readLocalSourceFile calls. The recent-files list below is a sample \u2014 not the full corpus.` : null;
81070
+ const folderIndexLine = input.folderIndexSummary ? [
81071
+ `Folder index: ${input.folderIndexSummary.indexedFiles}/${input.folderIndexSummary.totalFiles} indexed`,
81072
+ `${input.folderIndexSummary.registeredSources} registered source(s)`,
81073
+ `${input.folderIndexSummary.sourceChunkCount} text/source chunk(s)`,
81074
+ `${input.folderIndexSummary.retrievalChunkCount} retrieval chunk(s)`,
81075
+ `${input.folderIndexSummary.embeddedChunkCount} embedded retrieval chunk(s)`,
81076
+ `${input.folderIndexSummary.vectorReadyFiles} vector-ready file(s)`,
81077
+ `${input.folderIndexSummary.filesNeedingOcr} need OCR`
81078
+ ].join(" \xB7 ") + "." : "Folder index: not loaded for this turn.";
81079
+ const retrievalGuidance = input.folderIndexSummary && input.folderIndexSummary.vectorReadyFiles > 20 ? `This folder has ${input.folderIndexSummary.vectorReadyFiles} files with embedded retrieval chunks for semantic/vector search. Use retrieveContext for cross-file queries instead of serial readLocalSourceFile calls. The recent-files list below is a sample \u2014 not the full corpus.` : input.folderIndexSummary && input.folderIndexSummary.sourceChunkCount > 0 && input.folderIndexSummary.embeddedChunkCount === 0 ? "This folder has keyword/source chunks, but no embedded retrieval chunks yet. Use retrieveContext for keyword-backed source search; semantic/vector retrieval is pending." : null;
81031
81080
  const mcpToolNames = enabledToolSet.has("mcp__") ? [] : Array.from(enabledToolSet).filter((tool) => tool.startsWith("mcp__"));
81032
81081
  const mcpServerNames = Array.from(
81033
81082
  new Set(mcpToolNames.map((tool) => tool.split("__")[1]).filter(Boolean))
@@ -81060,7 +81109,7 @@ function buildDesktopContextSection(input) {
81060
81109
  content: [
81061
81110
  "## Desktop environment",
81062
81111
  "Local workspace access is connected. Local filesystem tools are available.",
81063
- input.activeRootPath?.trim() ? `Optional folder scope: ${input.activeRootPath} (you are NOT limited to it \u2014 absolute paths anywhere in the user's home work directly).` : "No folder scope is selected, and none is needed. When the user gives a file path, USE IT DIRECTLY \u2014 call readLocalFile / glob / grep / visionInspect with the absolute path (e.g. /Users/you/Desktop/shot.png). Full-access policy already allows any path in the user's home. NEVER tell the user to select or approve a folder, and NEVER refuse a file because no folder is selected.",
81112
+ input.activeRootPath?.trim() ? `Workspace folder: ${input.activeRootPath} (optional; absolute paths anywhere in the user's home work directly).` : "No workspace folder is selected, and none is needed. When the user gives a file path, use it directly with readLocalFile / glob / grep / visionInspect and the absolute path (for example /Users/you/Desktop/shot.png).",
81064
81113
  `Visible local sources: ${totalVisibleFiles}`,
81065
81114
  input.localSourcesMeta?.refreshedAt ? `Local source snapshot refreshed: ${input.localSourcesMeta.refreshedAt}` : null,
81066
81115
  input.localSourcesMeta?.truncated ? `Local source snapshot is truncated at ${input.localSourcesMeta.maxSources} files. ${input.localSourcesMeta.warning ?? "Use glob with a pattern for exact discovery."}` : null,
@@ -81077,7 +81126,7 @@ function buildDesktopContextSection(input) {
81077
81126
  availableReadTools.length > 0 ? `Read tools: ${availableReadTools.join(", ")}.` : "Read tools: none exposed for this turn.",
81078
81127
  availableWriteTools.length > 0 ? `Write/command tools: ${availableWriteTools.join(", ")}. These are governed by the selected permission mode and command policy.` : "Write/command tools: none exposed for this turn."
81079
81128
  ].filter(Boolean).join("\n"),
81080
- reason: "Local workspace access, optional folder scope, and tool availability for this turn.",
81129
+ reason: "Local workspace access, optional workspace folder, and tool availability for this turn.",
81081
81130
  sourcePath: input.activeRootPath,
81082
81131
  metadata: {
81083
81132
  desktopConnected: true,
@@ -85234,7 +85283,7 @@ function buildProjectMemorySections(meta) {
85234
85283
  lane: "project_memory",
85235
85284
  label: "PERCH.md",
85236
85285
  content: ["## PERCH.md", meta.perchMd.content].join("\n"),
85237
- reason: "Project operator memory loaded from the selected local folder scope.",
85286
+ reason: "Project operator memory loaded from the selected workspace folder.",
85238
85287
  sourcePath: meta.perchMd.relativePath,
85239
85288
  metadata: {
85240
85289
  relativePath: meta.perchMd.relativePath,
@@ -85271,7 +85320,7 @@ function buildProjectMemorySections(meta) {
85271
85320
  lane: "project_memory",
85272
85321
  label: `Rule: ${rule.fileName}`,
85273
85322
  content: [`## Project rule: ${rule.fileName}`, rule.content].join("\n"),
85274
- reason: "Rules-style project instruction loaded from the selected local folder scope.",
85323
+ reason: "Rules-style project instruction loaded from the selected workspace folder.",
85275
85324
  sourcePath: rule.relativePath,
85276
85325
  metadata: {
85277
85326
  relativePath: rule.relativePath,
@@ -85301,7 +85350,7 @@ function buildProjectMemorySections(meta) {
85301
85350
  content: [`## Project memory: ${memory.fileName}`, memory.content].join(
85302
85351
  "\n"
85303
85352
  ),
85304
- reason: "Typed project memory loaded from the selected local folder scope.",
85353
+ reason: "Typed project memory loaded from the selected workspace folder.",
85305
85354
  sourcePath: memory.relativePath,
85306
85355
  metadata: {
85307
85356
  relativePath: memory.relativePath,
@@ -85412,7 +85461,7 @@ Tool strengths:
85412
85461
  - For PO overages, compare each invoice/payment against the specific PO number referenced by that invoice, not only vendor-level PO coverage. If duplicate PO rows share a PO number, compare against each candidate approved amount and flag any row-level overage or ambiguous approval.
85413
85462
  - Model-written code should emit useful output first. The sandbox workspace has stdlib Python, input_manifest.json, sandbox_runtime.json, and perch_helpers. pandas/pdfplumber may not be installed, so use stdlib CSV parsing and perch_helpers.extract_invoice_fields() for invoice PDFs unless sandbox_runtime.json says package installs are enabled and an install is truly needed. Host paths like /Users/... are not readable inside the sandbox unless passed as sources and copied under input/. If output/report.json is present, it can be used as structured evidence; if not, use stdout/stderr and produced files honestly. If the sandbox run errors, say that plainly and iterate.
85414
85463
  - Cite sources for every reported figure. If a figure can't be traced to a document, mark it [UNSOURCED] rather than omitting it
85415
- - For a Mac folder: use glob and grep with the path; use listLocalSources/readLocalSourceFile only when a folder scope is already selected
85464
+ - For a Mac folder: use glob and grep with the path; use listLocalSources/readLocalSourceFile only when a workspace folder is already selected
85416
85465
  - Do not run a tool just because a keyword matches; reason from the user's current request and the existing thread state
85417
85466
  - Do not redo an audit, document, or email draft that is already done unless the user asks for a revision
85418
85467
  - Summarize tool outputs; don't stream raw JSON to the user
@@ -85426,9 +85475,9 @@ var QUILL_SPECIALIST_AGENTS_PROMPT;
85426
85475
  var init_quillSpecialistPrompt = __esm({
85427
85476
  "features/perchTerminal/agentPlatform/quillSpecialistPrompt.ts"() {
85428
85477
  QUILL_SPECIALIST_AGENTS_PROMPT = `
85429
- ## Quill \u2014 Specialist Delegation
85478
+ ## Writing & Research \u2014 Specialist Delegation
85430
85479
 
85431
- You are Quill, the writing persona. You do not run playbooks or suites. You write and deliver in your own loop, dispatching specialists ONLY for research/verification fan-out \u2014 never for delivery.
85480
+ This task is writing/research/legal shaped. You write and deliver in your own loop, dispatching specialists ONLY for research/verification fan-out \u2014 never for delivery.
85432
85481
 
85433
85482
  Use specialists (dispatch_agent) when independent research or verification benefits from separation:
85434
85483
  - general_writer: drafts polished letters, memos, essays, emails, and revisions from the user's request plus provided context. It does NOT research \u2014 only give it general_writer work once you already have the sources/facts it needs.
@@ -85466,7 +85515,6 @@ Success rules:
85466
85515
  - Do not say a Google Doc was created or an email was sent unless you have a verified receipt (URL / sent confirmation) or a screenshot showing the result.
85467
85516
  - 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".
85468
85517
  - If delivery hits a problem, recover in the same loop. Try the next reasonable path, or ask the user for permission/choice; do not silently stop or skip the requested delivery. Never claim success without proof.
85469
- - If the user asks for finance, AP, payroll, KYC, market, meeting, or close work, suggest Saffron unless the task is purely writing the prose.
85470
85518
  `.trim();
85471
85519
  }
85472
85520
  });
@@ -91867,6 +91915,7 @@ function buildCoreSystemSection(input) {
91867
91915
  ].join("\n")
91868
91916
  );
91869
91917
  }
91918
+ lines.push("", persona.styleContract);
91870
91919
  return lines.join("\n");
91871
91920
  }
91872
91921
  function appendAgentsModeGuidance(input) {
@@ -91899,9 +91948,8 @@ function appendAgentsModeGuidance(input) {
91899
91948
  });
91900
91949
  return;
91901
91950
  }
91902
- const isQuill = personaId === "quill";
91903
91951
  lines.push(
91904
- isQuill ? "You're in Quill mode. Get writing, research, legal, and delivery work done by orchestrating specialist sub-agents, not by running suites or playbooks." : "You're in Agents mode. Get the work done.",
91952
+ "You're in Agents mode. Get the work done.",
91905
91953
  "Execute directly when the task is clear and bounded.",
91906
91954
  "For greetings, thanks, quick check-ins, or explanation-only questions, answer directly without tools unless the user asks you to inspect, search, run, create, send, or change something.",
91907
91955
  "For delivery, write/send/change, filesystem, browser, or other external action turns, call the appropriate tool in the same turn and never end with only a promise, preamble, apology, or status line. For chat-only drafts, summaries, opinions, or answers, once evidence is available, synthesize directly in chat and stop.",
@@ -91921,9 +91969,10 @@ function appendAgentsModeGuidance(input) {
91921
91969
  if (approvedPlanBlock) {
91922
91970
  lines.push("", approvedPlanBlock);
91923
91971
  }
91924
- if (isQuill) {
91972
+ if (personaId === "quill" && isWritingResearchIntent(assemblyInput.trimmedInput)) {
91925
91973
  lines.push("", QUILL_SPECIALIST_AGENTS_PROMPT);
91926
- } else if (assemblyInput.coordinatorMode || isFinancialOperatorIntent(assemblyInput.trimmedInput)) {
91974
+ }
91975
+ if (assemblyInput.coordinatorMode || isFinancialOperatorIntent(assemblyInput.trimmedInput)) {
91927
91976
  lines.push("", FINANCIAL_OPERATOR_AGENTS_PROMPT);
91928
91977
  }
91929
91978
  lines.push("", PLATFORM_DELIVERY_GUIDANCE);
@@ -92036,6 +92085,11 @@ function isFinancialOperatorIntent(input) {
92036
92085
  input
92037
92086
  );
92038
92087
  }
92088
+ function isWritingResearchIntent(input) {
92089
+ return /\b(writ(?:e|ing|ten)|draft|redraft|rewrite|revise|edit|proofread|polish|memo(?:randum)?|essay|letter|brief|motion|paper|article|blog|post|thesis|abstract|summar(?:y|ize|ise)|cit(?:e|ation|ations)|research|sources?|irac|case[-\s]?law|statute|regulation|legal|law\s+review)\b/i.test(
92090
+ input
92091
+ );
92092
+ }
92039
92093
  function buildApprovedGeneralPlanBlock(session) {
92040
92094
  const approvedPlan = session?.approvedGeneralPlan;
92041
92095
  if (!approvedPlan) return null;
@@ -92427,7 +92481,7 @@ function buildContextSummary(input, rows) {
92427
92481
  `mode=${input.chatMode}`,
92428
92482
  `messages=${input.recentMessages.length}`,
92429
92483
  `source=${input.selectedSourceId ?? "none"}`,
92430
- `desktop=${input.desktopConnected ? "connected" : input.cliLocalTools === true ? "cli-local" : "browser"}`,
92484
+ `desktop=${input.desktopConnected ? "connected" : input.cliLocalTools === true ? "terminal" : "browser"}`,
92431
92485
  `sent=${sentCount}`,
92432
92486
  `compacted=${compactedCount}`,
92433
92487
  `not_found=${notFoundCount}`,
@@ -92786,9 +92840,15 @@ function createEmbeddingProviderFromEnv(env4 = readEnv()) {
92786
92840
  };
92787
92841
  }
92788
92842
  function createBrowserEmbeddingProvider(route = "/api/perch-terminal/embeddings") {
92843
+ let resolvedModel = "server-configured";
92844
+ let resolvedDimensions = DEFAULT_RETRIEVAL_EMBEDDING_DIMENSIONS;
92789
92845
  return {
92790
- model: "server-configured",
92791
- dimensions: DEFAULT_RETRIEVAL_EMBEDDING_DIMENSIONS,
92846
+ get model() {
92847
+ return resolvedModel;
92848
+ },
92849
+ get dimensions() {
92850
+ return resolvedDimensions;
92851
+ },
92792
92852
  async embed(text) {
92793
92853
  return (await this.embedBatch([text]))[0];
92794
92854
  },
@@ -92811,6 +92871,12 @@ function createBrowserEmbeddingProvider(route = "/api/perch-terminal/embeddings"
92811
92871
  if (!Array.isArray(payload.embeddings)) {
92812
92872
  throw new EmbeddingProviderError("Embedding route returned no embeddings.");
92813
92873
  }
92874
+ if (typeof payload.model === "string" && payload.model.trim()) {
92875
+ resolvedModel = payload.model;
92876
+ }
92877
+ if (typeof payload.dimensions === "number" && Number.isFinite(payload.dimensions)) {
92878
+ resolvedDimensions = payload.dimensions;
92879
+ }
92814
92880
  return payload.embeddings;
92815
92881
  }
92816
92882
  };
@@ -92885,7 +92951,7 @@ var init_embeddingProvider = __esm({
92885
92951
 
92886
92952
  // features/perchTerminal/persistence/permanentMemoryPersistence.ts
92887
92953
  function createPermanentMemoryPersistence(supabase, options = {}) {
92888
- const embeddingProvider = options.embeddingProvider ?? createBrowserEmbeddingProvider();
92954
+ const embeddingProvider = options.embeddingProvider ?? createDefaultPermanentMemoryEmbeddingProvider();
92889
92955
  const selectColumns = "id, user_id, workspace_id, project_id, scope, type, title, description, body, why, how_to_apply, source_kind, source_thread_id, source_message_id, source_run_id, confidence, created_at, updated_at, last_confirmed_at, stale_after_days, archived_at, tags, metadata";
92890
92956
  const api2 = {
92891
92957
  async listPermanentMemories({
@@ -93145,6 +93211,21 @@ function createPermanentMemoryPersistence(supabase, options = {}) {
93145
93211
  };
93146
93212
  return api2;
93147
93213
  }
93214
+ function createDefaultPermanentMemoryEmbeddingProvider() {
93215
+ if (typeof window !== "undefined") return createBrowserEmbeddingProvider();
93216
+ const status = getEmbeddingProviderStatus();
93217
+ let provider = null;
93218
+ const load = () => {
93219
+ provider ??= createEmbeddingProviderFromEnv();
93220
+ return provider;
93221
+ };
93222
+ return {
93223
+ model: status.configured ? status.model : DEFAULT_RETRIEVAL_EMBEDDING_MODEL,
93224
+ dimensions: status.configured ? status.dimensions : DEFAULT_RETRIEVAL_EMBEDDING_DIMENSIONS,
93225
+ embed: async (text) => load().embed(text),
93226
+ embedBatch: async (texts) => load().embedBatch(texts)
93227
+ };
93228
+ }
93148
93229
  async function findSimilarPermanentMemoryForInput(input) {
93149
93230
  const queryEmbedding = await input.embeddingProvider.embed(canonicalMemoryEmbeddingText(input.input));
93150
93231
  const { data, error } = await input.supabase.rpc("perch_ai_match_permanent_memories", {
@@ -117025,7 +117106,7 @@ function createSourcePersistence(supabase) {
117025
117106
  return count ?? 0;
117026
117107
  },
117027
117108
  async countEmbeddedRetrievalChunks(workspaceId) {
117028
- const { count, error } = await supabase.from("perch_ai_retrieval_chunks").select("*", { count: "exact", head: true }).eq("workspace_id", workspaceId).eq("is_stale", false).not("embedding", "is", null);
117109
+ const { count, error } = await supabase.from("perch_ai_retrieval_chunks").select("*", { count: "exact", head: true }).eq("workspace_id", workspaceId).eq("is_stale", false).not("embedding", "is", null).not("embedding_model", "is", null).not("embedded_at", "is", null);
117029
117110
  if (error) throw error;
117030
117111
  return count ?? 0;
117031
117112
  },
@@ -117131,18 +117212,30 @@ function createSourcePersistence(supabase) {
117131
117212
  return data ?? [];
117132
117213
  },
117133
117214
  async listIndexedSourceIds(workspaceId) {
117134
- const [chunks, retrieval, memory] = await Promise.all([
117215
+ const [chunks, retrieval, embeddedRetrieval, memory] = await Promise.all([
117135
117216
  supabase.from("perch_ai_source_chunks").select("source_id").eq("workspace_id", workspaceId),
117136
117217
  supabase.from("perch_ai_retrieval_chunks").select("source_id").eq("workspace_id", workspaceId).eq("is_stale", false),
117218
+ supabase.from("perch_ai_retrieval_chunks").select("source_id").eq("workspace_id", workspaceId).eq("is_stale", false).not("embedding", "is", null).not("embedding_model", "is", null).not("embedded_at", "is", null),
117137
117219
  supabase.from("perch_ai_source_memory").select("source_id").eq("workspace_id", workspaceId)
117138
117220
  ]);
117139
117221
  if (chunks.error) throw chunks.error;
117140
117222
  if (retrieval.error) throw retrieval.error;
117223
+ if (embeddedRetrieval.error) throw embeddedRetrieval.error;
117141
117224
  if (memory.error) throw memory.error;
117225
+ const sourceChunkSourceIds = sourceIdsFromRows(chunks.data);
117226
+ const retrievalChunkSourceIds = sourceIdsFromRows(retrieval.data);
117227
+ const embeddedRetrievalSourceIds = sourceIdsFromRows(embeddedRetrieval.data);
117142
117228
  return {
117143
- sourceChunkIds: new Set((chunks.data ?? []).map((r) => r.source_id)),
117144
- retrievalChunkIds: new Set((retrieval.data ?? []).map((r) => r.source_id)),
117145
- memorySourceIds: new Set((memory.data ?? []).map((r) => r.source_id))
117229
+ sourceChunkIds: new Set(sourceChunkSourceIds),
117230
+ retrievalChunkIds: new Set(retrievalChunkSourceIds),
117231
+ embeddedRetrievalChunkIds: new Set(embeddedRetrievalSourceIds),
117232
+ memorySourceIds: new Set((memory.data ?? []).map((r) => r.source_id)),
117233
+ sourceChunkCountsBySourceId: countBySourceId(sourceChunkSourceIds),
117234
+ retrievalChunkCountsBySourceId: countBySourceId(retrievalChunkSourceIds),
117235
+ embeddedRetrievalChunkCountsBySourceId: countBySourceId(embeddedRetrievalSourceIds),
117236
+ totalSourceChunks: sourceChunkSourceIds.length,
117237
+ totalRetrievalChunks: retrievalChunkSourceIds.length,
117238
+ totalEmbeddedRetrievalChunks: embeddedRetrievalSourceIds.length
117146
117239
  };
117147
117240
  },
117148
117241
  async keywordSearchSourcesMetadata(workspaceId, query, limit) {
@@ -117198,6 +117291,16 @@ function createSourcePersistence(supabase) {
117198
117291
  function escapeIlike2(value) {
117199
117292
  return value.replace(/[%_\\]/g, (ch) => `\\${ch}`);
117200
117293
  }
117294
+ function sourceIdsFromRows(rows) {
117295
+ return (rows ?? []).map((row) => row.source_id).filter((sourceId) => typeof sourceId === "string" && sourceId.trim().length > 0);
117296
+ }
117297
+ function countBySourceId(sourceIds) {
117298
+ const counts = /* @__PURE__ */ new Map();
117299
+ for (const sourceId of sourceIds) {
117300
+ counts.set(sourceId, (counts.get(sourceId) ?? 0) + 1);
117301
+ }
117302
+ return counts;
117303
+ }
117201
117304
  var SOURCE_LIST_COLUMNS;
117202
117305
  var init_sourcePersistence = __esm({
117203
117306
  "features/perchTerminal/persistence/sourcePersistence.ts"() {
@@ -117875,21 +117978,22 @@ function getSourceRetrievalToolDefinitions() {
117875
117978
  }
117876
117979
  ];
117877
117980
  }
117878
- async function dispatchSourceRetrievalTool(toolName, args, session) {
117879
- if (!isSupabaseConfigured()) {
117880
- return sessionError("Supabase is not configured for this environment.");
117981
+ async function dispatchSourceRetrievalTool(toolName, args, session, options = {}) {
117982
+ if (!options.supabase && !isSupabaseConfigured()) {
117983
+ return sessionError("Workspace source search is unavailable in this session.");
117881
117984
  }
117882
117985
  if (!session.workspaceId) {
117883
- return sessionError("workspace_id is required from the authenticated session.");
117986
+ return sessionError("Sign in to a workspace before using workspace source search.");
117884
117987
  }
117885
117988
  let supabase;
117886
117989
  try {
117887
- supabase = createClient();
117990
+ supabase = options.supabase ?? createClient();
117888
117991
  } catch (err) {
117889
117992
  const message = err instanceof Error ? err.message : String(err);
117890
117993
  return sessionError(message);
117891
117994
  }
117892
117995
  const store = createSourcePersistence(supabase);
117996
+ const retrievalStore = createRetrievalPersistence(supabase);
117893
117997
  const workspaceId = session.workspaceId;
117894
117998
  switch (toolName) {
117895
117999
  case TOOL_NAMES.listSources:
@@ -117905,9 +118009,9 @@ async function dispatchSourceRetrievalTool(toolName, args, session) {
117905
118009
  case TOOL_NAMES.resolveSourceCandidates:
117906
118010
  return resolveSourceCandidatesHandler(store, workspaceId, session, args);
117907
118011
  case TOOL_NAMES.retrieveContext:
117908
- return retrieveContextHandler(store, workspaceId, session, args);
118012
+ return retrieveContextHandler(store, retrievalStore, workspaceId, session, args);
117909
118013
  case TOOL_NAMES.semanticSearch:
117910
- return semanticSearchHandler(store, workspaceId, args);
118014
+ return semanticSearchHandler(store, retrievalStore, workspaceId, args);
117911
118015
  case TOOL_NAMES.diagnoseWorkspaceAccess:
117912
118016
  return diagnoseWorkspaceAccessHandler(store, workspaceId, session);
117913
118017
  default:
@@ -118170,7 +118274,7 @@ async function resolveSourceCandidatesHandler(store, workspaceId, session, args)
118170
118274
  schemaVersion: "perch-tool-result-v1"
118171
118275
  };
118172
118276
  }
118173
- async function retrieveContextHandler(store, workspaceId, session, args) {
118277
+ async function retrieveContextHandler(store, retrievalStore, workspaceId, session, args) {
118174
118278
  const query = stringArg(args.query)?.trim();
118175
118279
  if (!query) {
118176
118280
  return { ok: false, error: "query is required", toolName: TOOL_NAMES.retrieveContext };
@@ -118196,7 +118300,6 @@ async function retrieveContextHandler(store, workspaceId, session, args) {
118196
118300
  let searchMode = "keyword_only";
118197
118301
  let fallbackUsed = false;
118198
118302
  let semanticError = null;
118199
- const retrievalStore = createRetrievalPersistence(createClient());
118200
118303
  const embeddedCount = await store.countEmbeddedRetrievalChunks(workspaceId).catch(() => 0);
118201
118304
  retrievalAttempts.push("semantic_first");
118202
118305
  try {
@@ -118394,7 +118497,7 @@ async function retrieveContextHandler(store, workspaceId, session, args) {
118394
118497
  schemaVersion: "perch-tool-result-v1"
118395
118498
  };
118396
118499
  }
118397
- async function semanticSearchHandler(store, workspaceId, args) {
118500
+ async function semanticSearchHandler(store, retrievalStore, workspaceId, args) {
118398
118501
  const query = stringArg(args.query)?.trim();
118399
118502
  if (!query) {
118400
118503
  return { ok: false, error: "query is required", toolName: TOOL_NAMES.semanticSearch };
@@ -118403,7 +118506,7 @@ async function semanticSearchHandler(store, workspaceId, args) {
118403
118506
  const sourceIds = Array.isArray(args.sourceIds) ? args.sourceIds.map(String) : void 0;
118404
118507
  const includeStale = args.includeStale === true;
118405
118508
  const embeddedCount = await store.countEmbeddedRetrievalChunks(workspaceId);
118406
- const result2 = await createRetrievalPersistence(createClient()).matchRetrievalChunks({
118509
+ const result2 = await retrievalStore.matchRetrievalChunks({
118407
118510
  workspaceId,
118408
118511
  query,
118409
118512
  limit,
@@ -118448,7 +118551,7 @@ async function semanticSearchHandler(store, workspaceId, args) {
118448
118551
  return {
118449
118552
  rankSignals: signals,
118450
118553
  reason: result2.searchMode === "hybrid" ? "hybrid_sparse_vector_rrf" : result2.searchMode === "sparse" ? "postgres_full_text_rank" : "semantic_similarity",
118451
- scoreExplanation: result2.searchMode === "hybrid" ? `hybrid RRF rank${typeof signals.semanticRank === "number" ? `; semantic rank ${signals.semanticRank}` : ""}${typeof signals.sparseRank === "number" ? `; sparse rank ${signals.sparseRank}` : ""}` : result2.searchMode === "sparse" ? "Postgres full-text rank; semantic embeddings unavailable or not needed" : `cosine similarity ${hit.similarity.toFixed(3)} from ${hit.embedding_model ?? "indexed embedding"}`
118554
+ scoreExplanation: result2.searchMode === "hybrid" ? `hybrid rank${typeof signals.semanticRank === "number" ? `; vector rank ${signals.semanticRank}` : ""}${typeof signals.sparseRank === "number" ? `; text rank ${signals.sparseRank}` : ""}` : result2.searchMode === "sparse" ? "Workspace text-search rank; vector retrieval unavailable for this result" : `cosine similarity ${hit.similarity.toFixed(3)} from ${hit.embedding_model ?? "indexed embedding"}`
118452
118555
  };
118453
118556
  })(),
118454
118557
  rank: index + 1,
@@ -118488,7 +118591,7 @@ async function semanticSearchHandler(store, workspaceId, args) {
118488
118591
  semanticAvailable: result2.embeddingProviderConfigured,
118489
118592
  sparseAvailable: result2.sparseProviderConfigured,
118490
118593
  fallbackUsed: result2.fallbackUsed,
118491
- ranking: result2.searchMode === "hybrid" ? "reciprocal rank fusion over semantic vector and Postgres full-text ranks" : result2.searchMode === "sparse" ? "Postgres full-text rank; vector embeddings not required" : "semantic cosine similarity from perch_ai_match_retrieval_chunks"
118594
+ ranking: result2.searchMode === "hybrid" ? "combined vector and text-search ranks" : result2.searchMode === "sparse" ? "workspace text-search rank; vector retrieval not required" : "vector similarity over indexed source chunks"
118492
118595
  },
118493
118596
  schemaVersion: "perch-tool-result-v1"
118494
118597
  };
@@ -118497,7 +118600,7 @@ async function diagnoseWorkspaceAccessHandler(store, workspaceId, session) {
118497
118600
  const workspace = await store.getWorkspaceRow(workspaceId);
118498
118601
  const blockers = [];
118499
118602
  if (!workspace) {
118500
- blockers.push("Workspace row not found or not accessible under RLS.");
118603
+ blockers.push("Workspace is not available to this signed-in session.");
118501
118604
  }
118502
118605
  let sourcesCount = 0;
118503
118606
  let sourceChunksCount = 0;
@@ -133500,6 +133603,8 @@ var init_toolPermissionPolicy = __esm({
133500
133603
  TOOL_NAMES.deleteLocalFile,
133501
133604
  TOOL_NAMES.editLocalFile,
133502
133605
  TOOL_NAMES.statPath,
133606
+ TOOL_NAMES.readProjectMemory,
133607
+ TOOL_NAMES.saveToMemory,
133503
133608
  TOOL_NAMES.listLocalSources,
133504
133609
  TOOL_NAMES.readLocalSourceFile,
133505
133610
  TOOL_NAMES.generateAPAuditPacket,
@@ -135197,7 +135302,7 @@ function getDesktopToolDefinitions() {
135197
135302
  type: "function",
135198
135303
  function: {
135199
135304
  name: TOOL_NAMES.readProjectMemory,
135200
- description: "Read project memory metadata for the active folder scope.",
135305
+ description: "Read local project memory for the active workspace folder when .perch memory is present. Signed-in durable memory is admitted automatically at turn start.",
135201
135306
  parameters: {
135202
135307
  type: "object",
135203
135308
  properties: {},
@@ -135209,7 +135314,7 @@ function getDesktopToolDefinitions() {
135209
135314
  type: "function",
135210
135315
  function: {
135211
135316
  name: TOOL_NAMES.saveToMemory,
135212
- description: "Save something to a memory file in this project (.perch/memory/). Use when the user asks you to remember something or when you learn a durable fact about the project worth keeping across sessions. Prefer mode='merge' with a sectionHeading so existing memory isn't overwritten.",
135317
+ description: "Save durable memory when the user asks you to remember something or when you learn a stable user/project preference. In signed-in CLI sessions this writes server memory; in a .perch project folder it can also write local project memory. Prefer mode='merge' with a sectionHeading for local project memory.",
135213
135318
  parameters: {
135214
135319
  type: "object",
135215
135320
  properties: {
@@ -135225,6 +135330,15 @@ function getDesktopToolDefinitions() {
135225
135330
  sectionHeading: {
135226
135331
  type: "string",
135227
135332
  description: "Required for merge mode \u2014 markdown heading to upsert under."
135333
+ },
135334
+ scope: {
135335
+ type: "string",
135336
+ enum: ["private_user", "workspace", "project"],
135337
+ description: "Optional durable-memory scope for signed-in server memory. Defaults from fileName."
135338
+ },
135339
+ title: {
135340
+ type: "string",
135341
+ description: "Optional short title for signed-in durable memory."
135228
135342
  }
135229
135343
  },
135230
135344
  required: ["fileName", "mode", "content"],
@@ -135280,7 +135394,7 @@ function getDesktopToolDefinitions() {
135280
135394
  type: "function",
135281
135395
  function: {
135282
135396
  name: TOOL_NAMES.getProjectRules,
135283
- description: "Read PERCH.md and small .perch/rules/*.md or *.txt project rule files for the active folder scope.",
135397
+ description: "Read PERCH.md and small .perch/rules/*.md or *.txt project rule files for the active workspace folder.",
135284
135398
  parameters: {
135285
135399
  type: "object",
135286
135400
  properties: {},
@@ -135298,7 +135412,7 @@ function getDesktopToolDefinitions() {
135298
135412
  properties: {
135299
135413
  path: {
135300
135414
  type: "string",
135301
- description: "Absolute directory path to list (e.g. /Users/you/Desktop). Works with no folder scope selected."
135415
+ description: "Absolute directory path to list (e.g. /Users/you/Desktop). Works with no workspace folder selected."
135302
135416
  },
135303
135417
  query: {
135304
135418
  type: "string",
@@ -136486,7 +136600,7 @@ function getNativeToolDefinitions() {
136486
136600
  type: "function",
136487
136601
  function: {
136488
136602
  name: TOOL_NAMES.ctxInspect,
136489
- description: "Inspect current execution context: desktop connection status, selected folder scope, permission mode, tool counts, and available tools.",
136603
+ description: "Inspect current execution context: desktop connection status, selected workspace folder, permission mode, tool counts, and available tools.",
136490
136604
  parameters: {
136491
136605
  type: "object",
136492
136606
  properties: {},
@@ -136520,7 +136634,7 @@ function getNativeToolDefinitions() {
136520
136634
  type: "function",
136521
136635
  function: {
136522
136636
  name: TOOL_NAMES.configInspect,
136523
- description: "Inspect the Perch Terminal configuration: local workspace access, optional folder scope, permission mode, and capabilities.",
136637
+ description: "Inspect the Perch Terminal configuration: local workspace access, optional workspace folder, permission mode, and capabilities.",
136524
136638
  parameters: {
136525
136639
  type: "object",
136526
136640
  properties: {},
@@ -199835,6 +199949,7 @@ function containsBrowserDeliveryTask(tasks) {
199835
199949
  var BROWSER_DELIVERY_ROLE_IDS;
199836
199950
  var init_browserDeliveryLock = __esm({
199837
199951
  "features/perchTerminal/agentPlatform/browserDeliveryLock.ts"() {
199952
+ "use strict";
199838
199953
  BROWSER_DELIVERY_ROLE_IDS = /* @__PURE__ */ new Set([
199839
199954
  "doc_writer",
199840
199955
  "email_sender",
@@ -202460,8 +202575,6 @@ async function dispatchAgentHandler(args, ctx) {
202460
202575
  parentToolCallId: ctx.parentToolCallId,
202461
202576
  mcpTools: ctx.mcpTools ?? []
202462
202577
  };
202463
- const isQuillNormalTurn = ctx.personaId === "quill" && ctx.chatMode !== "coordinator" && !ctx.allowedCallableAgents?.length;
202464
- void isQuillNormalTurn;
202465
202578
  if (Array.isArray(args.tasks) && args.tasks.length > 0) {
202466
202579
  const tasks = args.tasks.map(
202467
202580
  (t) => ({
@@ -206113,7 +206226,7 @@ async function requireMemoryRootId(ctx, surface) {
206113
206226
  if (!rootId) {
206114
206227
  return {
206115
206228
  ok: false,
206116
- error: `No selected folder scope is available for ${surface}.`
206229
+ error: `No selected workspace folder is available for ${surface}.`
206117
206230
  };
206118
206231
  }
206119
206232
  return { ok: true, rootId };
@@ -206138,14 +206251,86 @@ var init_readProjectMemory = __esm({
206138
206251
  classification: { native: false },
206139
206252
  handler: async (_args, ctx) => {
206140
206253
  const root2 = await requireMemoryRootId(ctx, "project memory");
206141
- if (!root2.ok) return root2;
206142
- return readProjectMemory(root2.rootId);
206254
+ if (!root2.ok) {
206255
+ return ctx.cliServerAppUrl?.trim() && ctx.cliServerAccessToken?.trim() ? {
206256
+ ok: true,
206257
+ backend: "server_memory",
206258
+ message: "Signed-in durable memory is available. Relevant memories are added automatically at the start of each CLI turn."
206259
+ } : {
206260
+ ok: false,
206261
+ errorCode: "memory_unavailable",
206262
+ error: "Project memory is unavailable in this CLI session. Run from a project folder with .perch memory or sign in for durable memory."
206263
+ };
206264
+ }
206265
+ const result2 = await readProjectMemory(root2.rootId);
206266
+ if (!result2.ok && ctx.cliServerAppUrl?.trim() && ctx.cliServerAccessToken?.trim()) {
206267
+ return {
206268
+ ok: true,
206269
+ backend: "server_memory",
206270
+ localProjectMemory: result2,
206271
+ message: "Signed-in durable memory is available. Relevant memories are added automatically at the start of each CLI turn."
206272
+ };
206273
+ }
206274
+ return result2;
206143
206275
  }
206144
206276
  };
206145
206277
  }
206146
206278
  });
206147
206279
 
206148
206280
  // features/perchTerminal/runtime/toolSystem/tools/projectMemory/saveToMemory.ts
206281
+ function hasCliServerMemory(ctx) {
206282
+ return Boolean(
206283
+ ctx.cliServerAppUrl?.trim() && ctx.cliServerAccessToken?.trim()
206284
+ );
206285
+ }
206286
+ async function saveMemoryThroughCliServer(args, ctx) {
206287
+ const appUrl = ctx.cliServerAppUrl?.trim();
206288
+ const accessToken = ctx.cliServerAccessToken?.trim();
206289
+ if (!appUrl || !accessToken) return memoryUnavailable();
206290
+ try {
206291
+ const response = await fetch(`${appUrl.replace(/\/+$/, "")}/api/perch-terminal/cli-memory`, {
206292
+ method: "POST",
206293
+ headers: {
206294
+ Accept: "application/json",
206295
+ "Content-Type": "application/json",
206296
+ Authorization: `Bearer ${accessToken}`
206297
+ },
206298
+ body: JSON.stringify({
206299
+ action: "save",
206300
+ args,
206301
+ threadId: ctx.threadId ?? null,
206302
+ runId: ctx.runId ?? null,
206303
+ workspaceRoot: ctx.activeRootPath ?? ctx.workspaceRoot ?? null
206304
+ }),
206305
+ signal: ctx.signal
206306
+ });
206307
+ const payload = await response.json().catch(() => ({}));
206308
+ if (!response.ok) {
206309
+ return {
206310
+ ok: false,
206311
+ backend: "server_memory",
206312
+ errorCode: typeof payload.errorCode === "string" ? payload.errorCode : "memory_save_unavailable",
206313
+ error: typeof payload.message === "string" ? payload.message : "Durable memory is unavailable right now. Try again shortly or update Perch."
206314
+ };
206315
+ }
206316
+ return payload;
206317
+ } catch (error) {
206318
+ const aborted = error instanceof DOMException && error.name === "AbortError";
206319
+ return {
206320
+ ok: false,
206321
+ backend: "server_memory",
206322
+ errorCode: aborted ? "memory_save_cancelled" : "memory_save_unavailable",
206323
+ error: aborted ? "Memory save was stopped." : "Durable memory is unavailable right now. Try again shortly or update Perch."
206324
+ };
206325
+ }
206326
+ }
206327
+ function memoryUnavailable() {
206328
+ return {
206329
+ ok: false,
206330
+ errorCode: "memory_unavailable",
206331
+ error: "Memory is unavailable in this CLI session. Sign in with `perch login` or run from a project folder with .perch memory."
206332
+ };
206333
+ }
206149
206334
  var saveToMemoryTool;
206150
206335
  var init_saveToMemory = __esm({
206151
206336
  "features/perchTerminal/runtime/toolSystem/tools/projectMemory/saveToMemory.ts"() {
@@ -206157,14 +206342,24 @@ var init_saveToMemory = __esm({
206157
206342
  name: TOOL_NAMES.saveToMemory,
206158
206343
  classification: { native: false },
206159
206344
  handler: async (args, ctx) => {
206345
+ const serverResult = hasCliServerMemory(ctx) ? await saveMemoryThroughCliServer(args, ctx) : null;
206346
+ if (serverResult?.ok) return serverResult;
206160
206347
  const root2 = await requireMemoryRootId(ctx, "project memory");
206161
- if (!root2.ok) return root2;
206162
- return writeMemoryFile(root2.rootId, {
206348
+ if (!root2.ok) return serverResult ?? memoryUnavailable();
206349
+ const localResult = await writeMemoryFile(root2.rootId, {
206163
206350
  fileName: String(args.fileName),
206164
206351
  mode: String(args.mode),
206165
206352
  content: String(args.content ?? ""),
206166
206353
  sectionHeading: typeof args.sectionHeading === "string" ? args.sectionHeading : void 0
206167
206354
  });
206355
+ if (localResult.ok === true) {
206356
+ return {
206357
+ ...localResult,
206358
+ backend: "local_project_memory",
206359
+ message: serverResult ? "Saved to local project memory. Signed-in durable memory was unavailable, so this save is scoped to the current project folder." : "Saved to local project memory."
206360
+ };
206361
+ }
206362
+ return serverResult ?? localResult ?? memoryUnavailable();
206168
206363
  }
206169
206364
  };
206170
206365
  }
@@ -207133,7 +207328,7 @@ var init_sendWorkerMessage2 = __esm({
207133
207328
  });
207134
207329
 
207135
207330
  // features/perchTerminal/runtime/toolSystem/tools/workers/spawnWorker.ts
207136
- var QUILL_BLOCKED_DELIVERY_WORKER_IDS, spawnWorkerTool;
207331
+ var spawnWorkerTool;
207137
207332
  var init_spawnWorker2 = __esm({
207138
207333
  "features/perchTerminal/runtime/toolSystem/tools/workers/spawnWorker.ts"() {
207139
207334
  "use strict";
@@ -207142,11 +207337,6 @@ var init_spawnWorker2 = __esm({
207142
207337
  init_agentDispatch();
207143
207338
  init_localScope();
207144
207339
  init_toolNames();
207145
- QUILL_BLOCKED_DELIVERY_WORKER_IDS = /* @__PURE__ */ new Set([
207146
- "doc_writer",
207147
- "email_sender",
207148
- "calendar_scheduler"
207149
- ]);
207150
207340
  spawnWorkerTool = {
207151
207341
  name: TOOL_NAMES.spawnWorker,
207152
207342
  classification: { native: false },
@@ -207164,8 +207354,6 @@ var init_spawnWorker2 = __esm({
207164
207354
  errorCode: "worker_event_sink_missing"
207165
207355
  };
207166
207356
  }
207167
- if (ctx.personaId === "quill" && ctx.chatMode !== "coordinator" && !ctx.allowedCallableAgents?.length && QUILL_BLOCKED_DELIVERY_WORKER_IDS.has(workerId)) {
207168
- }
207169
207357
  const enrichedContext = await threadPriorSpecialistContext({
207170
207358
  threadId: ctx.threadId,
207171
207359
  roleId: workerId,
@@ -213219,7 +213407,7 @@ var init_localSources = __esm({
213219
213407
  return {
213220
213408
  ok: true,
213221
213409
  sources: [],
213222
- warning: "No folder scope and no path given. To read or search a file, call readLocalFile / glob / grep / visionInspect with its absolute path (e.g. /Users/you/Desktop/file.png) \u2014 no folder selection is needed."
213410
+ warning: "Local workspace access is unavailable in this chat. Open Perch Desktop or attach a workspace folder, then try again."
213223
213411
  };
213224
213412
  }
213225
213413
  const maxResults = sanitizeListLocalSourcesMaxResults(args.maxResults);
@@ -216703,6 +216891,8 @@ async function executeToolCall(call, opts) {
216703
216891
  workspaceId: opts.workspaceId,
216704
216892
  threadId: opts.threadId,
216705
216893
  supabase: opts.supabase,
216894
+ cliServerAppUrl: opts.cliServerAppUrl,
216895
+ cliServerAccessToken: opts.cliServerAccessToken,
216706
216896
  marketDeskProxyAppUrl: opts.marketDeskProxyAppUrl,
216707
216897
  marketDeskProxyAccessToken: opts.marketDeskProxyAccessToken,
216708
216898
  chatMode: opts.chatMode ?? null,
@@ -216762,7 +216952,7 @@ async function executeToolCall(call, opts) {
216762
216952
  const result2 = {
216763
216953
  ok: false,
216764
216954
  errorCode: "supabase_session_required",
216765
- error: isSupabaseConfigured() ? "Source retrieval tools require an authenticated workspace session." : "Supabase is not configured for source retrieval tools.",
216955
+ error: isSupabaseConfigured() ? "Source retrieval tools require an authenticated workspace session." : "Workspace source search is unavailable in this session.",
216766
216956
  schemaVersion: "perch-tool-result-v1",
216767
216957
  toolName: call.name
216768
216958
  };
@@ -216843,6 +217033,8 @@ async function executeToolCall(call, opts) {
216843
217033
  workspaceRoot: opts.workspaceRoot,
216844
217034
  threadId: opts.threadId,
216845
217035
  runId: opts.runId,
217036
+ cliServerAppUrl: opts.cliServerAppUrl,
217037
+ cliServerAccessToken: opts.cliServerAccessToken,
216846
217038
  marketDeskProxyAppUrl: opts.marketDeskProxyAppUrl,
216847
217039
  marketDeskProxyAccessToken: opts.marketDeskProxyAccessToken,
216848
217040
  onEvent: runtime.onEvent,
@@ -219043,6 +219235,8 @@ async function runModelToolLoop(input) {
219043
219235
  selectedSourceId: input.selectedSourceId,
219044
219236
  supabaseConfigured: input.supabaseConfigured,
219045
219237
  supabase: input.supabase,
219238
+ cliServerAppUrl: input.cliServerAppUrl,
219239
+ cliServerAccessToken: input.cliServerAccessToken,
219046
219240
  marketDeskProxyAppUrl: input.marketDeskProxyAppUrl,
219047
219241
  marketDeskProxyAccessToken: input.marketDeskProxyAccessToken,
219048
219242
  runId: input.runId,
@@ -219459,6 +219653,8 @@ async function executeInitialToolCall(input) {
219459
219653
  selectedSourceId: input.input.selectedSourceId,
219460
219654
  supabaseConfigured: input.input.supabaseConfigured,
219461
219655
  supabase: input.input.supabase,
219656
+ cliServerAppUrl: input.input.cliServerAppUrl,
219657
+ cliServerAccessToken: input.input.cliServerAccessToken,
219462
219658
  marketDeskProxyAppUrl: input.input.marketDeskProxyAppUrl,
219463
219659
  marketDeskProxyAccessToken: input.input.marketDeskProxyAccessToken,
219464
219660
  runId: input.input.runId,
@@ -220320,52 +220516,15 @@ var init_toolLoop = __esm({
220320
220516
  }
220321
220517
  });
220322
220518
 
220323
- // features/perchTerminal/runtime/personas/quillToolPolicy.ts
220324
- function isQuillBlockedToolName(toolName) {
220325
- return QUILL_BLOCKED_TOOL_NAMES.has(toolName);
220326
- }
220327
- function filterToolsForQuill(toolDefinitions) {
220328
- return toolDefinitions.filter((tool) => !isQuillBlockedToolName(tool.function.name));
220329
- }
220330
- var QUILL_BLOCKED_TOOL_NAMES;
220331
- var init_quillToolPolicy = __esm({
220332
- "features/perchTerminal/runtime/personas/quillToolPolicy.ts"() {
220333
- "use strict";
220334
- init_toolNames();
220335
- QUILL_BLOCKED_TOOL_NAMES = /* @__PURE__ */ new Set([
220336
- TOOL_NAMES.runSuite,
220337
- TOOL_NAMES.runManagedPlaybook,
220338
- TOOL_NAMES.listSuiteCatalog,
220339
- TOOL_NAMES.proposeSuitePlan,
220340
- TOOL_NAMES.executeSuitePlan,
220341
- TOOL_NAMES.proposeWork,
220342
- TOOL_NAMES.executeWork,
220343
- TOOL_NAMES.generateAPAuditPacket,
220344
- TOOL_NAMES.safeBrowserAction,
220345
- // Deprecated non-verified shortcuts — superseded by the verified surface tools.
220346
- TOOL_NAMES.gmailSendEmail,
220347
- TOOL_NAMES.gmailSaveDraft,
220348
- TOOL_NAMES.googleDocsCreate,
220349
- TOOL_NAMES.googleDocsAppend,
220350
- TOOL_NAMES.googleCalendarCreateEvent,
220351
- TOOL_NAMES.googleSheetsCreate,
220352
- TOOL_NAMES.googleSheetsAppendRows
220353
- ]);
220354
- }
220355
- });
220356
-
220357
- // features/perchTerminal/runtime/personas/saffronToolPolicy.ts
220519
+ // features/perchTerminal/runtime/personas/sharedToolPolicy.ts
220358
220520
  function filterSuiteRelayTools(toolDefinitions, opts = {}) {
220359
220521
  if (opts.allowSuiteRelay) return toolDefinitions;
220360
220522
  return toolDefinitions.filter(
220361
220523
  (tool) => !SUITE_RELAY_TOOL_NAMES.has(tool.function.name)
220362
220524
  );
220363
220525
  }
220364
- function filterToolsForSaffron(toolDefinitions, opts = {}) {
220365
- return filterSuiteRelayTools(toolDefinitions, opts);
220366
- }
220367
- var init_saffronToolPolicy = __esm({
220368
- "features/perchTerminal/runtime/personas/saffronToolPolicy.ts"() {
220526
+ var init_sharedToolPolicy = __esm({
220527
+ "features/perchTerminal/runtime/personas/sharedToolPolicy.ts"() {
220369
220528
  "use strict";
220370
220529
  init_suiteRelayKillSwitch();
220371
220530
  }
@@ -220483,10 +220642,7 @@ async function runLiveAgentsLoop(input) {
220483
220642
  PLAN_MODE_ALLOWED_TOOL_NAMES
220484
220643
  ) : effectiveChatMode === "ask" ? [] : getReadOnlyToolDefinitions(toolOpts);
220485
220644
  const suiteRelayEnabled = isSuiteRelayEnabled();
220486
- const personaFilteredTools = turn.personaId === "quill" ? filterToolsForQuill(baseAgentsTools) : turn.personaId === "saffron" ? filterToolsForSaffron(baseAgentsTools, {
220487
- allowSuiteRelay: suiteRelayEnabled
220488
- }) : baseAgentsTools;
220489
- const suiteRelayFilteredTools = filterSuiteRelayTools(personaFilteredTools, {
220645
+ const suiteRelayFilteredTools = filterSuiteRelayTools(baseAgentsTools, {
220490
220646
  allowSuiteRelay: suiteRelayEnabled
220491
220647
  });
220492
220648
  const deliveryPolicyDeliveryOperatorOnly = turn.deliveryOperatorOnly === true;
@@ -220520,7 +220676,7 @@ async function runLiveAgentsLoop(input) {
220520
220676
  const onEv = onEvent ?? deps.onEvent;
220521
220677
  onEv({
220522
220678
  type: "activity_delta",
220523
- text: "Workspace preflight: no folder scope selected \u2014 continuing with Desktop tools.",
220679
+ text: "Workspace preflight: no workspace folder selected; continuing with Desktop tools.",
220524
220680
  ts: (/* @__PURE__ */ new Date()).toISOString()
220525
220681
  });
220526
220682
  }
@@ -220559,6 +220715,8 @@ async function runLiveAgentsLoop(input) {
220559
220715
  untrustedContextPresent: context.untrustedContextPresent,
220560
220716
  supabaseConfigured: turn.supabaseConfigured,
220561
220717
  supabase: turn.supabase,
220718
+ cliServerAppUrl: turn.cliServerAppUrl ?? null,
220719
+ cliServerAccessToken: turn.cliServerAccessToken ?? null,
220562
220720
  marketDeskProxyAppUrl: turn.marketDeskProxyAppUrl ?? null,
220563
220721
  marketDeskProxyAccessToken: turn.marketDeskProxyAccessToken ?? null,
220564
220722
  onEvent: onEvent ?? deps.onEvent,
@@ -220937,8 +221095,7 @@ var init_liveAgentsLoop = __esm({
220937
221095
  init_toolNames();
220938
221096
  init_planModeStateMachine();
220939
221097
  init_toolPermissionPolicy();
220940
- init_quillToolPolicy();
220941
- init_saffronToolPolicy();
221098
+ init_sharedToolPolicy();
220942
221099
  init_deliveryToolPolicy();
220943
221100
  init_threadSession();
220944
221101
  init_sandboxProvenance();
@@ -221925,7 +222082,12 @@ async function ensureLocalSourceIndexed(input) {
221925
222082
  contentHash: null,
221926
222083
  chunksBuilt: 0,
221927
222084
  chunksUpserted: 0,
221928
- embeddingConfigured: getEmbeddingProviderStatus().configured,
222085
+ sourceChunksUpserted: 0,
222086
+ retrievalChunksBuilt: 0,
222087
+ retrievalChunksUpserted: 0,
222088
+ embeddedChunksUpserted: 0,
222089
+ vectorReady: false,
222090
+ embeddingConfigured: isBrowserRuntime() ? true : getEmbeddingProviderStatus().configured,
221929
222091
  errors: [],
221930
222092
  warnings: []
221931
222093
  };
@@ -221958,7 +222120,12 @@ async function ensureLocalSourceIndexed(input) {
221958
222120
  contentHash,
221959
222121
  chunksBuilt: 0,
221960
222122
  chunksUpserted: 0,
221961
- embeddingConfigured: getEmbeddingProviderStatus().configured,
222123
+ sourceChunksUpserted: 0,
222124
+ retrievalChunksBuilt: 0,
222125
+ retrievalChunksUpserted: 0,
222126
+ embeddedChunksUpserted: 0,
222127
+ vectorReady: true,
222128
+ embeddingConfigured: isBrowserRuntime() ? true : getEmbeddingProviderStatus().configured,
221962
222129
  errors: [],
221963
222130
  warnings: []
221964
222131
  };
@@ -222056,12 +222223,15 @@ async function ensureLocalSourceIndexed(input) {
222056
222223
  threadId: input.threadId ?? null,
222057
222224
  chunks: sourceTextChunks
222058
222225
  });
222059
- const embeddingStatus = getEmbeddingProviderStatus();
222060
222226
  let chunksBuilt = textChunks.length;
222061
222227
  let chunksUpserted = 0;
222062
222228
  const warnings = [...extracted.truncated ? ["Source text was truncated before indexing."] : []];
222063
- if (!embeddingStatus.configured) {
222064
- warnings.push(embeddingStatus.message);
222229
+ let embeddingProvider;
222230
+ try {
222231
+ embeddingProvider = input.embeddingProvider ?? createDefaultLocalSourceEmbeddingProvider();
222232
+ } catch (error) {
222233
+ const message = error instanceof Error ? error.message : String(error);
222234
+ warnings.push(message);
222065
222235
  return {
222066
222236
  ok: true,
222067
222237
  status: "indexed",
@@ -222071,38 +222241,69 @@ async function ensureLocalSourceIndexed(input) {
222071
222241
  contentHash: indexedContentHash,
222072
222242
  chunksBuilt,
222073
222243
  chunksUpserted: 0,
222244
+ sourceChunksUpserted: sourceTextChunks.length,
222245
+ retrievalChunksBuilt: chunksBuilt,
222246
+ retrievalChunksUpserted: 0,
222247
+ embeddedChunksUpserted: 0,
222248
+ vectorReady: false,
222249
+ embeddingConfigured: false,
222250
+ errors: [],
222251
+ warnings
222252
+ };
222253
+ }
222254
+ let indexResult;
222255
+ try {
222256
+ const indexer = createRetrievalIndexer(input.supabase, { embeddingProvider });
222257
+ indexResult = pdfPages ? await indexer.indexLocalFilePages({
222258
+ workspaceId: input.workspaceId,
222259
+ sourceId: source.id,
222260
+ fileName: source.file_name,
222261
+ fileType: extracted.fileType,
222262
+ mimeType: source.mime_type,
222263
+ provenanceType: "desktop_local",
222264
+ contentHash: indexedContentHash,
222265
+ threadId: input.threadId ?? null,
222266
+ pages: pdfPages,
222267
+ blocks: pdfBlocks,
222268
+ tables: pdfTables,
222269
+ facts: factPageEntries.length ? factPageEntries : void 0
222270
+ }) : await indexer.indexLocalFileText({
222271
+ workspaceId: input.workspaceId,
222272
+ sourceId: source.id,
222273
+ fileName: source.file_name,
222274
+ fileType: extracted.fileType,
222275
+ mimeType: source.mime_type,
222276
+ provenanceType: "desktop_local",
222277
+ contentHash: indexedContentHash,
222278
+ threadId: input.threadId ?? null,
222279
+ text: extracted.text
222280
+ });
222281
+ } catch (error) {
222282
+ if (!isEmbeddingProviderNotConfigured(error)) throw error;
222283
+ warnings.push(error.message);
222284
+ return {
222285
+ ok: true,
222286
+ status: "indexed",
222287
+ localSourceId: input.localSourceId,
222288
+ supabaseSourceId: source.id,
222289
+ retrievalSourceId: null,
222290
+ contentHash: indexedContentHash,
222291
+ chunksBuilt,
222292
+ chunksUpserted: 0,
222293
+ sourceChunksUpserted: sourceTextChunks.length,
222294
+ retrievalChunksBuilt: chunksBuilt,
222295
+ retrievalChunksUpserted: 0,
222296
+ embeddedChunksUpserted: 0,
222297
+ vectorReady: false,
222074
222298
  embeddingConfigured: false,
222075
222299
  errors: [],
222076
222300
  warnings
222077
222301
  };
222078
222302
  }
222079
- const indexer = createRetrievalIndexer(input.supabase);
222080
- const indexResult = pdfPages ? await indexer.indexLocalFilePages({
222081
- workspaceId: input.workspaceId,
222082
- sourceId: source.id,
222083
- fileName: source.file_name,
222084
- fileType: extracted.fileType,
222085
- mimeType: source.mime_type,
222086
- provenanceType: "desktop_local",
222087
- contentHash: indexedContentHash,
222088
- threadId: input.threadId ?? null,
222089
- pages: pdfPages,
222090
- blocks: pdfBlocks,
222091
- tables: pdfTables,
222092
- facts: factPageEntries.length ? factPageEntries : void 0
222093
- }) : await indexer.indexLocalFileText({
222094
- workspaceId: input.workspaceId,
222095
- sourceId: source.id,
222096
- fileName: source.file_name,
222097
- fileType: extracted.fileType,
222098
- mimeType: source.mime_type,
222099
- provenanceType: "desktop_local",
222100
- contentHash: indexedContentHash,
222101
- threadId: input.threadId ?? null,
222102
- text: extracted.text
222103
- });
222104
222303
  chunksBuilt = indexResult.chunksBuilt;
222105
222304
  chunksUpserted = indexResult.chunksUpserted;
222305
+ const embeddedChunksUpserted = indexResult.chunksEmbedded;
222306
+ const vectorReady = embeddedChunksUpserted > 0;
222106
222307
  if (!indexResult.ok) {
222107
222308
  return {
222108
222309
  ok: false,
@@ -222113,6 +222314,11 @@ async function ensureLocalSourceIndexed(input) {
222113
222314
  contentHash: indexedContentHash,
222114
222315
  chunksBuilt,
222115
222316
  chunksUpserted,
222317
+ sourceChunksUpserted: sourceTextChunks.length,
222318
+ retrievalChunksBuilt: indexResult.chunksBuilt,
222319
+ retrievalChunksUpserted: indexResult.chunksUpserted,
222320
+ embeddedChunksUpserted,
222321
+ vectorReady,
222116
222322
  embeddingConfigured: true,
222117
222323
  errors: indexResult.errors,
222118
222324
  warnings: [...warnings, ...indexResult.warnings]
@@ -222123,10 +222329,15 @@ async function ensureLocalSourceIndexed(input) {
222123
222329
  status: "indexed",
222124
222330
  localSourceId: input.localSourceId,
222125
222331
  supabaseSourceId: source.id,
222126
- retrievalSourceId: source.id,
222332
+ retrievalSourceId: vectorReady ? source.id : null,
222127
222333
  contentHash: indexedContentHash,
222128
222334
  chunksBuilt,
222129
222335
  chunksUpserted,
222336
+ sourceChunksUpserted: sourceTextChunks.length,
222337
+ retrievalChunksBuilt: indexResult.chunksBuilt,
222338
+ retrievalChunksUpserted: indexResult.chunksUpserted,
222339
+ embeddedChunksUpserted,
222340
+ vectorReady,
222130
222341
  embeddingConfigured: true,
222131
222342
  errors: [],
222132
222343
  warnings
@@ -222142,6 +222353,12 @@ async function ensureLocalSourceIndexed(input) {
222142
222353
  };
222143
222354
  }
222144
222355
  }
222356
+ function createDefaultLocalSourceEmbeddingProvider() {
222357
+ return isBrowserRuntime() ? createBrowserEmbeddingProvider() : createEmbeddingProviderFromEnv();
222358
+ }
222359
+ function isBrowserRuntime() {
222360
+ return typeof window !== "undefined";
222361
+ }
222145
222362
  async function resolveRetrievalSourceIdForTurn(input) {
222146
222363
  const selected = input.selectedSourceId?.trim() ?? null;
222147
222364
  if (!selected) return { retrievalSourceId: null, indexOutcome: null };
@@ -222417,7 +222634,11 @@ async function loadFolderIndexSummary(input) {
222417
222634
  }
222418
222635
  const files = input.localSources.filter((entry) => entry.rootId === input.rootId && isAutoIndexableLocalSource(entry)).map((entry) => {
222419
222636
  const row = rowByLocalId.get(entry.localSourceId) ?? null;
222420
- const retrievalReady = row ? indexSets.retrievalChunkIds.has(row.id) : false;
222637
+ const sourceChunkCount2 = row ? indexSets.sourceChunkCountsBySourceId.get(row.id) ?? 0 : 0;
222638
+ const retrievalChunkCount2 = row ? indexSets.retrievalChunkCountsBySourceId.get(row.id) ?? 0 : 0;
222639
+ const embeddedChunkCount2 = row ? indexSets.embeddedRetrievalChunkCountsBySourceId.get(row.id) ?? 0 : 0;
222640
+ const retrievalReady = retrievalChunkCount2 > 0;
222641
+ const vectorReady = embeddedChunkCount2 > 0;
222421
222642
  const stale = row ? Date.parse(entry.modifiedAt) > Date.parse(row.updated_at) : true;
222422
222643
  return {
222423
222644
  localSourceId: entry.localSourceId,
@@ -222426,19 +222647,30 @@ async function loadFolderIndexSummary(input) {
222426
222647
  mimeType: entry.mimeType,
222427
222648
  modifiedAt: entry.modifiedAt,
222428
222649
  sizeBytes: entry.sizeBytes,
222429
- status: row ? retrievalReady ? "indexed" : "pending" : "pending",
222650
+ status: row ? sourceChunkCount2 > 0 || retrievalReady ? "indexed" : "pending" : "pending",
222430
222651
  supabaseSourceId: row?.id ?? null,
222431
222652
  retrievalReady,
222653
+ vectorReady,
222654
+ sourceChunkCount: sourceChunkCount2,
222655
+ retrievalChunkCount: retrievalChunkCount2,
222656
+ embeddedChunkCount: embeddedChunkCount2,
222432
222657
  contentHash: row?.content_hash ?? null,
222433
222658
  message: stale && row ? "Changed since last indexing pass." : void 0
222434
222659
  };
222435
222660
  });
222436
222661
  const staleFiles = files.filter((file) => file.message?.includes("Changed since last indexing pass")).length;
222437
222662
  const retrievalReadySourceIds = files.filter((file) => file.retrievalReady && file.supabaseSourceId).map((file) => file.supabaseSourceId);
222663
+ const vectorReadySourceIds = files.filter((file) => file.vectorReady && file.supabaseSourceId).map((file) => file.supabaseSourceId);
222438
222664
  const indexedFiles = files.filter((file) => file.status === "indexed").length;
222665
+ const sourceChunkCount = sumFiles(files, "sourceChunkCount");
222666
+ const retrievalChunkCount = sumFiles(files, "retrievalChunkCount");
222667
+ const embeddedChunkCount = sumFiles(files, "embeddedChunkCount");
222439
222668
  const lastIndexedAt = rows[0]?.updated_at ?? null;
222440
222669
  const diagnostics = [
222670
+ rows.length > 0 ? `${rows.length} registered source(s).` : null,
222671
+ sourceChunkCount > 0 ? `${sourceChunkCount} text/source chunk(s) available for keyword search.` : null,
222441
222672
  retrievalReadySourceIds.length > 0 ? `${retrievalReadySourceIds.length} file(s) have fresh retrieval chunks.` : "No retrieval-ready files yet for this folder.",
222673
+ vectorReadySourceIds.length > 0 ? `${vectorReadySourceIds.length} file(s) have embedded retrieval chunks for vector search.` : sourceChunkCount > 0 ? "Keyword/source search is available; semantic/vector retrieval is pending." : null,
222442
222674
  staleFiles > 0 ? `${staleFiles} file(s) changed since the last index update.` : null
222443
222675
  ].filter(Boolean);
222444
222676
  return {
@@ -222448,8 +222680,14 @@ async function loadFolderIndexSummary(input) {
222448
222680
  indexedFiles,
222449
222681
  skippedFiles: 0,
222450
222682
  failedFiles: 0,
222683
+ registeredSources: rows.length,
222684
+ sourceChunkCount,
222685
+ sourceChunkFiles: files.filter((file) => file.sourceChunkCount > 0).length,
222686
+ retrievalChunkCount,
222451
222687
  staleFiles,
222452
222688
  retrievalReadyFiles: retrievalReadySourceIds.length,
222689
+ embeddedChunkCount,
222690
+ vectorReadyFiles: vectorReadySourceIds.length,
222453
222691
  filesNeedingOcr: files.filter((file) => needsOcrHint(file)).length,
222454
222692
  lastIndexedAt,
222455
222693
  status: files.length === 0 ? "idle" : staleFiles > 0 || indexedFiles < files.length ? "partial" : "ready",
@@ -222457,6 +222695,7 @@ async function loadFolderIndexSummary(input) {
222457
222695
  localOnlyEmbeddingsEnabled: false,
222458
222696
  remoteIndexArtifacts: [...DEFAULT_REMOTE_INDEX_ARTIFACTS],
222459
222697
  retrievalReadySourceIds,
222698
+ vectorReadySourceIds,
222460
222699
  diagnostics,
222461
222700
  manifestPath: DEFAULT_MANIFEST_PATH,
222462
222701
  files
@@ -222476,6 +222715,9 @@ function needsOcrHint(file) {
222476
222715
  const message = file.message?.toLowerCase() ?? "";
222477
222716
  return message.includes("ocr") || message.includes("scanned") || message.includes("no readable text");
222478
222717
  }
222718
+ function sumFiles(files, field) {
222719
+ return files.reduce((total, file) => total + file[field], 0);
222720
+ }
222479
222721
  var DEFAULT_MANIFEST_PATH, AUTO_INDEX_EXCLUDED_PATH_PREFIXES, AUTO_INDEX_EXCLUDED_EXACT_PATHS, DEFAULT_STORAGE_MODE, DEFAULT_REMOTE_INDEX_ARTIFACTS;
222480
222722
  var init_folderIndexing = __esm({
222481
222723
  "features/perchTerminal/runtime/folderIndexing.ts"() {
@@ -224731,6 +224973,54 @@ var init_objectiveClassifier = __esm({
224731
224973
  });
224732
224974
 
224733
224975
  // features/perchTerminal/runtime/turn/runOperatorTurn.ts
224976
+ function buildTurnMemoryRetriever(input) {
224977
+ if (input.supabase) {
224978
+ return async ({ userId, workspaceId, limitPerScope, query }) => createPermanentMemoryPersistence(
224979
+ input.supabase
224980
+ ).retrievePermanentMemoriesForTurn({
224981
+ userId,
224982
+ workspaceId,
224983
+ limitPerScope,
224984
+ query
224985
+ });
224986
+ }
224987
+ const appUrl = input.cliServerAppUrl?.trim();
224988
+ const accessToken = input.cliServerAccessToken?.trim();
224989
+ if (!appUrl || !accessToken) return null;
224990
+ return createCliServerMemoryRetriever({
224991
+ appUrl,
224992
+ accessToken,
224993
+ threadId: input.threadId
224994
+ });
224995
+ }
224996
+ function createCliServerMemoryRetriever(input) {
224997
+ return async ({ limitPerScope, query }) => {
224998
+ const response = await fetch(`${input.appUrl.replace(/\/+$/, "")}/api/perch-terminal/cli-context`, {
224999
+ method: "POST",
225000
+ headers: {
225001
+ Accept: "application/json",
225002
+ "Content-Type": "application/json",
225003
+ Authorization: `Bearer ${input.accessToken}`
225004
+ },
225005
+ body: JSON.stringify({
225006
+ query,
225007
+ threadId: input.threadId,
225008
+ limitPerScope
225009
+ })
225010
+ });
225011
+ const payload = await response.json().catch(() => ({}));
225012
+ if (!response.ok || payload.ok !== true) {
225013
+ throw new Error("Durable memory is unavailable right now.");
225014
+ }
225015
+ const memories = Array.isArray(payload.permanentMemories) ? payload.permanentMemories.filter(isPermanentMemoryLike) : [];
225016
+ return memories;
225017
+ };
225018
+ }
225019
+ function isPermanentMemoryLike(value) {
225020
+ if (!value || typeof value !== "object") return false;
225021
+ const memory = value;
225022
+ return typeof memory.id === "string" && typeof memory.user_id === "string" && (memory.workspace_id === null || typeof memory.workspace_id === "string") && typeof memory.scope === "string" && typeof memory.type === "string" && typeof memory.title === "string" && typeof memory.body === "string" && typeof memory.source_kind === "string" && typeof memory.confidence === "number" && typeof memory.created_at === "string" && typeof memory.updated_at === "string" && Array.isArray(memory.tags) && Boolean(memory.metadata && typeof memory.metadata === "object");
225023
+ }
224734
225024
  async function runOperatorTurn(input, deps) {
224735
225025
  const runId = input.clientRunId?.trim() || makeRunId();
224736
225026
  const startMs = Date.now();
@@ -224796,21 +225086,15 @@ async function runOperatorTurn(input, deps) {
224796
225086
  });
224797
225087
  }
224798
225088
  try {
225089
+ const memoryRetriever = buildTurnMemoryRetriever(input);
224799
225090
  const memoryContext = await buildMemoryContext({
224800
225091
  query: input.trimmedInput,
224801
- userId: input.userId,
225092
+ userId: input.userId ?? (memoryRetriever && input.cliServerAccessToken?.trim() ? "cli_authenticated_user" : null),
224802
225093
  workspaceId: input.workspaceId,
224803
225094
  threadId: input.threadId,
224804
225095
  permanentMemories: input.permanentMemories,
224805
225096
  userMemories: input.userMemories,
224806
- retriever: input.supabase ? async ({ userId, workspaceId, limitPerScope, query }) => createPermanentMemoryPersistence(
224807
- input.supabase
224808
- ).retrievePermanentMemoriesForTurn({
224809
- userId,
224810
- workspaceId,
224811
- limitPerScope,
224812
- query
224813
- }) : null
225097
+ retriever: memoryRetriever
224814
225098
  });
224815
225099
  const earlySession = input.threadId ? await loadThreadSession(input.threadId, { supabase: input.supabase ?? null }) : null;
224816
225100
  const persona = getPersona(input.personaId ?? earlySession?.personaId);
@@ -224949,10 +225233,7 @@ async function runOperatorTurn(input, deps) {
224949
225233
  getEnabledToolDefinitions(toolOpts),
224950
225234
  PLAN_MODE_ALLOWED_TOOL_NAMES
224951
225235
  ) : effectiveChatMode === "agents" ? getExecutableToolDefinitions(toolOpts) : effectiveChatMode === "ask" ? [] : getReadOnlyToolDefinitions(toolOpts);
224952
- const personaFilteredTools = enrichedInput.personaId === "quill" ? filterToolsForQuill(baseTurnToolDefinitions) : enrichedInput.personaId === "saffron" ? filterToolsForSaffron(baseTurnToolDefinitions, {
224953
- allowSuiteRelay: suiteRelayEnabled
224954
- }) : baseTurnToolDefinitions;
224955
- const suiteRelayFilteredTools = filterSuiteRelayTools(personaFilteredTools, {
225236
+ const suiteRelayFilteredTools = filterSuiteRelayTools(baseTurnToolDefinitions, {
224956
225237
  allowSuiteRelay: suiteRelayEnabled
224957
225238
  });
224958
225239
  const turnToolDefinitions = filterMainPersonaDeliveryTools(suiteRelayFilteredTools, {
@@ -225098,8 +225379,15 @@ ${planStateLines}`
225098
225379
  if (perchMdRow?.status === "not_found") {
225099
225380
  extendedWarnings.push("No PERCH.md \u2014 add an operator playbook to .perch/PERCH.md to customise behaviour.");
225100
225381
  }
225101
- if (!enrichedInput.supabase) {
225102
- extendedWarnings.push("Memory retrieval skipped \u2014 no Supabase session available.");
225382
+ const hostedMemoryAvailable = Boolean(
225383
+ enrichedInput.cliServerAppUrl?.trim() && enrichedInput.cliServerAccessToken?.trim()
225384
+ );
225385
+ const memoryDiagnostics = enrichedInput.memoryContext?.diagnostics ?? null;
225386
+ if (memoryDiagnostics?.warnings.length) {
225387
+ extendedWarnings.push(...memoryDiagnostics.warnings);
225388
+ }
225389
+ if (!enrichedInput.supabase && !hostedMemoryAvailable && memoryDiagnostics?.retrieval.attempted !== true) {
225390
+ extendedWarnings.push("Memory retrieval skipped \u2014 sign in to use durable memory.");
225103
225391
  }
225104
225392
  const contextJob = resolveJobContextTokens({
225105
225393
  threadContextTokens: threadContextAccounting.threadContextTokens,
@@ -225633,8 +225921,7 @@ var init_runOperatorTurn = __esm({
225633
225921
  init_folderIndexing();
225634
225922
  init_approvalResume();
225635
225923
  init_personaRegistry();
225636
- init_quillToolPolicy();
225637
- init_saffronToolPolicy();
225924
+ init_sharedToolPolicy();
225638
225925
  init_deliveryToolPolicy();
225639
225926
  init_voiceFilters();
225640
225927
  init_progressEventBridge();
@@ -226511,6 +226798,21 @@ function installCliNodeLocalBridge(input) {
226511
226798
  }
226512
226799
  };
226513
226800
  }
226801
+ function readCliProjectMemoryState(workspaceRoot) {
226802
+ const root2 = path11.resolve(expandHome4(workspaceRoot));
226803
+ if (!isCliProjectMemoryAvailable(root2)) {
226804
+ return {
226805
+ available: false,
226806
+ meta: null,
226807
+ reason: "Local project memory is unavailable in this folder."
226808
+ };
226809
+ }
226810
+ return {
226811
+ available: true,
226812
+ meta: enrichCliProjectMeta(root2, loadCliStoredMeta(root2)),
226813
+ reason: "Local project memory is available."
226814
+ };
226815
+ }
226514
226816
  function createCliNodeLocalBridge(input) {
226515
226817
  const workspaceRoot = path11.resolve(input.workspaceRoot);
226516
226818
  const now13 = () => (/* @__PURE__ */ new Date()).toISOString();
@@ -226793,10 +227095,20 @@ function createCliNodeLocalBridge(input) {
226793
227095
  encoding: "base64"
226794
227096
  };
226795
227097
  },
226796
- getProjectRules: async () => [],
226797
- readProjectMemory: async () => ({ ok: false, error: "Project memory is not available in this CLI package yet." }),
226798
- writeProjectMemory: async () => ({ ok: false, error: "Project memory writes are not available in this CLI package yet." }),
226799
- writeMemoryFile: async () => ({ ok: false, error: "Project memory files are not available in this CLI package yet." }),
227098
+ getProjectRules: async () => {
227099
+ const state = readCliProjectMemoryState(workspaceRoot);
227100
+ return state.meta ? [{
227101
+ rootId: CLI_ROOT_ID,
227102
+ perchMd: state.meta.perchMd?.content ?? null,
227103
+ rules: state.meta.rules.map((rule) => ({
227104
+ fileName: rule.fileName,
227105
+ content: rule.content
227106
+ }))
227107
+ }] : [];
227108
+ },
227109
+ readProjectMemory: async () => readCliProjectMemoryBridge(workspaceRoot),
227110
+ writeProjectMemory: async (request) => writeCliProjectMemory(workspaceRoot, request.meta),
227111
+ writeMemoryFile: async (request) => writeCliMemoryFile(workspaceRoot, request),
226800
227112
  writeRule: async () => ({ ok: false, error: "Project rule writes are not available in this CLI package yet." }),
226801
227113
  writePerchMd: async () => ({ ok: false, error: "PERCH.md writes are not available in this CLI package yet." }),
226802
227114
  readGlobalPerchMd: async () => ({ ok: false, error: "Global PERCH.md is not available in this CLI package yet." }),
@@ -227075,6 +227387,292 @@ function guessCliFileType(filePath) {
227075
227387
  if ([".doc", ".docx", ".txt", ".md", ".rtf"].includes(ext)) return "document";
227076
227388
  return "unknown";
227077
227389
  }
227390
+ async function readCliProjectMemoryBridge(workspaceRoot) {
227391
+ const state = readCliProjectMemoryState(workspaceRoot);
227392
+ if (!state.available || !state.meta) {
227393
+ return {
227394
+ ok: false,
227395
+ error: "Local project memory is unavailable in this folder. Run from a project folder with .perch memory or sign in for durable memory."
227396
+ };
227397
+ }
227398
+ return { ok: true, meta: state.meta };
227399
+ }
227400
+ async function writeCliProjectMemory(workspaceRoot, patch) {
227401
+ const root2 = path11.resolve(expandHome4(workspaceRoot));
227402
+ if (!isCliProjectMemoryAvailable(root2)) {
227403
+ return {
227404
+ ok: false,
227405
+ error: "Local project memory is unavailable in this folder. Run from a project folder with .perch memory or sign in for durable memory."
227406
+ };
227407
+ }
227408
+ const current = loadCliStoredMeta(root2);
227409
+ const next = {
227410
+ ...current,
227411
+ projectName: patch.projectName !== void 0 ? patch.projectName : current.projectName,
227412
+ notes: patch.notes !== void 0 ? patch.notes : current.notes,
227413
+ preferredRoot: patch.preferredRoot !== void 0 ? patch.preferredRoot : current.preferredRoot,
227414
+ lastOpenedThreadId: patch.lastOpenedThreadId !== void 0 ? patch.lastOpenedThreadId : current.lastOpenedThreadId,
227415
+ memorySummary: patch.memorySummary !== void 0 ? patch.memorySummary : current.memorySummary
227416
+ };
227417
+ await fsp.mkdir(path11.join(root2, PERCH_DIR), { recursive: true });
227418
+ await atomicWriteCliUtf8(getCliProjectFilePath(root2), JSON.stringify(next, null, 2));
227419
+ return { ok: true, meta: enrichCliProjectMeta(root2, next) };
227420
+ }
227421
+ async function writeCliMemoryFile(workspaceRoot, request) {
227422
+ const root2 = path11.resolve(expandHome4(workspaceRoot));
227423
+ if (!isCliProjectMemoryAvailable(root2)) {
227424
+ return {
227425
+ ok: false,
227426
+ error: "Local project memory is unavailable in this folder. Run from a project folder with .perch memory or sign in for durable memory."
227427
+ };
227428
+ }
227429
+ if (request.rootId && request.rootId !== CLI_ROOT_ID) {
227430
+ return { ok: false, error: "Local project memory is unavailable for that workspace root." };
227431
+ }
227432
+ const fileName = normalizeCliMemoryFileName(request.fileName);
227433
+ if (!fileName) return { ok: false, error: "Invalid memory file name." };
227434
+ const relativePath = path11.join(PERCH_DIR, MEMORY_DIR, fileName);
227435
+ const fullPath = path11.join(root2, relativePath);
227436
+ const existing = await fsp.readFile(fullPath, "utf8").catch(() => "");
227437
+ let nextContent;
227438
+ try {
227439
+ nextContent = applyCliMemoryWrite(existing, request);
227440
+ } catch (error) {
227441
+ return {
227442
+ ok: false,
227443
+ error: error instanceof Error ? error.message : "Failed to merge memory content."
227444
+ };
227445
+ }
227446
+ const capError = assertCliMemoryWithinCap(nextContent);
227447
+ if (capError) return { ok: false, error: capError };
227448
+ await fsp.mkdir(path11.dirname(fullPath), { recursive: true });
227449
+ await atomicWriteCliUtf8(fullPath, nextContent);
227450
+ const updatedFile = readCliTextMemoryFile(root2, relativePath, MAX_MEMORY_BYTES);
227451
+ return {
227452
+ ok: true,
227453
+ meta: enrichCliProjectMeta(root2, loadCliStoredMeta(root2)),
227454
+ updatedFile
227455
+ };
227456
+ }
227457
+ function defaultCliProjectMeta() {
227458
+ return {
227459
+ projectName: null,
227460
+ notes: null,
227461
+ preferredRoot: null,
227462
+ lastOpenedThreadId: null,
227463
+ memorySummary: null,
227464
+ perchMd: null,
227465
+ rules: [],
227466
+ memoryFiles: [],
227467
+ missingMemoryFiles: [],
227468
+ projectMemoryEmpty: true,
227469
+ evidenceOnly: true
227470
+ };
227471
+ }
227472
+ function isCliProjectMemoryAvailable(root2) {
227473
+ try {
227474
+ return fs10.statSync(path11.join(root2, PERCH_DIR)).isDirectory();
227475
+ } catch {
227476
+ return false;
227477
+ }
227478
+ }
227479
+ function getCliProjectFilePath(root2) {
227480
+ return path11.join(root2, PERCH_DIR, PROJECT_FILE);
227481
+ }
227482
+ function loadCliStoredMeta(root2) {
227483
+ try {
227484
+ const raw = fs10.readFileSync(getCliProjectFilePath(root2), "utf8");
227485
+ return { ...defaultCliProjectMeta(), ...JSON.parse(raw) };
227486
+ } catch {
227487
+ return defaultCliProjectMeta();
227488
+ }
227489
+ }
227490
+ function enrichCliProjectMeta(root2, base) {
227491
+ const perchMd = readCliFirstExisting(root2, [
227492
+ path11.join(PERCH_DIR, PERCH_INDEX_FILE),
227493
+ PERCH_INDEX_FILE
227494
+ ], MAX_TEXT_BYTES);
227495
+ const rules = readCliRules(root2);
227496
+ const { memoryFiles, missingMemoryFiles } = readCliMemoryFiles(root2);
227497
+ const projectMemoryEmpty = !perchMd?.found && rules.length === 0 && memoryFiles.filter((file) => file.found).length === 0;
227498
+ return {
227499
+ ...base,
227500
+ perchMd,
227501
+ rules,
227502
+ memoryFiles,
227503
+ missingMemoryFiles,
227504
+ projectMemoryEmpty,
227505
+ evidenceOnly: projectMemoryEmpty
227506
+ };
227507
+ }
227508
+ function normalizeCliMemoryFileName(fileName) {
227509
+ if (typeof fileName !== "string") return null;
227510
+ const trimmed = fileName.trim();
227511
+ if (trimmed.includes("..") || trimmed.includes("/") || trimmed.includes("\\")) return null;
227512
+ return EXPECTED_MEMORY_FILES.includes(trimmed) ? trimmed : null;
227513
+ }
227514
+ function applyCliMemoryWrite(existing, request) {
227515
+ const incoming = request.content ?? "";
227516
+ if (request.mode === "replace") return incoming;
227517
+ if (request.mode === "append") {
227518
+ if (request.sectionHeading?.trim()) {
227519
+ return upsertCliMarkdownSection(existing, request.sectionHeading, incoming, "append");
227520
+ }
227521
+ if (!existing.trim()) return incoming;
227522
+ return `${existing}${existing.endsWith("\n") ? "" : "\n"}${incoming}`;
227523
+ }
227524
+ if (!request.sectionHeading?.trim()) {
227525
+ throw new Error('mode "merge" requires sectionHeading.');
227526
+ }
227527
+ return upsertCliMarkdownSection(existing, request.sectionHeading, incoming, "replace");
227528
+ }
227529
+ function upsertCliMarkdownSection(existing, sectionHeading, body, mode) {
227530
+ const heading = sectionHeading.trim().startsWith("#") ? sectionHeading.trim() : `## ${sectionHeading.trim()}`;
227531
+ const targetKey = normalizeCliHeadingKey(heading);
227532
+ const sections = parseCliMarkdownSections(existing);
227533
+ const index = sections.findIndex((section) => normalizeCliHeadingKey(section.heading) === targetKey);
227534
+ if (index >= 0) {
227535
+ const prior = sections[index];
227536
+ sections[index] = {
227537
+ heading: prior.heading || heading,
227538
+ body: mode === "append" && prior.body.trim() ? `${prior.body.trim()}
227539
+ ${body.trim()}` : body.trim()
227540
+ };
227541
+ } else {
227542
+ sections.push({ heading, body: body.trim() });
227543
+ }
227544
+ return sections.map((section) => {
227545
+ if (!section.heading) return section.body.trim();
227546
+ return `${section.heading}
227547
+
227548
+ ${section.body.trim()}`.trim();
227549
+ }).filter(Boolean).join("\n\n").concat("\n");
227550
+ }
227551
+ function parseCliMarkdownSections(content) {
227552
+ const sections = [];
227553
+ let current = null;
227554
+ for (const line of content.split(/\r?\n/)) {
227555
+ if (/^#{1,6}\s+/.test(line)) {
227556
+ if (current) sections.push(current);
227557
+ current = { heading: line.trimEnd(), body: "" };
227558
+ continue;
227559
+ }
227560
+ if (!current) {
227561
+ if (!line.trim()) continue;
227562
+ current = { heading: "", body: line };
227563
+ continue;
227564
+ }
227565
+ current.body = current.body ? `${current.body}
227566
+ ${line}` : line;
227567
+ }
227568
+ if (current) sections.push(current);
227569
+ return sections;
227570
+ }
227571
+ function normalizeCliHeadingKey(heading) {
227572
+ return heading.replace(/^#+\s*/, "").trim().toLowerCase();
227573
+ }
227574
+ function assertCliMemoryWithinCap(content) {
227575
+ return Buffer.byteLength(content, "utf8") > MAX_MEMORY_BYTES ? "Memory file exceeds 128KB cap." : null;
227576
+ }
227577
+ async function atomicWriteCliUtf8(filePath, content) {
227578
+ const tmpPath = `${filePath}.tmp`;
227579
+ await fsp.writeFile(tmpPath, content, "utf8");
227580
+ try {
227581
+ await fsp.rename(tmpPath, filePath);
227582
+ } catch (error) {
227583
+ await fsp.rm(tmpPath, { force: true }).catch(() => void 0);
227584
+ throw error;
227585
+ }
227586
+ }
227587
+ function readCliFirstExisting(root2, relativePaths, maxBytes) {
227588
+ for (const relativePath of relativePaths) {
227589
+ const file = readCliTextMemoryFile(root2, relativePath, maxBytes);
227590
+ if (file.found) {
227591
+ return {
227592
+ relativePath: file.relativePath,
227593
+ content: file.content,
227594
+ found: true,
227595
+ sizeBytes: file.sizeBytes,
227596
+ modifiedAt: file.modifiedAt
227597
+ };
227598
+ }
227599
+ }
227600
+ return null;
227601
+ }
227602
+ function readCliRules(root2) {
227603
+ const dirPath = path11.join(root2, PERCH_DIR, RULES_DIR);
227604
+ try {
227605
+ return fs10.readdirSync(dirPath).filter((name) => [".md", ".txt"].includes(path11.extname(name).toLowerCase())).slice(0, 40).map((name) => readCliTextMemoryFile(root2, path11.join(PERCH_DIR, RULES_DIR, name), MAX_TEXT_BYTES)).filter((file) => file.found).map((file) => ({
227606
+ fileName: path11.basename(file.relativePath),
227607
+ relativePath: file.relativePath,
227608
+ content: file.content,
227609
+ sizeBytes: file.sizeBytes,
227610
+ modifiedAt: file.modifiedAt
227611
+ }));
227612
+ } catch {
227613
+ return [];
227614
+ }
227615
+ }
227616
+ function readCliMemoryFiles(root2) {
227617
+ const memoryDir = path11.join(root2, PERCH_DIR, MEMORY_DIR);
227618
+ const discovered = /* @__PURE__ */ new Set();
227619
+ const memoryFiles = [];
227620
+ try {
227621
+ for (const name of fs10.readdirSync(memoryDir).slice(0, MAX_MEMORY_FILES)) {
227622
+ const ext = path11.extname(name).toLowerCase();
227623
+ if (ext !== ".md" && ext !== ".txt") continue;
227624
+ discovered.add(name);
227625
+ memoryFiles.push(readCliTextMemoryFile(root2, path11.join(PERCH_DIR, MEMORY_DIR, name), MAX_MEMORY_BYTES));
227626
+ }
227627
+ } catch {
227628
+ }
227629
+ return {
227630
+ memoryFiles,
227631
+ missingMemoryFiles: EXPECTED_MEMORY_FILES.filter((name) => !discovered.has(name)).map((name) => ({
227632
+ fileName: name,
227633
+ relativePath: path11.join(PERCH_DIR, MEMORY_DIR, name),
227634
+ reason: "expected project memory file was not found"
227635
+ }))
227636
+ };
227637
+ }
227638
+ function readCliTextMemoryFile(root2, relativePath, maxBytes) {
227639
+ const fullPath = path11.join(root2, relativePath);
227640
+ const fileName = path11.basename(relativePath);
227641
+ try {
227642
+ const stat2 = fs10.statSync(fullPath);
227643
+ if (!stat2.isFile()) {
227644
+ return { fileName, relativePath, content: "", found: false, error: "not a regular file" };
227645
+ }
227646
+ const content = fs10.readFileSync(fullPath, "utf8");
227647
+ if (stat2.size > maxBytes) {
227648
+ return {
227649
+ fileName,
227650
+ relativePath,
227651
+ content: `${content.slice(0, maxBytes)}
227652
+ [truncated: file exceeds ${maxBytes} bytes]`,
227653
+ found: true,
227654
+ sizeBytes: stat2.size,
227655
+ modifiedAt: stat2.mtime.toISOString()
227656
+ };
227657
+ }
227658
+ return {
227659
+ fileName,
227660
+ relativePath,
227661
+ content,
227662
+ found: true,
227663
+ sizeBytes: stat2.size,
227664
+ modifiedAt: stat2.mtime.toISOString()
227665
+ };
227666
+ } catch (error) {
227667
+ return {
227668
+ fileName,
227669
+ relativePath,
227670
+ content: "",
227671
+ found: false,
227672
+ error: error instanceof Error ? error.message : "read failed"
227673
+ };
227674
+ }
227675
+ }
227078
227676
  async function runShellCommand(input) {
227079
227677
  const startedAt = Date.now();
227080
227678
  const cwd2 = input.cwd ? resolveReadPath(input.workspaceRoot, input.cwd) : input.workspaceRoot;
@@ -227312,7 +227910,7 @@ function capOutput(stdout, stderr) {
227312
227910
  function shellQuote(parts) {
227313
227911
  return parts.map((part) => `'${part.replace(/'/g, "'\\''")}'`).join(" ");
227314
227912
  }
227315
- var CLI_ROOT_ID, DEFAULT_MAX_RESULTS, MAX_READ_BYTES, IGNORED_DIRS;
227913
+ var CLI_ROOT_ID, DEFAULT_MAX_RESULTS, MAX_READ_BYTES, IGNORED_DIRS, PERCH_DIR, PROJECT_FILE, PERCH_INDEX_FILE, MEMORY_DIR, RULES_DIR, MAX_TEXT_BYTES, MAX_MEMORY_BYTES, MAX_MEMORY_FILES, EXPECTED_MEMORY_FILES;
227316
227914
  var init_nodeLocalBridge = __esm({
227317
227915
  "features/perchTerminal/runtime/cliHost/nodeLocalBridge.ts"() {
227318
227916
  "use strict";
@@ -227323,6 +227921,20 @@ var init_nodeLocalBridge = __esm({
227323
227921
  DEFAULT_MAX_RESULTS = 200;
227324
227922
  MAX_READ_BYTES = 2e6;
227325
227923
  IGNORED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", "release"]);
227924
+ PERCH_DIR = ".perch";
227925
+ PROJECT_FILE = "project.json";
227926
+ PERCH_INDEX_FILE = "PERCH.md";
227927
+ MEMORY_DIR = "memory";
227928
+ RULES_DIR = "rules";
227929
+ MAX_TEXT_BYTES = 64e3;
227930
+ MAX_MEMORY_BYTES = 128e3;
227931
+ MAX_MEMORY_FILES = 80;
227932
+ EXPECTED_MEMORY_FILES = [
227933
+ "project.md",
227934
+ "preferences.md",
227935
+ "decisions.md",
227936
+ "permanent.md"
227937
+ ];
227326
227938
  }
227327
227939
  });
227328
227940
 
@@ -227404,10 +228016,13 @@ function buildCliTurnInput(input, resolved) {
227404
228016
  activeRootPath: input.activeRootPath ?? resolved.cwd,
227405
228017
  localSources: [],
227406
228018
  localSourcesMeta: null,
228019
+ projectMeta: readCliProjectMemoryState(resolved.cwd).meta,
227407
228020
  permanentMemories: input.permanentMemories ?? [],
227408
228021
  userMemories: input.userMemories ?? [],
227409
228022
  supabase: null,
227410
228023
  supabaseConfigured: false,
228024
+ cliServerAppUrl: input.cliServerAppUrl ?? input.marketDeskProxyAppUrl ?? input.appUrl ?? null,
228025
+ cliServerAccessToken: input.cliServerAccessToken ?? input.marketDeskProxyAccessToken ?? null,
227411
228026
  marketDeskProxyAppUrl: input.marketDeskProxyAppUrl ?? input.appUrl ?? null,
227412
228027
  marketDeskProxyAccessToken: input.marketDeskProxyAccessToken ?? null,
227413
228028
  permissionMode: normalizePermissionMode(input.permissionMode ?? "default"),
@@ -227483,6 +228098,7 @@ var init_runCliTurn = __esm({
227483
228098
  init_personaRegistry();
227484
228099
  init_runOperatorTurn();
227485
228100
  init_nodeLocalBridge();
228101
+ init_nodeLocalBridge();
227486
228102
  }
227487
228103
  });
227488
228104
 
@@ -227652,12 +228268,12 @@ import os2 from "node:os";
227652
228268
  import path13 from "node:path";
227653
228269
  import { promisify } from "node:util";
227654
228270
  async function readStoredCliAuthSession() {
227655
- const raw = process.platform === "darwin" ? await readKeychainSecret().catch(() => null) : await readFallbackSecret().catch(() => null);
228271
+ const raw = shouldUseFallbackAuthFile() ? await readFallbackSecret().catch(() => null) : process.platform === "darwin" ? await readKeychainSecret().catch(() => null) : await readFallbackSecret().catch(() => null);
227656
228272
  return parseStoredCliAuthSession(raw);
227657
228273
  }
227658
228274
  async function writeStoredCliAuthSession(session) {
227659
228275
  const raw = JSON.stringify(session);
227660
- if (process.platform === "darwin") {
228276
+ if (process.platform === "darwin" && !shouldUseFallbackAuthFile()) {
227661
228277
  await execFileAsync("security", [
227662
228278
  "add-generic-password",
227663
228279
  "-U",
@@ -227673,7 +228289,7 @@ async function writeStoredCliAuthSession(session) {
227673
228289
  await writeFallbackSecret(raw);
227674
228290
  }
227675
228291
  async function clearStoredCliAuthSession() {
227676
- if (process.platform === "darwin") {
228292
+ if (process.platform === "darwin" && !shouldUseFallbackAuthFile()) {
227677
228293
  await execFileAsync("security", [
227678
228294
  "delete-generic-password",
227679
228295
  "-s",
@@ -227685,6 +228301,9 @@ async function clearStoredCliAuthSession() {
227685
228301
  }
227686
228302
  await fs11.rm(fallbackSessionPath(), { force: true }).catch(() => void 0);
227687
228303
  }
228304
+ function shouldUseFallbackAuthFile() {
228305
+ return Boolean(process.env.PERCH_CLI_AUTH_DIR?.trim());
228306
+ }
227688
228307
  function isStoredCliAuthSessionUsable(session, nowSeconds = Math.floor(Date.now() / 1e3)) {
227689
228308
  if (!session?.accessToken?.trim()) return false;
227690
228309
  if (!session.appUrl?.trim()) return false;
@@ -283213,7 +283832,7 @@ async function runInkInteractivePerchCli(writer, deps, options) {
283213
283832
  text: `bash \xB7 ${truncateMiddle(commandText, 54)} \xB7 running`,
283214
283833
  tone: "muted",
283215
283834
  detailLines: [
283216
- { tone: "command", text: `$ ${commandText}` },
283835
+ { tone: "command", text: `$ ${commandText}`, language: "bash" },
283217
283836
  { tone: "meta", text: `cwd ${event.cwd || "."}` }
283218
283837
  ],
283219
283838
  expanded: false
@@ -283261,7 +283880,7 @@ async function runInkInteractivePerchCli(writer, deps, options) {
283261
283880
  richToolIds.current.add(itemId2);
283262
283881
  const commandText = renderCommandLine(event.command, event.args);
283263
283882
  const details = [
283264
- { tone: "command", text: `$ ${commandText}` },
283883
+ { tone: "command", text: `$ ${commandText}`, language: "bash" },
283265
283884
  { tone: "meta", text: `cwd ${event.cwd || "."}` },
283266
283885
  ...outputChunkToDetailLines(event.stdout, "stdout"),
283267
283886
  ...outputChunkToDetailLines(event.stderr, "stderr")
@@ -283294,9 +283913,10 @@ async function runInkInteractivePerchCli(writer, deps, options) {
283294
283913
  detailLines: [
283295
283914
  {
283296
283915
  tone: "command",
283297
- text: event.language === "shell" ? `$ ${event.command}` : `${event.language} cell`
283916
+ text: event.language === "shell" ? `$ ${event.command}` : `${event.language} cell`,
283917
+ language: event.language === "shell" ? "bash" : void 0
283298
283918
  },
283299
- ...event.language === "shell" ? [] : codePreviewDetailLines(event.command)
283919
+ ...event.language === "shell" ? [] : codePreviewDetailLines(event.command, cliLanguageForSandbox(event.language))
283300
283920
  ],
283301
283921
  expanded: false
283302
283922
  });
@@ -283489,14 +284109,7 @@ async function runInkInteractivePerchCli(writer, deps, options) {
283489
284109
  React11.createElement(
283490
284110
  Ink2.Box,
283491
284111
  { flexGrow: 1 },
283492
- React11.createElement(
283493
- Ink2.Text,
283494
- {
283495
- color: colorForInkDetailTone(line.tone),
283496
- dimColor: line.tone === "meta"
283497
- },
283498
- formatInkDetailLine(line)
283499
- )
284112
+ renderInkDetailContent(React11, Ink2, line)
283500
284113
  )
283501
284114
  );
283502
284115
  const renderTranscriptItem = (item, index) => {
@@ -283805,6 +284418,7 @@ function parseInteractiveSlashCommand(input) {
283805
284418
  }
283806
284419
  function renderInteractiveStatus(state, connection, session, workspaceId) {
283807
284420
  const storedAuth = session === void 0 ? renderCliAuthSummary(connection) : isStoredCliAuthSessionUsable(session) ? `signed in${session.email ? ` as ${session.email}` : ""}` : "not signed in";
284421
+ const signedIn = session === void 0 ? isCliModelConnectionReady(connection) : isStoredCliAuthSessionUsable(session);
283808
284422
  const connectionStatus = isCliModelConnectionReady(connection) ? "connected" : "locked \xB7 run /login";
283809
284423
  const color = shouldUseCliColor();
283810
284424
  const lines = [
@@ -283812,6 +284426,7 @@ function renderInteractiveStatus(state, connection, session, workspaceId) {
283812
284426
  ["cwd", state.cwd],
283813
284427
  ["auth", storedAuth],
283814
284428
  ["connection", connectionStatus],
284429
+ ["memory", renderCliMemoryAvailability(state.cwd, workspaceId ?? null, signedIn)],
283815
284430
  ["tools", renderCliCapabilityCount(state, workspaceId ?? null)],
283816
284431
  ["permission", state.permissionMode],
283817
284432
  ["mode", state.chatMode],
@@ -283823,6 +284438,15 @@ function renderInteractiveStatus(state, connection, session, workspaceId) {
283823
284438
  ];
283824
284439
  return lines.map(([key, value]) => `${paint(key.padEnd(11), "muted", color)} ${value}`).join("\n") + "\n";
283825
284440
  }
284441
+ function renderCliMemoryAvailability(cwd2, workspaceId, signedIn) {
284442
+ const local = readCliProjectMemoryState(cwd2).available;
284443
+ const parts = [];
284444
+ if (signedIn && workspaceId) parts.push("server memory");
284445
+ if (local) parts.push("local project memory");
284446
+ if (parts.length) return parts.join(" + ");
284447
+ if (signedIn) return "unavailable until a workspace is available";
284448
+ return "unavailable; sign in or run from a .perch project";
284449
+ }
283826
284450
  function renderCliCapabilityCount(state, workspaceId) {
283827
284451
  const count = getExecutableToolDefinitions({
283828
284452
  surface: "cli",
@@ -284017,7 +284641,21 @@ async function fetchCliHostedContext(connection, input) {
284017
284641
  return {
284018
284642
  userId: typeof context.session?.userId === "string" ? context.session.userId : session.userId ?? null,
284019
284643
  workspaceId: typeof context.session?.workspaceId === "string" ? context.session.workspaceId : null,
284020
- permanentMemories: Array.isArray(context.permanentMemories) ? context.permanentMemories.filter(isPermanentMemoryLike) : []
284644
+ permanentMemories: Array.isArray(context.permanentMemories) ? context.permanentMemories.filter(isPermanentMemoryLike2) : []
284645
+ };
284646
+ }
284647
+ async function fetchCliHostedContextForSession(session) {
284648
+ if (!isStoredCliAuthSessionUsable(session)) return null;
284649
+ const body = await postCliJson(session.appUrl, session, "/api/perch-terminal/cli-context", {
284650
+ query: "/status",
284651
+ threadId: "cli-status",
284652
+ limitPerScope: 1
284653
+ });
284654
+ if (!body || body.ok !== true) return null;
284655
+ const context = body;
284656
+ return {
284657
+ userId: typeof context.session?.userId === "string" ? context.session.userId : session.userId ?? null,
284658
+ workspaceId: typeof context.session?.workspaceId === "string" ? context.session.workspaceId : null
284021
284659
  };
284022
284660
  }
284023
284661
  async function resolveCliMarketDeskProxy(connection) {
@@ -284025,6 +284663,8 @@ async function resolveCliMarketDeskProxy(connection) {
284025
284663
  const session = await readStoredCliAuthSession();
284026
284664
  if (!isStoredCliAuthSessionUsable(session)) return {};
284027
284665
  return {
284666
+ cliServerAppUrl: session.appUrl || connection.appUrl,
284667
+ cliServerAccessToken: session.accessToken,
284028
284668
  marketDeskProxyAppUrl: session.appUrl || connection.appUrl,
284029
284669
  marketDeskProxyAccessToken: session.accessToken
284030
284670
  };
@@ -284069,7 +284709,7 @@ async function postCliJson(appUrl, session, pathname, payload) {
284069
284709
  clearTimeout(timeout);
284070
284710
  }
284071
284711
  }
284072
- function isPermanentMemoryLike(value) {
284712
+ function isPermanentMemoryLike2(value) {
284073
284713
  if (!value || typeof value !== "object") return false;
284074
284714
  const memory = value;
284075
284715
  return typeof memory.id === "string" && typeof memory.title === "string" && typeof memory.body === "string";
@@ -284156,6 +284796,53 @@ function bodyColorForInkTone(tone) {
284156
284796
  return "#fff8f0";
284157
284797
  }
284158
284798
  }
284799
+ function renderInkDetailContent(React11, Ink2, line) {
284800
+ const prefix = inkDetailPrefix(line.tone);
284801
+ const baseColor = colorForInkDetailTone(line.tone);
284802
+ const language = normalizeCliDetailLanguage(line.language);
284803
+ const tokens = shouldSyntaxHighlightDetail(line) ? tokenizeCliDetailSyntax(language, line.text) : [{ text: line.text, tone: "plain" }];
284804
+ return React11.createElement(
284805
+ Ink2.Text,
284806
+ null,
284807
+ prefix ? React11.createElement(
284808
+ Ink2.Text,
284809
+ { color: baseColor, bold: line.tone === "add" || line.tone === "remove" },
284810
+ prefix
284811
+ ) : null,
284812
+ ...tokens.map(
284813
+ (token, index) => React11.createElement(
284814
+ Ink2.Text,
284815
+ {
284816
+ key: `${line.tone}-${index}-${token.tone}`,
284817
+ color: colorForCliSyntaxTone(token.tone, line.tone),
284818
+ dimColor: line.tone === "meta" || token.tone === "comment"
284819
+ },
284820
+ token.text
284821
+ )
284822
+ )
284823
+ );
284824
+ }
284825
+ function shouldSyntaxHighlightDetail(line) {
284826
+ if (line.tone === "meta" || line.tone === "hunk") return false;
284827
+ if (line.tone === "stdout" || line.tone === "stderr") return false;
284828
+ return Boolean(normalizeCliDetailLanguage(line.language)) || line.tone === "command";
284829
+ }
284830
+ function inkDetailPrefix(tone) {
284831
+ switch (tone) {
284832
+ case "add":
284833
+ return "+ ";
284834
+ case "remove":
284835
+ return "- ";
284836
+ case "stderr":
284837
+ return "! ";
284838
+ case "meta":
284839
+ return "# ";
284840
+ case "stdout":
284841
+ return " ";
284842
+ default:
284843
+ return "";
284844
+ }
284845
+ }
284159
284846
  function colorForInkDetailTone(tone) {
284160
284847
  switch (tone) {
284161
284848
  case "add":
@@ -284174,21 +284861,168 @@ function colorForInkDetailTone(tone) {
284174
284861
  return CLI_BRAND.cream;
284175
284862
  }
284176
284863
  }
284177
- function formatInkDetailLine(line) {
284178
- switch (line.tone) {
284179
- case "add":
284180
- return `+ ${line.text}`;
284181
- case "remove":
284182
- return `- ${line.text}`;
284183
- case "stderr":
284184
- return `! ${line.text}`;
284185
- case "meta":
284186
- return `# ${line.text}`;
284187
- case "stdout":
284188
- return ` ${line.text}`;
284189
- default:
284190
- return line.text;
284864
+ function colorForCliSyntaxTone(tone, lineTone) {
284865
+ if (lineTone === "remove") {
284866
+ switch (tone) {
284867
+ case "comment":
284868
+ return "#8e6a55";
284869
+ case "string":
284870
+ return "#c48656";
284871
+ case "keyword":
284872
+ case "flag":
284873
+ return CLI_BRAND.bronzeGlint;
284874
+ case "number":
284875
+ return "#c77952";
284876
+ case "operator":
284877
+ return CLI_BRAND.bronzeDeep;
284878
+ default:
284879
+ return CLI_BRAND.bronzeGlint;
284880
+ }
284881
+ }
284882
+ if (lineTone === "add") {
284883
+ switch (tone) {
284884
+ case "comment":
284885
+ return "#7f9586";
284886
+ case "string":
284887
+ return "#b7c989";
284888
+ case "keyword":
284889
+ case "flag":
284890
+ return CLI_BRAND.patinaActive;
284891
+ case "number":
284892
+ return "#8fd19c";
284893
+ case "operator":
284894
+ return "#7e9f87";
284895
+ default:
284896
+ return CLI_BRAND.cream;
284897
+ }
284898
+ }
284899
+ if (lineTone === "command") {
284900
+ switch (tone) {
284901
+ case "comment":
284902
+ return "#7a6f66";
284903
+ case "string":
284904
+ case "path":
284905
+ return "#e5bc75";
284906
+ case "keyword":
284907
+ return CLI_BRAND.bronzeGlint;
284908
+ case "flag":
284909
+ return CLI_BRAND.patinaActive;
284910
+ case "number":
284911
+ return "#8fd19c";
284912
+ case "operator":
284913
+ return CLI_BRAND.muted;
284914
+ default:
284915
+ return CLI_BRAND.cream;
284916
+ }
284917
+ }
284918
+ return colorForInkDetailTone(lineTone);
284919
+ }
284920
+ function tokenizeCliDetailSyntax(language, text) {
284921
+ const normalized = language ?? "text";
284922
+ const pattern = cliSyntaxPatternForLanguage(normalized);
284923
+ if (!pattern) return [{ text, tone: "plain" }];
284924
+ const tokens = [];
284925
+ let lastIndex = 0;
284926
+ pattern.lastIndex = 0;
284927
+ for (let match = pattern.exec(text); match; match = pattern.exec(text)) {
284928
+ if (match.index > lastIndex) {
284929
+ tokens.push({ text: text.slice(lastIndex, match.index), tone: "plain" });
284930
+ }
284931
+ tokens.push({ text: match[0], tone: cliToneForSyntaxToken(normalized, match[0]) });
284932
+ lastIndex = match.index + match[0].length;
284933
+ }
284934
+ if (lastIndex < text.length) {
284935
+ tokens.push({ text: text.slice(lastIndex), tone: "plain" });
284191
284936
  }
284937
+ return tokens.length ? tokens : [{ text, tone: "plain" }];
284938
+ }
284939
+ function cliSyntaxPatternForLanguage(language) {
284940
+ if (["javascript", "typescript", "jsx", "tsx"].includes(language)) {
284941
+ return /(\/\/.*$|\/\*.*?\*\/|`(?:\\.|[^`])*`|'(?:\\.|[^'])*'|"(?:\\.|[^"])*"|\b(?:const|let|var|function|return|if|else|for|while|import|export|from|class|extends|new|await|async|try|catch|throw|type|interface|implements|switch|case|break|continue|null|undefined|true|false)\b|\b\d+(?:\.\d+)?\b|=>|===|!==|==|!=|\|\||&&|[{}()[\].,:;<>/+*=-])/g;
284942
+ }
284943
+ if (language === "python") {
284944
+ return /(#.*$|'''[\s\S]*?'''|"""[\s\S]*?"""|'(?:\\.|[^'])*'|"(?:\\.|[^"])*"|\b(?:def|class|return|if|elif|else|for|while|import|from|as|try|except|raise|with|yield|lambda|None|True|False|async|await|pass|break|continue|and|or|not)\b|\b\d+(?:\.\d+)?\b|[{}()[\].,:;<>/+*=-])/g;
284945
+ }
284946
+ if (language === "bash" || language === "shell") {
284947
+ return /(#.*$|'[^']*'|"(?:\\.|[^"])*"|\$(?:\w+|\{[^}]+\})|(?:^|\s)-{1,2}[A-Za-z0-9][\w-]*|\b(?:if|then|fi|for|do|done|case|esac|function|export|local|sudo|cd|echo|grep|sed|awk|find|cat|ls|mkdir|rm|mv|cp|npm|node|tsx|python|git|rg)\b|\b\d+\b|[|&;()<>])/g;
284948
+ }
284949
+ if (language === "json" || language === "yaml") {
284950
+ return /("(?:\\.|[^"\\])*"(?=\s*:)|"(?:\\.|[^"\\])*"|\b(?:true|false|null)\b|\b\d+(?:\.\d+)?\b|[:{}\[\],-])/g;
284951
+ }
284952
+ if (language === "css") {
284953
+ return /(\/\*.*?\*\/|"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|#[0-9a-fA-F]{3,8}\b|\b\d+(?:\.\d+)?(?:px|rem|em|%)?\b|[{}:;(),])/g;
284954
+ }
284955
+ if (language === "html" || language === "xml") {
284956
+ return /(<!--.*?-->|<\/?[A-Za-z0-9:-]+|"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|\/?>)/g;
284957
+ }
284958
+ return null;
284959
+ }
284960
+ function cliToneForSyntaxToken(language, text) {
284961
+ if (text.startsWith("//") || text.startsWith("/*") || text.startsWith("#") || text.startsWith("<!--")) {
284962
+ return "comment";
284963
+ }
284964
+ if (text.startsWith("'") || text.startsWith('"') || text.startsWith("`")) {
284965
+ return language === "json" && text.endsWith(":") ? "keyword" : "string";
284966
+ }
284967
+ if (/^\s*-{1,2}[A-Za-z0-9]/.test(text)) return "flag";
284968
+ if (/^\$/.test(text)) return "keyword";
284969
+ if (/^\d/.test(text)) return "number";
284970
+ if (/^[{}()[\].,:;<>/+*=\-|&]+$/.test(text)) return "operator";
284971
+ if (/[\\/]/.test(text) && !/\s/.test(text)) return "path";
284972
+ return "keyword";
284973
+ }
284974
+ function normalizeCliDetailLanguage(language) {
284975
+ const value = language?.trim().toLowerCase();
284976
+ if (!value) return null;
284977
+ const map2 = {
284978
+ js: "javascript",
284979
+ mjs: "javascript",
284980
+ cjs: "javascript",
284981
+ jsx: "jsx",
284982
+ ts: "typescript",
284983
+ mts: "typescript",
284984
+ cts: "typescript",
284985
+ tsx: "tsx",
284986
+ py: "python",
284987
+ python3: "python",
284988
+ sh: "bash",
284989
+ shell: "bash",
284990
+ zsh: "bash",
284991
+ yml: "yaml",
284992
+ htm: "html"
284993
+ };
284994
+ return map2[value] ?? value;
284995
+ }
284996
+ function cliLanguageForSandbox(language) {
284997
+ if (language === "node") return "javascript";
284998
+ if (language === "shell") return "bash";
284999
+ return normalizeCliDetailLanguage(language) ?? "text";
285000
+ }
285001
+ function inferCliLanguageFromPath(filePath) {
285002
+ const extension2 = filePath.split(/[./\\]/).pop()?.toLowerCase();
285003
+ if (!extension2 || extension2 === filePath.toLowerCase()) return null;
285004
+ const map2 = {
285005
+ js: "javascript",
285006
+ mjs: "javascript",
285007
+ cjs: "javascript",
285008
+ jsx: "jsx",
285009
+ ts: "typescript",
285010
+ mts: "typescript",
285011
+ cts: "typescript",
285012
+ tsx: "tsx",
285013
+ py: "python",
285014
+ sh: "bash",
285015
+ zsh: "bash",
285016
+ json: "json",
285017
+ jsonc: "json",
285018
+ yml: "yaml",
285019
+ yaml: "yaml",
285020
+ css: "css",
285021
+ html: "html",
285022
+ htm: "html",
285023
+ xml: "xml"
285024
+ };
285025
+ return map2[extension2] ?? null;
284192
285026
  }
284193
285027
  function buildFileToolDisplay(toolName, input, phase, summary) {
284194
285028
  const normalizedName = toolName.toLowerCase();
@@ -284199,12 +285033,13 @@ function buildFileToolDisplay(toolName, input, phase, summary) {
284199
285033
  const filePath = stringValue8(input.path) ?? stringValue8(input.filePath) ?? summary?.filePath ?? "file";
284200
285034
  const short = shortFilePath(filePath);
284201
285035
  const status = phase === "running" ? "running" : summary?.changeKind ?? "done";
285036
+ const language = inferCliLanguageFromPath(filePath);
284202
285037
  if (isWrite) {
284203
285038
  const content = stringValue8(input.content) ?? "";
284204
285039
  const added = summary?.linesAdded ?? countTextLines(content);
284205
285040
  const detailLines = [
284206
285041
  { tone: "hunk", text: `@@ ${short}` },
284207
- ...textToDetailLines(content, "add")
285042
+ ...textToDetailLines(content, "add", language)
284208
285043
  ];
284209
285044
  if (detailLines.length === 1 && summary) {
284210
285045
  detailLines.push({ tone: "meta", text: describeChangeSummary(summary) });
@@ -284221,8 +285056,8 @@ function buildFileToolDisplay(toolName, input, phase, summary) {
284221
285056
  const added = summary?.linesAdded ?? countTextLines(newText);
284222
285057
  const detailLines = [
284223
285058
  { tone: "hunk", text: `@@ ${short}` },
284224
- ...textToDetailLines(oldText, "remove"),
284225
- ...textToDetailLines(newText, "add")
285059
+ ...textToDetailLines(oldText, "remove", language),
285060
+ ...textToDetailLines(newText, "add", language)
284226
285061
  ];
284227
285062
  if (detailLines.length === 1 && summary) {
284228
285063
  detailLines.push({ tone: "meta", text: describeChangeSummary(summary) });
@@ -284248,18 +285083,23 @@ function describeChangeSummary(summary) {
284248
285083
  const removed = summary.linesRemoved ?? 0;
284249
285084
  return `${kind} \xB7 +${added} -${removed}`;
284250
285085
  }
284251
- function textToDetailLines(text, tone) {
285086
+ function textToDetailLines(text, tone, language) {
284252
285087
  if (!text) return [];
284253
285088
  const lines = text.split(/\r?\n/);
284254
- const preview = lines.slice(0, 38).map((line) => ({ tone, text: line }));
285089
+ const normalizedLanguage = normalizeCliDetailLanguage(language);
285090
+ const preview = lines.slice(0, 38).map((line) => ({
285091
+ tone,
285092
+ text: line,
285093
+ ...normalizedLanguage ? { language: normalizedLanguage } : {}
285094
+ }));
284255
285095
  if (lines.length > preview.length) {
284256
285096
  preview.push({ tone: "meta", text: `${lines.length - preview.length} more line(s)` });
284257
285097
  }
284258
285098
  return preview;
284259
285099
  }
284260
- function codePreviewDetailLines(code) {
285100
+ function codePreviewDetailLines(code, language) {
284261
285101
  if (!code.trim()) return [];
284262
- return textToDetailLines(code, "command").slice(0, 18);
285102
+ return textToDetailLines(code, "command", language).slice(0, 18);
284263
285103
  }
284264
285104
  function outputChunkToDetailLines(text, tone) {
284265
285105
  if (!text) return [];
@@ -284369,12 +285209,17 @@ async function runAuthCommand(parsed, writer) {
284369
285209
  if (parsed.action === "status") {
284370
285210
  const session = await readStoredCliAuthSession();
284371
285211
  if (isStoredCliAuthSessionUsable(session)) {
284372
- writer.stdout(`Perch CLI ${CLI_PACKAGE_VERSION} \xB7 Signed in${session.email ? ` as ${session.email}` : ""} \xB7 ${session.appUrl}
284373
- `);
285212
+ const hostedContext = await fetchCliHostedContextForSession(session).catch(() => null);
285213
+ writer.stdout([
285214
+ `Perch CLI ${CLI_PACKAGE_VERSION} \xB7 Signed in${session.email ? ` as ${session.email}` : ""} \xB7 ${session.appUrl}`,
285215
+ `Memory: ${renderCliMemoryAvailability(process.cwd(), hostedContext?.workspaceId ?? null, true)}`
285216
+ ].join("\n") + "\n");
284374
285217
  return 0;
284375
285218
  }
284376
- writer.stdout(`Perch CLI ${CLI_PACKAGE_VERSION} \xB7 Not signed in. Run \`perch login\`.
284377
- `);
285219
+ writer.stdout([
285220
+ `Perch CLI ${CLI_PACKAGE_VERSION} \xB7 Not signed in. Run \`perch login\`.`,
285221
+ `Memory: ${renderCliMemoryAvailability(process.cwd(), null, false)}`
285222
+ ].join("\n") + "\n");
284378
285223
  return 2;
284379
285224
  }
284380
285225
  const appUrl = resolveCliAppUrl(parsed.appUrl, DEFAULT_CLI_LOGIN_APP_URL) ?? DEFAULT_CLI_LOGIN_APP_URL;
@@ -284634,6 +285479,7 @@ var init_perch_cli = __esm({
284634
285479
  init_runRegistry();
284635
285480
  init_learningMemory();
284636
285481
  init_toolDefinitions();
285482
+ init_nodeLocalBridge();
284637
285483
  execFileAsync3 = promisify3(execFile3);
284638
285484
  DEFAULT_CLI_LOGIN_APP_URL = "https://app.perchai.app";
284639
285485
  CLI_PACKAGE_VERSION = readCliPackageVersion();