perchai-cli 2.4.19 → 2.4.20

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 +778 -110
  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",
@@ -80931,12 +80932,14 @@ function buildDesktopContextSection(input) {
80931
80932
  "grep",
80932
80933
  "statPath",
80933
80934
  "readLocalFile",
80935
+ "readProjectMemory",
80934
80936
  "listLocalSources",
80935
80937
  "readLocalSourceFile"
80936
80938
  ].filter((name) => enabledToolSet2.has(name));
80937
80939
  const localWriteTools = [
80938
80940
  "writeLocalFile",
80939
80941
  "editLocalFile",
80942
+ "saveToMemory",
80940
80943
  "bash",
80941
80944
  "runBashTerminalCommand",
80942
80945
  "generateAPAuditPacket",
@@ -80948,16 +80951,16 @@ function buildDesktopContextSection(input) {
80948
80951
  return {
80949
80952
  id: "desktop-context",
80950
80953
  lane: "desktop",
80951
- label: "CLI workspace",
80954
+ label: "Terminal workspace",
80952
80955
  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.",
80956
+ "## Terminal workspace",
80957
+ "Perch is running from a terminal. Files, shell, sandbox-code, and AP evidence tools are available for the current workspace.",
80955
80958
  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.",
80959
+ "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
80960
  localReadTools.length > 0 ? `Read tools: ${localReadTools.join(", ")}.` : "Read tools: none exposed for this turn.",
80958
80961
  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
80962
  ].join("\n"),
80960
- reason: "Terminal local workspace access and CLI tool availability for this turn.",
80963
+ reason: "Terminal workspace and tool availability for this turn.",
80961
80964
  sourcePath: input.activeRootPath,
80962
80965
  metadata: {
80963
80966
  desktopConnected: false,
@@ -81016,8 +81019,14 @@ function buildDesktopContextSection(input) {
81016
81019
  (file) => `- ${file.relativePath} (${file.matchReason})`
81017
81020
  ) : [];
81018
81021
  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).",
81022
+ const hasIndexedSourceSearch = Boolean(
81023
+ input.folderIndexSummary && (input.folderIndexSummary.sourceChunkCount > 0 || input.folderIndexSummary.retrievalChunkCount > 0)
81024
+ );
81025
+ const hasVectorSearch = Boolean(
81026
+ input.folderIndexSummary && (input.folderIndexSummary.embeddedChunkCount > 0 || input.folderIndexSummary.vectorReadyFiles > 0)
81027
+ );
81028
+ const searchHints = hasIndexedSourceSearch ? [
81029
+ 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
81030
  "Use glob for named files like *Screenshot* or exact extensions like *.png.",
81022
81031
  "Use statPath or glob before claiming a file does not exist.",
81023
81032
  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 +81035,16 @@ function buildDesktopContextSection(input) {
81026
81035
  "Use statPath or glob before claiming a file does not exist.",
81027
81036
  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
81037
  ];
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;
81038
+ const folderIndexLine = input.folderIndexSummary ? [
81039
+ `Folder index: ${input.folderIndexSummary.indexedFiles}/${input.folderIndexSummary.totalFiles} indexed`,
81040
+ `${input.folderIndexSummary.registeredSources} registered source(s)`,
81041
+ `${input.folderIndexSummary.sourceChunkCount} text/source chunk(s)`,
81042
+ `${input.folderIndexSummary.retrievalChunkCount} retrieval chunk(s)`,
81043
+ `${input.folderIndexSummary.embeddedChunkCount} embedded retrieval chunk(s)`,
81044
+ `${input.folderIndexSummary.vectorReadyFiles} vector-ready file(s)`,
81045
+ `${input.folderIndexSummary.filesNeedingOcr} need OCR`
81046
+ ].join(" \xB7 ") + "." : "Folder index: not loaded for this turn.";
81047
+ 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
81048
  const mcpToolNames = enabledToolSet.has("mcp__") ? [] : Array.from(enabledToolSet).filter((tool) => tool.startsWith("mcp__"));
81032
81049
  const mcpServerNames = Array.from(
81033
81050
  new Set(mcpToolNames.map((tool) => tool.split("__")[1]).filter(Boolean))
@@ -81060,7 +81077,7 @@ function buildDesktopContextSection(input) {
81060
81077
  content: [
81061
81078
  "## Desktop environment",
81062
81079
  "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.",
81080
+ 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
81081
  `Visible local sources: ${totalVisibleFiles}`,
81065
81082
  input.localSourcesMeta?.refreshedAt ? `Local source snapshot refreshed: ${input.localSourcesMeta.refreshedAt}` : null,
81066
81083
  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 +81094,7 @@ function buildDesktopContextSection(input) {
81077
81094
  availableReadTools.length > 0 ? `Read tools: ${availableReadTools.join(", ")}.` : "Read tools: none exposed for this turn.",
81078
81095
  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
81096
  ].filter(Boolean).join("\n"),
81080
- reason: "Local workspace access, optional folder scope, and tool availability for this turn.",
81097
+ reason: "Local workspace access, optional workspace folder, and tool availability for this turn.",
81081
81098
  sourcePath: input.activeRootPath,
81082
81099
  metadata: {
81083
81100
  desktopConnected: true,
@@ -85234,7 +85251,7 @@ function buildProjectMemorySections(meta) {
85234
85251
  lane: "project_memory",
85235
85252
  label: "PERCH.md",
85236
85253
  content: ["## PERCH.md", meta.perchMd.content].join("\n"),
85237
- reason: "Project operator memory loaded from the selected local folder scope.",
85254
+ reason: "Project operator memory loaded from the selected workspace folder.",
85238
85255
  sourcePath: meta.perchMd.relativePath,
85239
85256
  metadata: {
85240
85257
  relativePath: meta.perchMd.relativePath,
@@ -85271,7 +85288,7 @@ function buildProjectMemorySections(meta) {
85271
85288
  lane: "project_memory",
85272
85289
  label: `Rule: ${rule.fileName}`,
85273
85290
  content: [`## Project rule: ${rule.fileName}`, rule.content].join("\n"),
85274
- reason: "Rules-style project instruction loaded from the selected local folder scope.",
85291
+ reason: "Rules-style project instruction loaded from the selected workspace folder.",
85275
85292
  sourcePath: rule.relativePath,
85276
85293
  metadata: {
85277
85294
  relativePath: rule.relativePath,
@@ -85301,7 +85318,7 @@ function buildProjectMemorySections(meta) {
85301
85318
  content: [`## Project memory: ${memory.fileName}`, memory.content].join(
85302
85319
  "\n"
85303
85320
  ),
85304
- reason: "Typed project memory loaded from the selected local folder scope.",
85321
+ reason: "Typed project memory loaded from the selected workspace folder.",
85305
85322
  sourcePath: memory.relativePath,
85306
85323
  metadata: {
85307
85324
  relativePath: memory.relativePath,
@@ -85412,7 +85429,7 @@ Tool strengths:
85412
85429
  - 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
85430
  - 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
85431
  - 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
85432
+ - For a Mac folder: use glob and grep with the path; use listLocalSources/readLocalSourceFile only when a workspace folder is already selected
85416
85433
  - Do not run a tool just because a keyword matches; reason from the user's current request and the existing thread state
85417
85434
  - Do not redo an audit, document, or email draft that is already done unless the user asks for a revision
85418
85435
  - Summarize tool outputs; don't stream raw JSON to the user
@@ -92427,7 +92444,7 @@ function buildContextSummary(input, rows) {
92427
92444
  `mode=${input.chatMode}`,
92428
92445
  `messages=${input.recentMessages.length}`,
92429
92446
  `source=${input.selectedSourceId ?? "none"}`,
92430
- `desktop=${input.desktopConnected ? "connected" : input.cliLocalTools === true ? "cli-local" : "browser"}`,
92447
+ `desktop=${input.desktopConnected ? "connected" : input.cliLocalTools === true ? "terminal" : "browser"}`,
92431
92448
  `sent=${sentCount}`,
92432
92449
  `compacted=${compactedCount}`,
92433
92450
  `not_found=${notFoundCount}`,
@@ -92786,9 +92803,15 @@ function createEmbeddingProviderFromEnv(env4 = readEnv()) {
92786
92803
  };
92787
92804
  }
92788
92805
  function createBrowserEmbeddingProvider(route = "/api/perch-terminal/embeddings") {
92806
+ let resolvedModel = "server-configured";
92807
+ let resolvedDimensions = DEFAULT_RETRIEVAL_EMBEDDING_DIMENSIONS;
92789
92808
  return {
92790
- model: "server-configured",
92791
- dimensions: DEFAULT_RETRIEVAL_EMBEDDING_DIMENSIONS,
92809
+ get model() {
92810
+ return resolvedModel;
92811
+ },
92812
+ get dimensions() {
92813
+ return resolvedDimensions;
92814
+ },
92792
92815
  async embed(text) {
92793
92816
  return (await this.embedBatch([text]))[0];
92794
92817
  },
@@ -92811,6 +92834,12 @@ function createBrowserEmbeddingProvider(route = "/api/perch-terminal/embeddings"
92811
92834
  if (!Array.isArray(payload.embeddings)) {
92812
92835
  throw new EmbeddingProviderError("Embedding route returned no embeddings.");
92813
92836
  }
92837
+ if (typeof payload.model === "string" && payload.model.trim()) {
92838
+ resolvedModel = payload.model;
92839
+ }
92840
+ if (typeof payload.dimensions === "number" && Number.isFinite(payload.dimensions)) {
92841
+ resolvedDimensions = payload.dimensions;
92842
+ }
92814
92843
  return payload.embeddings;
92815
92844
  }
92816
92845
  };
@@ -92885,7 +92914,7 @@ var init_embeddingProvider = __esm({
92885
92914
 
92886
92915
  // features/perchTerminal/persistence/permanentMemoryPersistence.ts
92887
92916
  function createPermanentMemoryPersistence(supabase, options = {}) {
92888
- const embeddingProvider = options.embeddingProvider ?? createBrowserEmbeddingProvider();
92917
+ const embeddingProvider = options.embeddingProvider ?? createDefaultPermanentMemoryEmbeddingProvider();
92889
92918
  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
92919
  const api2 = {
92891
92920
  async listPermanentMemories({
@@ -93145,6 +93174,21 @@ function createPermanentMemoryPersistence(supabase, options = {}) {
93145
93174
  };
93146
93175
  return api2;
93147
93176
  }
93177
+ function createDefaultPermanentMemoryEmbeddingProvider() {
93178
+ if (typeof window !== "undefined") return createBrowserEmbeddingProvider();
93179
+ const status = getEmbeddingProviderStatus();
93180
+ let provider = null;
93181
+ const load = () => {
93182
+ provider ??= createEmbeddingProviderFromEnv();
93183
+ return provider;
93184
+ };
93185
+ return {
93186
+ model: status.configured ? status.model : DEFAULT_RETRIEVAL_EMBEDDING_MODEL,
93187
+ dimensions: status.configured ? status.dimensions : DEFAULT_RETRIEVAL_EMBEDDING_DIMENSIONS,
93188
+ embed: async (text) => load().embed(text),
93189
+ embedBatch: async (texts) => load().embedBatch(texts)
93190
+ };
93191
+ }
93148
93192
  async function findSimilarPermanentMemoryForInput(input) {
93149
93193
  const queryEmbedding = await input.embeddingProvider.embed(canonicalMemoryEmbeddingText(input.input));
93150
93194
  const { data, error } = await input.supabase.rpc("perch_ai_match_permanent_memories", {
@@ -117025,7 +117069,7 @@ function createSourcePersistence(supabase) {
117025
117069
  return count ?? 0;
117026
117070
  },
117027
117071
  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);
117072
+ 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
117073
  if (error) throw error;
117030
117074
  return count ?? 0;
117031
117075
  },
@@ -117131,18 +117175,30 @@ function createSourcePersistence(supabase) {
117131
117175
  return data ?? [];
117132
117176
  },
117133
117177
  async listIndexedSourceIds(workspaceId) {
117134
- const [chunks, retrieval, memory] = await Promise.all([
117178
+ const [chunks, retrieval, embeddedRetrieval, memory] = await Promise.all([
117135
117179
  supabase.from("perch_ai_source_chunks").select("source_id").eq("workspace_id", workspaceId),
117136
117180
  supabase.from("perch_ai_retrieval_chunks").select("source_id").eq("workspace_id", workspaceId).eq("is_stale", false),
117181
+ 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
117182
  supabase.from("perch_ai_source_memory").select("source_id").eq("workspace_id", workspaceId)
117138
117183
  ]);
117139
117184
  if (chunks.error) throw chunks.error;
117140
117185
  if (retrieval.error) throw retrieval.error;
117186
+ if (embeddedRetrieval.error) throw embeddedRetrieval.error;
117141
117187
  if (memory.error) throw memory.error;
117188
+ const sourceChunkSourceIds = sourceIdsFromRows(chunks.data);
117189
+ const retrievalChunkSourceIds = sourceIdsFromRows(retrieval.data);
117190
+ const embeddedRetrievalSourceIds = sourceIdsFromRows(embeddedRetrieval.data);
117142
117191
  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))
117192
+ sourceChunkIds: new Set(sourceChunkSourceIds),
117193
+ retrievalChunkIds: new Set(retrievalChunkSourceIds),
117194
+ embeddedRetrievalChunkIds: new Set(embeddedRetrievalSourceIds),
117195
+ memorySourceIds: new Set((memory.data ?? []).map((r) => r.source_id)),
117196
+ sourceChunkCountsBySourceId: countBySourceId(sourceChunkSourceIds),
117197
+ retrievalChunkCountsBySourceId: countBySourceId(retrievalChunkSourceIds),
117198
+ embeddedRetrievalChunkCountsBySourceId: countBySourceId(embeddedRetrievalSourceIds),
117199
+ totalSourceChunks: sourceChunkSourceIds.length,
117200
+ totalRetrievalChunks: retrievalChunkSourceIds.length,
117201
+ totalEmbeddedRetrievalChunks: embeddedRetrievalSourceIds.length
117146
117202
  };
117147
117203
  },
117148
117204
  async keywordSearchSourcesMetadata(workspaceId, query, limit) {
@@ -117198,6 +117254,16 @@ function createSourcePersistence(supabase) {
117198
117254
  function escapeIlike2(value) {
117199
117255
  return value.replace(/[%_\\]/g, (ch) => `\\${ch}`);
117200
117256
  }
117257
+ function sourceIdsFromRows(rows) {
117258
+ return (rows ?? []).map((row) => row.source_id).filter((sourceId) => typeof sourceId === "string" && sourceId.trim().length > 0);
117259
+ }
117260
+ function countBySourceId(sourceIds) {
117261
+ const counts = /* @__PURE__ */ new Map();
117262
+ for (const sourceId of sourceIds) {
117263
+ counts.set(sourceId, (counts.get(sourceId) ?? 0) + 1);
117264
+ }
117265
+ return counts;
117266
+ }
117201
117267
  var SOURCE_LIST_COLUMNS;
117202
117268
  var init_sourcePersistence = __esm({
117203
117269
  "features/perchTerminal/persistence/sourcePersistence.ts"() {
@@ -117875,21 +117941,22 @@ function getSourceRetrievalToolDefinitions() {
117875
117941
  }
117876
117942
  ];
117877
117943
  }
117878
- async function dispatchSourceRetrievalTool(toolName, args, session) {
117879
- if (!isSupabaseConfigured()) {
117880
- return sessionError("Supabase is not configured for this environment.");
117944
+ async function dispatchSourceRetrievalTool(toolName, args, session, options = {}) {
117945
+ if (!options.supabase && !isSupabaseConfigured()) {
117946
+ return sessionError("Workspace source search is unavailable in this session.");
117881
117947
  }
117882
117948
  if (!session.workspaceId) {
117883
- return sessionError("workspace_id is required from the authenticated session.");
117949
+ return sessionError("Sign in to a workspace before using workspace source search.");
117884
117950
  }
117885
117951
  let supabase;
117886
117952
  try {
117887
- supabase = createClient();
117953
+ supabase = options.supabase ?? createClient();
117888
117954
  } catch (err) {
117889
117955
  const message = err instanceof Error ? err.message : String(err);
117890
117956
  return sessionError(message);
117891
117957
  }
117892
117958
  const store = createSourcePersistence(supabase);
117959
+ const retrievalStore = createRetrievalPersistence(supabase);
117893
117960
  const workspaceId = session.workspaceId;
117894
117961
  switch (toolName) {
117895
117962
  case TOOL_NAMES.listSources:
@@ -117905,9 +117972,9 @@ async function dispatchSourceRetrievalTool(toolName, args, session) {
117905
117972
  case TOOL_NAMES.resolveSourceCandidates:
117906
117973
  return resolveSourceCandidatesHandler(store, workspaceId, session, args);
117907
117974
  case TOOL_NAMES.retrieveContext:
117908
- return retrieveContextHandler(store, workspaceId, session, args);
117975
+ return retrieveContextHandler(store, retrievalStore, workspaceId, session, args);
117909
117976
  case TOOL_NAMES.semanticSearch:
117910
- return semanticSearchHandler(store, workspaceId, args);
117977
+ return semanticSearchHandler(store, retrievalStore, workspaceId, args);
117911
117978
  case TOOL_NAMES.diagnoseWorkspaceAccess:
117912
117979
  return diagnoseWorkspaceAccessHandler(store, workspaceId, session);
117913
117980
  default:
@@ -118170,7 +118237,7 @@ async function resolveSourceCandidatesHandler(store, workspaceId, session, args)
118170
118237
  schemaVersion: "perch-tool-result-v1"
118171
118238
  };
118172
118239
  }
118173
- async function retrieveContextHandler(store, workspaceId, session, args) {
118240
+ async function retrieveContextHandler(store, retrievalStore, workspaceId, session, args) {
118174
118241
  const query = stringArg(args.query)?.trim();
118175
118242
  if (!query) {
118176
118243
  return { ok: false, error: "query is required", toolName: TOOL_NAMES.retrieveContext };
@@ -118196,7 +118263,6 @@ async function retrieveContextHandler(store, workspaceId, session, args) {
118196
118263
  let searchMode = "keyword_only";
118197
118264
  let fallbackUsed = false;
118198
118265
  let semanticError = null;
118199
- const retrievalStore = createRetrievalPersistence(createClient());
118200
118266
  const embeddedCount = await store.countEmbeddedRetrievalChunks(workspaceId).catch(() => 0);
118201
118267
  retrievalAttempts.push("semantic_first");
118202
118268
  try {
@@ -118394,7 +118460,7 @@ async function retrieveContextHandler(store, workspaceId, session, args) {
118394
118460
  schemaVersion: "perch-tool-result-v1"
118395
118461
  };
118396
118462
  }
118397
- async function semanticSearchHandler(store, workspaceId, args) {
118463
+ async function semanticSearchHandler(store, retrievalStore, workspaceId, args) {
118398
118464
  const query = stringArg(args.query)?.trim();
118399
118465
  if (!query) {
118400
118466
  return { ok: false, error: "query is required", toolName: TOOL_NAMES.semanticSearch };
@@ -118403,7 +118469,7 @@ async function semanticSearchHandler(store, workspaceId, args) {
118403
118469
  const sourceIds = Array.isArray(args.sourceIds) ? args.sourceIds.map(String) : void 0;
118404
118470
  const includeStale = args.includeStale === true;
118405
118471
  const embeddedCount = await store.countEmbeddedRetrievalChunks(workspaceId);
118406
- const result2 = await createRetrievalPersistence(createClient()).matchRetrievalChunks({
118472
+ const result2 = await retrievalStore.matchRetrievalChunks({
118407
118473
  workspaceId,
118408
118474
  query,
118409
118475
  limit,
@@ -118448,7 +118514,7 @@ async function semanticSearchHandler(store, workspaceId, args) {
118448
118514
  return {
118449
118515
  rankSignals: signals,
118450
118516
  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"}`
118517
+ 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
118518
  };
118453
118519
  })(),
118454
118520
  rank: index + 1,
@@ -118488,7 +118554,7 @@ async function semanticSearchHandler(store, workspaceId, args) {
118488
118554
  semanticAvailable: result2.embeddingProviderConfigured,
118489
118555
  sparseAvailable: result2.sparseProviderConfigured,
118490
118556
  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"
118557
+ 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
118558
  },
118493
118559
  schemaVersion: "perch-tool-result-v1"
118494
118560
  };
@@ -118497,7 +118563,7 @@ async function diagnoseWorkspaceAccessHandler(store, workspaceId, session) {
118497
118563
  const workspace = await store.getWorkspaceRow(workspaceId);
118498
118564
  const blockers = [];
118499
118565
  if (!workspace) {
118500
- blockers.push("Workspace row not found or not accessible under RLS.");
118566
+ blockers.push("Workspace is not available to this signed-in session.");
118501
118567
  }
118502
118568
  let sourcesCount = 0;
118503
118569
  let sourceChunksCount = 0;
@@ -133500,6 +133566,8 @@ var init_toolPermissionPolicy = __esm({
133500
133566
  TOOL_NAMES.deleteLocalFile,
133501
133567
  TOOL_NAMES.editLocalFile,
133502
133568
  TOOL_NAMES.statPath,
133569
+ TOOL_NAMES.readProjectMemory,
133570
+ TOOL_NAMES.saveToMemory,
133503
133571
  TOOL_NAMES.listLocalSources,
133504
133572
  TOOL_NAMES.readLocalSourceFile,
133505
133573
  TOOL_NAMES.generateAPAuditPacket,
@@ -135197,7 +135265,7 @@ function getDesktopToolDefinitions() {
135197
135265
  type: "function",
135198
135266
  function: {
135199
135267
  name: TOOL_NAMES.readProjectMemory,
135200
- description: "Read project memory metadata for the active folder scope.",
135268
+ 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
135269
  parameters: {
135202
135270
  type: "object",
135203
135271
  properties: {},
@@ -135209,7 +135277,7 @@ function getDesktopToolDefinitions() {
135209
135277
  type: "function",
135210
135278
  function: {
135211
135279
  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.",
135280
+ 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
135281
  parameters: {
135214
135282
  type: "object",
135215
135283
  properties: {
@@ -135225,6 +135293,15 @@ function getDesktopToolDefinitions() {
135225
135293
  sectionHeading: {
135226
135294
  type: "string",
135227
135295
  description: "Required for merge mode \u2014 markdown heading to upsert under."
135296
+ },
135297
+ scope: {
135298
+ type: "string",
135299
+ enum: ["private_user", "workspace", "project"],
135300
+ description: "Optional durable-memory scope for signed-in server memory. Defaults from fileName."
135301
+ },
135302
+ title: {
135303
+ type: "string",
135304
+ description: "Optional short title for signed-in durable memory."
135228
135305
  }
135229
135306
  },
135230
135307
  required: ["fileName", "mode", "content"],
@@ -135280,7 +135357,7 @@ function getDesktopToolDefinitions() {
135280
135357
  type: "function",
135281
135358
  function: {
135282
135359
  name: TOOL_NAMES.getProjectRules,
135283
- description: "Read PERCH.md and small .perch/rules/*.md or *.txt project rule files for the active folder scope.",
135360
+ description: "Read PERCH.md and small .perch/rules/*.md or *.txt project rule files for the active workspace folder.",
135284
135361
  parameters: {
135285
135362
  type: "object",
135286
135363
  properties: {},
@@ -135298,7 +135375,7 @@ function getDesktopToolDefinitions() {
135298
135375
  properties: {
135299
135376
  path: {
135300
135377
  type: "string",
135301
- description: "Absolute directory path to list (e.g. /Users/you/Desktop). Works with no folder scope selected."
135378
+ description: "Absolute directory path to list (e.g. /Users/you/Desktop). Works with no workspace folder selected."
135302
135379
  },
135303
135380
  query: {
135304
135381
  type: "string",
@@ -136486,7 +136563,7 @@ function getNativeToolDefinitions() {
136486
136563
  type: "function",
136487
136564
  function: {
136488
136565
  name: TOOL_NAMES.ctxInspect,
136489
- description: "Inspect current execution context: desktop connection status, selected folder scope, permission mode, tool counts, and available tools.",
136566
+ description: "Inspect current execution context: desktop connection status, selected workspace folder, permission mode, tool counts, and available tools.",
136490
136567
  parameters: {
136491
136568
  type: "object",
136492
136569
  properties: {},
@@ -136520,7 +136597,7 @@ function getNativeToolDefinitions() {
136520
136597
  type: "function",
136521
136598
  function: {
136522
136599
  name: TOOL_NAMES.configInspect,
136523
- description: "Inspect the Perch Terminal configuration: local workspace access, optional folder scope, permission mode, and capabilities.",
136600
+ description: "Inspect the Perch Terminal configuration: local workspace access, optional workspace folder, permission mode, and capabilities.",
136524
136601
  parameters: {
136525
136602
  type: "object",
136526
136603
  properties: {},
@@ -206113,7 +206190,7 @@ async function requireMemoryRootId(ctx, surface) {
206113
206190
  if (!rootId) {
206114
206191
  return {
206115
206192
  ok: false,
206116
- error: `No selected folder scope is available for ${surface}.`
206193
+ error: `No selected workspace folder is available for ${surface}.`
206117
206194
  };
206118
206195
  }
206119
206196
  return { ok: true, rootId };
@@ -206138,14 +206215,86 @@ var init_readProjectMemory = __esm({
206138
206215
  classification: { native: false },
206139
206216
  handler: async (_args, ctx) => {
206140
206217
  const root2 = await requireMemoryRootId(ctx, "project memory");
206141
- if (!root2.ok) return root2;
206142
- return readProjectMemory(root2.rootId);
206218
+ if (!root2.ok) {
206219
+ return ctx.cliServerAppUrl?.trim() && ctx.cliServerAccessToken?.trim() ? {
206220
+ ok: true,
206221
+ backend: "server_memory",
206222
+ message: "Signed-in durable memory is available. Relevant memories are added automatically at the start of each CLI turn."
206223
+ } : {
206224
+ ok: false,
206225
+ errorCode: "memory_unavailable",
206226
+ error: "Project memory is unavailable in this CLI session. Run from a project folder with .perch memory or sign in for durable memory."
206227
+ };
206228
+ }
206229
+ const result2 = await readProjectMemory(root2.rootId);
206230
+ if (!result2.ok && ctx.cliServerAppUrl?.trim() && ctx.cliServerAccessToken?.trim()) {
206231
+ return {
206232
+ ok: true,
206233
+ backend: "server_memory",
206234
+ localProjectMemory: result2,
206235
+ message: "Signed-in durable memory is available. Relevant memories are added automatically at the start of each CLI turn."
206236
+ };
206237
+ }
206238
+ return result2;
206143
206239
  }
206144
206240
  };
206145
206241
  }
206146
206242
  });
206147
206243
 
206148
206244
  // features/perchTerminal/runtime/toolSystem/tools/projectMemory/saveToMemory.ts
206245
+ function hasCliServerMemory(ctx) {
206246
+ return Boolean(
206247
+ ctx.cliServerAppUrl?.trim() && ctx.cliServerAccessToken?.trim()
206248
+ );
206249
+ }
206250
+ async function saveMemoryThroughCliServer(args, ctx) {
206251
+ const appUrl = ctx.cliServerAppUrl?.trim();
206252
+ const accessToken = ctx.cliServerAccessToken?.trim();
206253
+ if (!appUrl || !accessToken) return memoryUnavailable();
206254
+ try {
206255
+ const response = await fetch(`${appUrl.replace(/\/+$/, "")}/api/perch-terminal/cli-memory`, {
206256
+ method: "POST",
206257
+ headers: {
206258
+ Accept: "application/json",
206259
+ "Content-Type": "application/json",
206260
+ Authorization: `Bearer ${accessToken}`
206261
+ },
206262
+ body: JSON.stringify({
206263
+ action: "save",
206264
+ args,
206265
+ threadId: ctx.threadId ?? null,
206266
+ runId: ctx.runId ?? null,
206267
+ workspaceRoot: ctx.activeRootPath ?? ctx.workspaceRoot ?? null
206268
+ }),
206269
+ signal: ctx.signal
206270
+ });
206271
+ const payload = await response.json().catch(() => ({}));
206272
+ if (!response.ok) {
206273
+ return {
206274
+ ok: false,
206275
+ backend: "server_memory",
206276
+ errorCode: typeof payload.errorCode === "string" ? payload.errorCode : "memory_save_unavailable",
206277
+ error: typeof payload.message === "string" ? payload.message : "Durable memory is unavailable right now. Try again shortly or update Perch."
206278
+ };
206279
+ }
206280
+ return payload;
206281
+ } catch (error) {
206282
+ const aborted = error instanceof DOMException && error.name === "AbortError";
206283
+ return {
206284
+ ok: false,
206285
+ backend: "server_memory",
206286
+ errorCode: aborted ? "memory_save_cancelled" : "memory_save_unavailable",
206287
+ error: aborted ? "Memory save was stopped." : "Durable memory is unavailable right now. Try again shortly or update Perch."
206288
+ };
206289
+ }
206290
+ }
206291
+ function memoryUnavailable() {
206292
+ return {
206293
+ ok: false,
206294
+ errorCode: "memory_unavailable",
206295
+ error: "Memory is unavailable in this CLI session. Sign in with `perch login` or run from a project folder with .perch memory."
206296
+ };
206297
+ }
206149
206298
  var saveToMemoryTool;
206150
206299
  var init_saveToMemory = __esm({
206151
206300
  "features/perchTerminal/runtime/toolSystem/tools/projectMemory/saveToMemory.ts"() {
@@ -206157,14 +206306,24 @@ var init_saveToMemory = __esm({
206157
206306
  name: TOOL_NAMES.saveToMemory,
206158
206307
  classification: { native: false },
206159
206308
  handler: async (args, ctx) => {
206309
+ const serverResult = hasCliServerMemory(ctx) ? await saveMemoryThroughCliServer(args, ctx) : null;
206310
+ if (serverResult?.ok) return serverResult;
206160
206311
  const root2 = await requireMemoryRootId(ctx, "project memory");
206161
- if (!root2.ok) return root2;
206162
- return writeMemoryFile(root2.rootId, {
206312
+ if (!root2.ok) return serverResult ?? memoryUnavailable();
206313
+ const localResult = await writeMemoryFile(root2.rootId, {
206163
206314
  fileName: String(args.fileName),
206164
206315
  mode: String(args.mode),
206165
206316
  content: String(args.content ?? ""),
206166
206317
  sectionHeading: typeof args.sectionHeading === "string" ? args.sectionHeading : void 0
206167
206318
  });
206319
+ if (localResult.ok === true) {
206320
+ return {
206321
+ ...localResult,
206322
+ backend: "local_project_memory",
206323
+ 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."
206324
+ };
206325
+ }
206326
+ return serverResult ?? localResult ?? memoryUnavailable();
206168
206327
  }
206169
206328
  };
206170
206329
  }
@@ -213219,7 +213378,7 @@ var init_localSources = __esm({
213219
213378
  return {
213220
213379
  ok: true,
213221
213380
  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."
213381
+ warning: "Local workspace access is unavailable in this chat. Open Perch Desktop or attach a workspace folder, then try again."
213223
213382
  };
213224
213383
  }
213225
213384
  const maxResults = sanitizeListLocalSourcesMaxResults(args.maxResults);
@@ -216703,6 +216862,8 @@ async function executeToolCall(call, opts) {
216703
216862
  workspaceId: opts.workspaceId,
216704
216863
  threadId: opts.threadId,
216705
216864
  supabase: opts.supabase,
216865
+ cliServerAppUrl: opts.cliServerAppUrl,
216866
+ cliServerAccessToken: opts.cliServerAccessToken,
216706
216867
  marketDeskProxyAppUrl: opts.marketDeskProxyAppUrl,
216707
216868
  marketDeskProxyAccessToken: opts.marketDeskProxyAccessToken,
216708
216869
  chatMode: opts.chatMode ?? null,
@@ -216762,7 +216923,7 @@ async function executeToolCall(call, opts) {
216762
216923
  const result2 = {
216763
216924
  ok: false,
216764
216925
  errorCode: "supabase_session_required",
216765
- error: isSupabaseConfigured() ? "Source retrieval tools require an authenticated workspace session." : "Supabase is not configured for source retrieval tools.",
216926
+ error: isSupabaseConfigured() ? "Source retrieval tools require an authenticated workspace session." : "Workspace source search is unavailable in this session.",
216766
216927
  schemaVersion: "perch-tool-result-v1",
216767
216928
  toolName: call.name
216768
216929
  };
@@ -216843,6 +217004,8 @@ async function executeToolCall(call, opts) {
216843
217004
  workspaceRoot: opts.workspaceRoot,
216844
217005
  threadId: opts.threadId,
216845
217006
  runId: opts.runId,
217007
+ cliServerAppUrl: opts.cliServerAppUrl,
217008
+ cliServerAccessToken: opts.cliServerAccessToken,
216846
217009
  marketDeskProxyAppUrl: opts.marketDeskProxyAppUrl,
216847
217010
  marketDeskProxyAccessToken: opts.marketDeskProxyAccessToken,
216848
217011
  onEvent: runtime.onEvent,
@@ -219043,6 +219206,8 @@ async function runModelToolLoop(input) {
219043
219206
  selectedSourceId: input.selectedSourceId,
219044
219207
  supabaseConfigured: input.supabaseConfigured,
219045
219208
  supabase: input.supabase,
219209
+ cliServerAppUrl: input.cliServerAppUrl,
219210
+ cliServerAccessToken: input.cliServerAccessToken,
219046
219211
  marketDeskProxyAppUrl: input.marketDeskProxyAppUrl,
219047
219212
  marketDeskProxyAccessToken: input.marketDeskProxyAccessToken,
219048
219213
  runId: input.runId,
@@ -219459,6 +219624,8 @@ async function executeInitialToolCall(input) {
219459
219624
  selectedSourceId: input.input.selectedSourceId,
219460
219625
  supabaseConfigured: input.input.supabaseConfigured,
219461
219626
  supabase: input.input.supabase,
219627
+ cliServerAppUrl: input.input.cliServerAppUrl,
219628
+ cliServerAccessToken: input.input.cliServerAccessToken,
219462
219629
  marketDeskProxyAppUrl: input.input.marketDeskProxyAppUrl,
219463
219630
  marketDeskProxyAccessToken: input.input.marketDeskProxyAccessToken,
219464
219631
  runId: input.input.runId,
@@ -220520,7 +220687,7 @@ async function runLiveAgentsLoop(input) {
220520
220687
  const onEv = onEvent ?? deps.onEvent;
220521
220688
  onEv({
220522
220689
  type: "activity_delta",
220523
- text: "Workspace preflight: no folder scope selected \u2014 continuing with Desktop tools.",
220690
+ text: "Workspace preflight: no workspace folder selected; continuing with Desktop tools.",
220524
220691
  ts: (/* @__PURE__ */ new Date()).toISOString()
220525
220692
  });
220526
220693
  }
@@ -220559,6 +220726,8 @@ async function runLiveAgentsLoop(input) {
220559
220726
  untrustedContextPresent: context.untrustedContextPresent,
220560
220727
  supabaseConfigured: turn.supabaseConfigured,
220561
220728
  supabase: turn.supabase,
220729
+ cliServerAppUrl: turn.cliServerAppUrl ?? null,
220730
+ cliServerAccessToken: turn.cliServerAccessToken ?? null,
220562
220731
  marketDeskProxyAppUrl: turn.marketDeskProxyAppUrl ?? null,
220563
220732
  marketDeskProxyAccessToken: turn.marketDeskProxyAccessToken ?? null,
220564
220733
  onEvent: onEvent ?? deps.onEvent,
@@ -221925,7 +222094,12 @@ async function ensureLocalSourceIndexed(input) {
221925
222094
  contentHash: null,
221926
222095
  chunksBuilt: 0,
221927
222096
  chunksUpserted: 0,
221928
- embeddingConfigured: getEmbeddingProviderStatus().configured,
222097
+ sourceChunksUpserted: 0,
222098
+ retrievalChunksBuilt: 0,
222099
+ retrievalChunksUpserted: 0,
222100
+ embeddedChunksUpserted: 0,
222101
+ vectorReady: false,
222102
+ embeddingConfigured: isBrowserRuntime() ? true : getEmbeddingProviderStatus().configured,
221929
222103
  errors: [],
221930
222104
  warnings: []
221931
222105
  };
@@ -221958,7 +222132,12 @@ async function ensureLocalSourceIndexed(input) {
221958
222132
  contentHash,
221959
222133
  chunksBuilt: 0,
221960
222134
  chunksUpserted: 0,
221961
- embeddingConfigured: getEmbeddingProviderStatus().configured,
222135
+ sourceChunksUpserted: 0,
222136
+ retrievalChunksBuilt: 0,
222137
+ retrievalChunksUpserted: 0,
222138
+ embeddedChunksUpserted: 0,
222139
+ vectorReady: true,
222140
+ embeddingConfigured: isBrowserRuntime() ? true : getEmbeddingProviderStatus().configured,
221962
222141
  errors: [],
221963
222142
  warnings: []
221964
222143
  };
@@ -222056,12 +222235,15 @@ async function ensureLocalSourceIndexed(input) {
222056
222235
  threadId: input.threadId ?? null,
222057
222236
  chunks: sourceTextChunks
222058
222237
  });
222059
- const embeddingStatus = getEmbeddingProviderStatus();
222060
222238
  let chunksBuilt = textChunks.length;
222061
222239
  let chunksUpserted = 0;
222062
222240
  const warnings = [...extracted.truncated ? ["Source text was truncated before indexing."] : []];
222063
- if (!embeddingStatus.configured) {
222064
- warnings.push(embeddingStatus.message);
222241
+ let embeddingProvider;
222242
+ try {
222243
+ embeddingProvider = input.embeddingProvider ?? createDefaultLocalSourceEmbeddingProvider();
222244
+ } catch (error) {
222245
+ const message = error instanceof Error ? error.message : String(error);
222246
+ warnings.push(message);
222065
222247
  return {
222066
222248
  ok: true,
222067
222249
  status: "indexed",
@@ -222071,38 +222253,69 @@ async function ensureLocalSourceIndexed(input) {
222071
222253
  contentHash: indexedContentHash,
222072
222254
  chunksBuilt,
222073
222255
  chunksUpserted: 0,
222256
+ sourceChunksUpserted: sourceTextChunks.length,
222257
+ retrievalChunksBuilt: chunksBuilt,
222258
+ retrievalChunksUpserted: 0,
222259
+ embeddedChunksUpserted: 0,
222260
+ vectorReady: false,
222261
+ embeddingConfigured: false,
222262
+ errors: [],
222263
+ warnings
222264
+ };
222265
+ }
222266
+ let indexResult;
222267
+ try {
222268
+ const indexer = createRetrievalIndexer(input.supabase, { embeddingProvider });
222269
+ indexResult = pdfPages ? await indexer.indexLocalFilePages({
222270
+ workspaceId: input.workspaceId,
222271
+ sourceId: source.id,
222272
+ fileName: source.file_name,
222273
+ fileType: extracted.fileType,
222274
+ mimeType: source.mime_type,
222275
+ provenanceType: "desktop_local",
222276
+ contentHash: indexedContentHash,
222277
+ threadId: input.threadId ?? null,
222278
+ pages: pdfPages,
222279
+ blocks: pdfBlocks,
222280
+ tables: pdfTables,
222281
+ facts: factPageEntries.length ? factPageEntries : void 0
222282
+ }) : await indexer.indexLocalFileText({
222283
+ workspaceId: input.workspaceId,
222284
+ sourceId: source.id,
222285
+ fileName: source.file_name,
222286
+ fileType: extracted.fileType,
222287
+ mimeType: source.mime_type,
222288
+ provenanceType: "desktop_local",
222289
+ contentHash: indexedContentHash,
222290
+ threadId: input.threadId ?? null,
222291
+ text: extracted.text
222292
+ });
222293
+ } catch (error) {
222294
+ if (!isEmbeddingProviderNotConfigured(error)) throw error;
222295
+ warnings.push(error.message);
222296
+ return {
222297
+ ok: true,
222298
+ status: "indexed",
222299
+ localSourceId: input.localSourceId,
222300
+ supabaseSourceId: source.id,
222301
+ retrievalSourceId: null,
222302
+ contentHash: indexedContentHash,
222303
+ chunksBuilt,
222304
+ chunksUpserted: 0,
222305
+ sourceChunksUpserted: sourceTextChunks.length,
222306
+ retrievalChunksBuilt: chunksBuilt,
222307
+ retrievalChunksUpserted: 0,
222308
+ embeddedChunksUpserted: 0,
222309
+ vectorReady: false,
222074
222310
  embeddingConfigured: false,
222075
222311
  errors: [],
222076
222312
  warnings
222077
222313
  };
222078
222314
  }
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
222315
  chunksBuilt = indexResult.chunksBuilt;
222105
222316
  chunksUpserted = indexResult.chunksUpserted;
222317
+ const embeddedChunksUpserted = indexResult.chunksEmbedded;
222318
+ const vectorReady = embeddedChunksUpserted > 0;
222106
222319
  if (!indexResult.ok) {
222107
222320
  return {
222108
222321
  ok: false,
@@ -222113,6 +222326,11 @@ async function ensureLocalSourceIndexed(input) {
222113
222326
  contentHash: indexedContentHash,
222114
222327
  chunksBuilt,
222115
222328
  chunksUpserted,
222329
+ sourceChunksUpserted: sourceTextChunks.length,
222330
+ retrievalChunksBuilt: indexResult.chunksBuilt,
222331
+ retrievalChunksUpserted: indexResult.chunksUpserted,
222332
+ embeddedChunksUpserted,
222333
+ vectorReady,
222116
222334
  embeddingConfigured: true,
222117
222335
  errors: indexResult.errors,
222118
222336
  warnings: [...warnings, ...indexResult.warnings]
@@ -222123,10 +222341,15 @@ async function ensureLocalSourceIndexed(input) {
222123
222341
  status: "indexed",
222124
222342
  localSourceId: input.localSourceId,
222125
222343
  supabaseSourceId: source.id,
222126
- retrievalSourceId: source.id,
222344
+ retrievalSourceId: vectorReady ? source.id : null,
222127
222345
  contentHash: indexedContentHash,
222128
222346
  chunksBuilt,
222129
222347
  chunksUpserted,
222348
+ sourceChunksUpserted: sourceTextChunks.length,
222349
+ retrievalChunksBuilt: indexResult.chunksBuilt,
222350
+ retrievalChunksUpserted: indexResult.chunksUpserted,
222351
+ embeddedChunksUpserted,
222352
+ vectorReady,
222130
222353
  embeddingConfigured: true,
222131
222354
  errors: [],
222132
222355
  warnings
@@ -222142,6 +222365,12 @@ async function ensureLocalSourceIndexed(input) {
222142
222365
  };
222143
222366
  }
222144
222367
  }
222368
+ function createDefaultLocalSourceEmbeddingProvider() {
222369
+ return isBrowserRuntime() ? createBrowserEmbeddingProvider() : createEmbeddingProviderFromEnv();
222370
+ }
222371
+ function isBrowserRuntime() {
222372
+ return typeof window !== "undefined";
222373
+ }
222145
222374
  async function resolveRetrievalSourceIdForTurn(input) {
222146
222375
  const selected = input.selectedSourceId?.trim() ?? null;
222147
222376
  if (!selected) return { retrievalSourceId: null, indexOutcome: null };
@@ -222417,7 +222646,11 @@ async function loadFolderIndexSummary(input) {
222417
222646
  }
222418
222647
  const files = input.localSources.filter((entry) => entry.rootId === input.rootId && isAutoIndexableLocalSource(entry)).map((entry) => {
222419
222648
  const row = rowByLocalId.get(entry.localSourceId) ?? null;
222420
- const retrievalReady = row ? indexSets.retrievalChunkIds.has(row.id) : false;
222649
+ const sourceChunkCount2 = row ? indexSets.sourceChunkCountsBySourceId.get(row.id) ?? 0 : 0;
222650
+ const retrievalChunkCount2 = row ? indexSets.retrievalChunkCountsBySourceId.get(row.id) ?? 0 : 0;
222651
+ const embeddedChunkCount2 = row ? indexSets.embeddedRetrievalChunkCountsBySourceId.get(row.id) ?? 0 : 0;
222652
+ const retrievalReady = retrievalChunkCount2 > 0;
222653
+ const vectorReady = embeddedChunkCount2 > 0;
222421
222654
  const stale = row ? Date.parse(entry.modifiedAt) > Date.parse(row.updated_at) : true;
222422
222655
  return {
222423
222656
  localSourceId: entry.localSourceId,
@@ -222426,19 +222659,30 @@ async function loadFolderIndexSummary(input) {
222426
222659
  mimeType: entry.mimeType,
222427
222660
  modifiedAt: entry.modifiedAt,
222428
222661
  sizeBytes: entry.sizeBytes,
222429
- status: row ? retrievalReady ? "indexed" : "pending" : "pending",
222662
+ status: row ? sourceChunkCount2 > 0 || retrievalReady ? "indexed" : "pending" : "pending",
222430
222663
  supabaseSourceId: row?.id ?? null,
222431
222664
  retrievalReady,
222665
+ vectorReady,
222666
+ sourceChunkCount: sourceChunkCount2,
222667
+ retrievalChunkCount: retrievalChunkCount2,
222668
+ embeddedChunkCount: embeddedChunkCount2,
222432
222669
  contentHash: row?.content_hash ?? null,
222433
222670
  message: stale && row ? "Changed since last indexing pass." : void 0
222434
222671
  };
222435
222672
  });
222436
222673
  const staleFiles = files.filter((file) => file.message?.includes("Changed since last indexing pass")).length;
222437
222674
  const retrievalReadySourceIds = files.filter((file) => file.retrievalReady && file.supabaseSourceId).map((file) => file.supabaseSourceId);
222675
+ const vectorReadySourceIds = files.filter((file) => file.vectorReady && file.supabaseSourceId).map((file) => file.supabaseSourceId);
222438
222676
  const indexedFiles = files.filter((file) => file.status === "indexed").length;
222677
+ const sourceChunkCount = sumFiles(files, "sourceChunkCount");
222678
+ const retrievalChunkCount = sumFiles(files, "retrievalChunkCount");
222679
+ const embeddedChunkCount = sumFiles(files, "embeddedChunkCount");
222439
222680
  const lastIndexedAt = rows[0]?.updated_at ?? null;
222440
222681
  const diagnostics = [
222682
+ rows.length > 0 ? `${rows.length} registered source(s).` : null,
222683
+ sourceChunkCount > 0 ? `${sourceChunkCount} text/source chunk(s) available for keyword search.` : null,
222441
222684
  retrievalReadySourceIds.length > 0 ? `${retrievalReadySourceIds.length} file(s) have fresh retrieval chunks.` : "No retrieval-ready files yet for this folder.",
222685
+ 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
222686
  staleFiles > 0 ? `${staleFiles} file(s) changed since the last index update.` : null
222443
222687
  ].filter(Boolean);
222444
222688
  return {
@@ -222448,8 +222692,14 @@ async function loadFolderIndexSummary(input) {
222448
222692
  indexedFiles,
222449
222693
  skippedFiles: 0,
222450
222694
  failedFiles: 0,
222695
+ registeredSources: rows.length,
222696
+ sourceChunkCount,
222697
+ sourceChunkFiles: files.filter((file) => file.sourceChunkCount > 0).length,
222698
+ retrievalChunkCount,
222451
222699
  staleFiles,
222452
222700
  retrievalReadyFiles: retrievalReadySourceIds.length,
222701
+ embeddedChunkCount,
222702
+ vectorReadyFiles: vectorReadySourceIds.length,
222453
222703
  filesNeedingOcr: files.filter((file) => needsOcrHint(file)).length,
222454
222704
  lastIndexedAt,
222455
222705
  status: files.length === 0 ? "idle" : staleFiles > 0 || indexedFiles < files.length ? "partial" : "ready",
@@ -222457,6 +222707,7 @@ async function loadFolderIndexSummary(input) {
222457
222707
  localOnlyEmbeddingsEnabled: false,
222458
222708
  remoteIndexArtifacts: [...DEFAULT_REMOTE_INDEX_ARTIFACTS],
222459
222709
  retrievalReadySourceIds,
222710
+ vectorReadySourceIds,
222460
222711
  diagnostics,
222461
222712
  manifestPath: DEFAULT_MANIFEST_PATH,
222462
222713
  files
@@ -222476,6 +222727,9 @@ function needsOcrHint(file) {
222476
222727
  const message = file.message?.toLowerCase() ?? "";
222477
222728
  return message.includes("ocr") || message.includes("scanned") || message.includes("no readable text");
222478
222729
  }
222730
+ function sumFiles(files, field) {
222731
+ return files.reduce((total, file) => total + file[field], 0);
222732
+ }
222479
222733
  var DEFAULT_MANIFEST_PATH, AUTO_INDEX_EXCLUDED_PATH_PREFIXES, AUTO_INDEX_EXCLUDED_EXACT_PATHS, DEFAULT_STORAGE_MODE, DEFAULT_REMOTE_INDEX_ARTIFACTS;
222480
222734
  var init_folderIndexing = __esm({
222481
222735
  "features/perchTerminal/runtime/folderIndexing.ts"() {
@@ -224731,6 +224985,54 @@ var init_objectiveClassifier = __esm({
224731
224985
  });
224732
224986
 
224733
224987
  // features/perchTerminal/runtime/turn/runOperatorTurn.ts
224988
+ function buildTurnMemoryRetriever(input) {
224989
+ if (input.supabase) {
224990
+ return async ({ userId, workspaceId, limitPerScope, query }) => createPermanentMemoryPersistence(
224991
+ input.supabase
224992
+ ).retrievePermanentMemoriesForTurn({
224993
+ userId,
224994
+ workspaceId,
224995
+ limitPerScope,
224996
+ query
224997
+ });
224998
+ }
224999
+ const appUrl = input.cliServerAppUrl?.trim();
225000
+ const accessToken = input.cliServerAccessToken?.trim();
225001
+ if (!appUrl || !accessToken) return null;
225002
+ return createCliServerMemoryRetriever({
225003
+ appUrl,
225004
+ accessToken,
225005
+ threadId: input.threadId
225006
+ });
225007
+ }
225008
+ function createCliServerMemoryRetriever(input) {
225009
+ return async ({ limitPerScope, query }) => {
225010
+ const response = await fetch(`${input.appUrl.replace(/\/+$/, "")}/api/perch-terminal/cli-context`, {
225011
+ method: "POST",
225012
+ headers: {
225013
+ Accept: "application/json",
225014
+ "Content-Type": "application/json",
225015
+ Authorization: `Bearer ${input.accessToken}`
225016
+ },
225017
+ body: JSON.stringify({
225018
+ query,
225019
+ threadId: input.threadId,
225020
+ limitPerScope
225021
+ })
225022
+ });
225023
+ const payload = await response.json().catch(() => ({}));
225024
+ if (!response.ok || payload.ok !== true) {
225025
+ throw new Error("Durable memory is unavailable right now.");
225026
+ }
225027
+ const memories = Array.isArray(payload.permanentMemories) ? payload.permanentMemories.filter(isPermanentMemoryLike) : [];
225028
+ return memories;
225029
+ };
225030
+ }
225031
+ function isPermanentMemoryLike(value) {
225032
+ if (!value || typeof value !== "object") return false;
225033
+ const memory = value;
225034
+ 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");
225035
+ }
224734
225036
  async function runOperatorTurn(input, deps) {
224735
225037
  const runId = input.clientRunId?.trim() || makeRunId();
224736
225038
  const startMs = Date.now();
@@ -224796,21 +225098,15 @@ async function runOperatorTurn(input, deps) {
224796
225098
  });
224797
225099
  }
224798
225100
  try {
225101
+ const memoryRetriever = buildTurnMemoryRetriever(input);
224799
225102
  const memoryContext = await buildMemoryContext({
224800
225103
  query: input.trimmedInput,
224801
- userId: input.userId,
225104
+ userId: input.userId ?? (memoryRetriever && input.cliServerAccessToken?.trim() ? "cli_authenticated_user" : null),
224802
225105
  workspaceId: input.workspaceId,
224803
225106
  threadId: input.threadId,
224804
225107
  permanentMemories: input.permanentMemories,
224805
225108
  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
225109
+ retriever: memoryRetriever
224814
225110
  });
224815
225111
  const earlySession = input.threadId ? await loadThreadSession(input.threadId, { supabase: input.supabase ?? null }) : null;
224816
225112
  const persona = getPersona(input.personaId ?? earlySession?.personaId);
@@ -225098,8 +225394,15 @@ ${planStateLines}`
225098
225394
  if (perchMdRow?.status === "not_found") {
225099
225395
  extendedWarnings.push("No PERCH.md \u2014 add an operator playbook to .perch/PERCH.md to customise behaviour.");
225100
225396
  }
225101
- if (!enrichedInput.supabase) {
225102
- extendedWarnings.push("Memory retrieval skipped \u2014 no Supabase session available.");
225397
+ const hostedMemoryAvailable = Boolean(
225398
+ enrichedInput.cliServerAppUrl?.trim() && enrichedInput.cliServerAccessToken?.trim()
225399
+ );
225400
+ const memoryDiagnostics = enrichedInput.memoryContext?.diagnostics ?? null;
225401
+ if (memoryDiagnostics?.warnings.length) {
225402
+ extendedWarnings.push(...memoryDiagnostics.warnings);
225403
+ }
225404
+ if (!enrichedInput.supabase && !hostedMemoryAvailable && memoryDiagnostics?.retrieval.attempted !== true) {
225405
+ extendedWarnings.push("Memory retrieval skipped \u2014 sign in to use durable memory.");
225103
225406
  }
225104
225407
  const contextJob = resolveJobContextTokens({
225105
225408
  threadContextTokens: threadContextAccounting.threadContextTokens,
@@ -226511,6 +226814,21 @@ function installCliNodeLocalBridge(input) {
226511
226814
  }
226512
226815
  };
226513
226816
  }
226817
+ function readCliProjectMemoryState(workspaceRoot) {
226818
+ const root2 = path11.resolve(expandHome4(workspaceRoot));
226819
+ if (!isCliProjectMemoryAvailable(root2)) {
226820
+ return {
226821
+ available: false,
226822
+ meta: null,
226823
+ reason: "Local project memory is unavailable in this folder."
226824
+ };
226825
+ }
226826
+ return {
226827
+ available: true,
226828
+ meta: enrichCliProjectMeta(root2, loadCliStoredMeta(root2)),
226829
+ reason: "Local project memory is available."
226830
+ };
226831
+ }
226514
226832
  function createCliNodeLocalBridge(input) {
226515
226833
  const workspaceRoot = path11.resolve(input.workspaceRoot);
226516
226834
  const now13 = () => (/* @__PURE__ */ new Date()).toISOString();
@@ -226793,10 +227111,20 @@ function createCliNodeLocalBridge(input) {
226793
227111
  encoding: "base64"
226794
227112
  };
226795
227113
  },
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." }),
227114
+ getProjectRules: async () => {
227115
+ const state = readCliProjectMemoryState(workspaceRoot);
227116
+ return state.meta ? [{
227117
+ rootId: CLI_ROOT_ID,
227118
+ perchMd: state.meta.perchMd?.content ?? null,
227119
+ rules: state.meta.rules.map((rule) => ({
227120
+ fileName: rule.fileName,
227121
+ content: rule.content
227122
+ }))
227123
+ }] : [];
227124
+ },
227125
+ readProjectMemory: async () => readCliProjectMemoryBridge(workspaceRoot),
227126
+ writeProjectMemory: async (request) => writeCliProjectMemory(workspaceRoot, request.meta),
227127
+ writeMemoryFile: async (request) => writeCliMemoryFile(workspaceRoot, request),
226800
227128
  writeRule: async () => ({ ok: false, error: "Project rule writes are not available in this CLI package yet." }),
226801
227129
  writePerchMd: async () => ({ ok: false, error: "PERCH.md writes are not available in this CLI package yet." }),
226802
227130
  readGlobalPerchMd: async () => ({ ok: false, error: "Global PERCH.md is not available in this CLI package yet." }),
@@ -227075,6 +227403,292 @@ function guessCliFileType(filePath) {
227075
227403
  if ([".doc", ".docx", ".txt", ".md", ".rtf"].includes(ext)) return "document";
227076
227404
  return "unknown";
227077
227405
  }
227406
+ async function readCliProjectMemoryBridge(workspaceRoot) {
227407
+ const state = readCliProjectMemoryState(workspaceRoot);
227408
+ if (!state.available || !state.meta) {
227409
+ return {
227410
+ ok: false,
227411
+ error: "Local project memory is unavailable in this folder. Run from a project folder with .perch memory or sign in for durable memory."
227412
+ };
227413
+ }
227414
+ return { ok: true, meta: state.meta };
227415
+ }
227416
+ async function writeCliProjectMemory(workspaceRoot, patch) {
227417
+ const root2 = path11.resolve(expandHome4(workspaceRoot));
227418
+ if (!isCliProjectMemoryAvailable(root2)) {
227419
+ return {
227420
+ ok: false,
227421
+ error: "Local project memory is unavailable in this folder. Run from a project folder with .perch memory or sign in for durable memory."
227422
+ };
227423
+ }
227424
+ const current = loadCliStoredMeta(root2);
227425
+ const next = {
227426
+ ...current,
227427
+ projectName: patch.projectName !== void 0 ? patch.projectName : current.projectName,
227428
+ notes: patch.notes !== void 0 ? patch.notes : current.notes,
227429
+ preferredRoot: patch.preferredRoot !== void 0 ? patch.preferredRoot : current.preferredRoot,
227430
+ lastOpenedThreadId: patch.lastOpenedThreadId !== void 0 ? patch.lastOpenedThreadId : current.lastOpenedThreadId,
227431
+ memorySummary: patch.memorySummary !== void 0 ? patch.memorySummary : current.memorySummary
227432
+ };
227433
+ await fsp.mkdir(path11.join(root2, PERCH_DIR), { recursive: true });
227434
+ await atomicWriteCliUtf8(getCliProjectFilePath(root2), JSON.stringify(next, null, 2));
227435
+ return { ok: true, meta: enrichCliProjectMeta(root2, next) };
227436
+ }
227437
+ async function writeCliMemoryFile(workspaceRoot, request) {
227438
+ const root2 = path11.resolve(expandHome4(workspaceRoot));
227439
+ if (!isCliProjectMemoryAvailable(root2)) {
227440
+ return {
227441
+ ok: false,
227442
+ error: "Local project memory is unavailable in this folder. Run from a project folder with .perch memory or sign in for durable memory."
227443
+ };
227444
+ }
227445
+ if (request.rootId && request.rootId !== CLI_ROOT_ID) {
227446
+ return { ok: false, error: "Local project memory is unavailable for that workspace root." };
227447
+ }
227448
+ const fileName = normalizeCliMemoryFileName(request.fileName);
227449
+ if (!fileName) return { ok: false, error: "Invalid memory file name." };
227450
+ const relativePath = path11.join(PERCH_DIR, MEMORY_DIR, fileName);
227451
+ const fullPath = path11.join(root2, relativePath);
227452
+ const existing = await fsp.readFile(fullPath, "utf8").catch(() => "");
227453
+ let nextContent;
227454
+ try {
227455
+ nextContent = applyCliMemoryWrite(existing, request);
227456
+ } catch (error) {
227457
+ return {
227458
+ ok: false,
227459
+ error: error instanceof Error ? error.message : "Failed to merge memory content."
227460
+ };
227461
+ }
227462
+ const capError = assertCliMemoryWithinCap(nextContent);
227463
+ if (capError) return { ok: false, error: capError };
227464
+ await fsp.mkdir(path11.dirname(fullPath), { recursive: true });
227465
+ await atomicWriteCliUtf8(fullPath, nextContent);
227466
+ const updatedFile = readCliTextMemoryFile(root2, relativePath, MAX_MEMORY_BYTES);
227467
+ return {
227468
+ ok: true,
227469
+ meta: enrichCliProjectMeta(root2, loadCliStoredMeta(root2)),
227470
+ updatedFile
227471
+ };
227472
+ }
227473
+ function defaultCliProjectMeta() {
227474
+ return {
227475
+ projectName: null,
227476
+ notes: null,
227477
+ preferredRoot: null,
227478
+ lastOpenedThreadId: null,
227479
+ memorySummary: null,
227480
+ perchMd: null,
227481
+ rules: [],
227482
+ memoryFiles: [],
227483
+ missingMemoryFiles: [],
227484
+ projectMemoryEmpty: true,
227485
+ evidenceOnly: true
227486
+ };
227487
+ }
227488
+ function isCliProjectMemoryAvailable(root2) {
227489
+ try {
227490
+ return fs10.statSync(path11.join(root2, PERCH_DIR)).isDirectory();
227491
+ } catch {
227492
+ return false;
227493
+ }
227494
+ }
227495
+ function getCliProjectFilePath(root2) {
227496
+ return path11.join(root2, PERCH_DIR, PROJECT_FILE);
227497
+ }
227498
+ function loadCliStoredMeta(root2) {
227499
+ try {
227500
+ const raw = fs10.readFileSync(getCliProjectFilePath(root2), "utf8");
227501
+ return { ...defaultCliProjectMeta(), ...JSON.parse(raw) };
227502
+ } catch {
227503
+ return defaultCliProjectMeta();
227504
+ }
227505
+ }
227506
+ function enrichCliProjectMeta(root2, base) {
227507
+ const perchMd = readCliFirstExisting(root2, [
227508
+ path11.join(PERCH_DIR, PERCH_INDEX_FILE),
227509
+ PERCH_INDEX_FILE
227510
+ ], MAX_TEXT_BYTES);
227511
+ const rules = readCliRules(root2);
227512
+ const { memoryFiles, missingMemoryFiles } = readCliMemoryFiles(root2);
227513
+ const projectMemoryEmpty = !perchMd?.found && rules.length === 0 && memoryFiles.filter((file) => file.found).length === 0;
227514
+ return {
227515
+ ...base,
227516
+ perchMd,
227517
+ rules,
227518
+ memoryFiles,
227519
+ missingMemoryFiles,
227520
+ projectMemoryEmpty,
227521
+ evidenceOnly: projectMemoryEmpty
227522
+ };
227523
+ }
227524
+ function normalizeCliMemoryFileName(fileName) {
227525
+ if (typeof fileName !== "string") return null;
227526
+ const trimmed = fileName.trim();
227527
+ if (trimmed.includes("..") || trimmed.includes("/") || trimmed.includes("\\")) return null;
227528
+ return EXPECTED_MEMORY_FILES.includes(trimmed) ? trimmed : null;
227529
+ }
227530
+ function applyCliMemoryWrite(existing, request) {
227531
+ const incoming = request.content ?? "";
227532
+ if (request.mode === "replace") return incoming;
227533
+ if (request.mode === "append") {
227534
+ if (request.sectionHeading?.trim()) {
227535
+ return upsertCliMarkdownSection(existing, request.sectionHeading, incoming, "append");
227536
+ }
227537
+ if (!existing.trim()) return incoming;
227538
+ return `${existing}${existing.endsWith("\n") ? "" : "\n"}${incoming}`;
227539
+ }
227540
+ if (!request.sectionHeading?.trim()) {
227541
+ throw new Error('mode "merge" requires sectionHeading.');
227542
+ }
227543
+ return upsertCliMarkdownSection(existing, request.sectionHeading, incoming, "replace");
227544
+ }
227545
+ function upsertCliMarkdownSection(existing, sectionHeading, body, mode) {
227546
+ const heading = sectionHeading.trim().startsWith("#") ? sectionHeading.trim() : `## ${sectionHeading.trim()}`;
227547
+ const targetKey = normalizeCliHeadingKey(heading);
227548
+ const sections = parseCliMarkdownSections(existing);
227549
+ const index = sections.findIndex((section) => normalizeCliHeadingKey(section.heading) === targetKey);
227550
+ if (index >= 0) {
227551
+ const prior = sections[index];
227552
+ sections[index] = {
227553
+ heading: prior.heading || heading,
227554
+ body: mode === "append" && prior.body.trim() ? `${prior.body.trim()}
227555
+ ${body.trim()}` : body.trim()
227556
+ };
227557
+ } else {
227558
+ sections.push({ heading, body: body.trim() });
227559
+ }
227560
+ return sections.map((section) => {
227561
+ if (!section.heading) return section.body.trim();
227562
+ return `${section.heading}
227563
+
227564
+ ${section.body.trim()}`.trim();
227565
+ }).filter(Boolean).join("\n\n").concat("\n");
227566
+ }
227567
+ function parseCliMarkdownSections(content) {
227568
+ const sections = [];
227569
+ let current = null;
227570
+ for (const line of content.split(/\r?\n/)) {
227571
+ if (/^#{1,6}\s+/.test(line)) {
227572
+ if (current) sections.push(current);
227573
+ current = { heading: line.trimEnd(), body: "" };
227574
+ continue;
227575
+ }
227576
+ if (!current) {
227577
+ if (!line.trim()) continue;
227578
+ current = { heading: "", body: line };
227579
+ continue;
227580
+ }
227581
+ current.body = current.body ? `${current.body}
227582
+ ${line}` : line;
227583
+ }
227584
+ if (current) sections.push(current);
227585
+ return sections;
227586
+ }
227587
+ function normalizeCliHeadingKey(heading) {
227588
+ return heading.replace(/^#+\s*/, "").trim().toLowerCase();
227589
+ }
227590
+ function assertCliMemoryWithinCap(content) {
227591
+ return Buffer.byteLength(content, "utf8") > MAX_MEMORY_BYTES ? "Memory file exceeds 128KB cap." : null;
227592
+ }
227593
+ async function atomicWriteCliUtf8(filePath, content) {
227594
+ const tmpPath = `${filePath}.tmp`;
227595
+ await fsp.writeFile(tmpPath, content, "utf8");
227596
+ try {
227597
+ await fsp.rename(tmpPath, filePath);
227598
+ } catch (error) {
227599
+ await fsp.rm(tmpPath, { force: true }).catch(() => void 0);
227600
+ throw error;
227601
+ }
227602
+ }
227603
+ function readCliFirstExisting(root2, relativePaths, maxBytes) {
227604
+ for (const relativePath of relativePaths) {
227605
+ const file = readCliTextMemoryFile(root2, relativePath, maxBytes);
227606
+ if (file.found) {
227607
+ return {
227608
+ relativePath: file.relativePath,
227609
+ content: file.content,
227610
+ found: true,
227611
+ sizeBytes: file.sizeBytes,
227612
+ modifiedAt: file.modifiedAt
227613
+ };
227614
+ }
227615
+ }
227616
+ return null;
227617
+ }
227618
+ function readCliRules(root2) {
227619
+ const dirPath = path11.join(root2, PERCH_DIR, RULES_DIR);
227620
+ try {
227621
+ 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) => ({
227622
+ fileName: path11.basename(file.relativePath),
227623
+ relativePath: file.relativePath,
227624
+ content: file.content,
227625
+ sizeBytes: file.sizeBytes,
227626
+ modifiedAt: file.modifiedAt
227627
+ }));
227628
+ } catch {
227629
+ return [];
227630
+ }
227631
+ }
227632
+ function readCliMemoryFiles(root2) {
227633
+ const memoryDir = path11.join(root2, PERCH_DIR, MEMORY_DIR);
227634
+ const discovered = /* @__PURE__ */ new Set();
227635
+ const memoryFiles = [];
227636
+ try {
227637
+ for (const name of fs10.readdirSync(memoryDir).slice(0, MAX_MEMORY_FILES)) {
227638
+ const ext = path11.extname(name).toLowerCase();
227639
+ if (ext !== ".md" && ext !== ".txt") continue;
227640
+ discovered.add(name);
227641
+ memoryFiles.push(readCliTextMemoryFile(root2, path11.join(PERCH_DIR, MEMORY_DIR, name), MAX_MEMORY_BYTES));
227642
+ }
227643
+ } catch {
227644
+ }
227645
+ return {
227646
+ memoryFiles,
227647
+ missingMemoryFiles: EXPECTED_MEMORY_FILES.filter((name) => !discovered.has(name)).map((name) => ({
227648
+ fileName: name,
227649
+ relativePath: path11.join(PERCH_DIR, MEMORY_DIR, name),
227650
+ reason: "expected project memory file was not found"
227651
+ }))
227652
+ };
227653
+ }
227654
+ function readCliTextMemoryFile(root2, relativePath, maxBytes) {
227655
+ const fullPath = path11.join(root2, relativePath);
227656
+ const fileName = path11.basename(relativePath);
227657
+ try {
227658
+ const stat2 = fs10.statSync(fullPath);
227659
+ if (!stat2.isFile()) {
227660
+ return { fileName, relativePath, content: "", found: false, error: "not a regular file" };
227661
+ }
227662
+ const content = fs10.readFileSync(fullPath, "utf8");
227663
+ if (stat2.size > maxBytes) {
227664
+ return {
227665
+ fileName,
227666
+ relativePath,
227667
+ content: `${content.slice(0, maxBytes)}
227668
+ [truncated: file exceeds ${maxBytes} bytes]`,
227669
+ found: true,
227670
+ sizeBytes: stat2.size,
227671
+ modifiedAt: stat2.mtime.toISOString()
227672
+ };
227673
+ }
227674
+ return {
227675
+ fileName,
227676
+ relativePath,
227677
+ content,
227678
+ found: true,
227679
+ sizeBytes: stat2.size,
227680
+ modifiedAt: stat2.mtime.toISOString()
227681
+ };
227682
+ } catch (error) {
227683
+ return {
227684
+ fileName,
227685
+ relativePath,
227686
+ content: "",
227687
+ found: false,
227688
+ error: error instanceof Error ? error.message : "read failed"
227689
+ };
227690
+ }
227691
+ }
227078
227692
  async function runShellCommand(input) {
227079
227693
  const startedAt = Date.now();
227080
227694
  const cwd2 = input.cwd ? resolveReadPath(input.workspaceRoot, input.cwd) : input.workspaceRoot;
@@ -227312,7 +227926,7 @@ function capOutput(stdout, stderr) {
227312
227926
  function shellQuote(parts) {
227313
227927
  return parts.map((part) => `'${part.replace(/'/g, "'\\''")}'`).join(" ");
227314
227928
  }
227315
- var CLI_ROOT_ID, DEFAULT_MAX_RESULTS, MAX_READ_BYTES, IGNORED_DIRS;
227929
+ 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
227930
  var init_nodeLocalBridge = __esm({
227317
227931
  "features/perchTerminal/runtime/cliHost/nodeLocalBridge.ts"() {
227318
227932
  "use strict";
@@ -227323,6 +227937,20 @@ var init_nodeLocalBridge = __esm({
227323
227937
  DEFAULT_MAX_RESULTS = 200;
227324
227938
  MAX_READ_BYTES = 2e6;
227325
227939
  IGNORED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", "release"]);
227940
+ PERCH_DIR = ".perch";
227941
+ PROJECT_FILE = "project.json";
227942
+ PERCH_INDEX_FILE = "PERCH.md";
227943
+ MEMORY_DIR = "memory";
227944
+ RULES_DIR = "rules";
227945
+ MAX_TEXT_BYTES = 64e3;
227946
+ MAX_MEMORY_BYTES = 128e3;
227947
+ MAX_MEMORY_FILES = 80;
227948
+ EXPECTED_MEMORY_FILES = [
227949
+ "project.md",
227950
+ "preferences.md",
227951
+ "decisions.md",
227952
+ "permanent.md"
227953
+ ];
227326
227954
  }
227327
227955
  });
227328
227956
 
@@ -227404,10 +228032,13 @@ function buildCliTurnInput(input, resolved) {
227404
228032
  activeRootPath: input.activeRootPath ?? resolved.cwd,
227405
228033
  localSources: [],
227406
228034
  localSourcesMeta: null,
228035
+ projectMeta: readCliProjectMemoryState(resolved.cwd).meta,
227407
228036
  permanentMemories: input.permanentMemories ?? [],
227408
228037
  userMemories: input.userMemories ?? [],
227409
228038
  supabase: null,
227410
228039
  supabaseConfigured: false,
228040
+ cliServerAppUrl: input.cliServerAppUrl ?? input.marketDeskProxyAppUrl ?? input.appUrl ?? null,
228041
+ cliServerAccessToken: input.cliServerAccessToken ?? input.marketDeskProxyAccessToken ?? null,
227411
228042
  marketDeskProxyAppUrl: input.marketDeskProxyAppUrl ?? input.appUrl ?? null,
227412
228043
  marketDeskProxyAccessToken: input.marketDeskProxyAccessToken ?? null,
227413
228044
  permissionMode: normalizePermissionMode(input.permissionMode ?? "default"),
@@ -227483,6 +228114,7 @@ var init_runCliTurn = __esm({
227483
228114
  init_personaRegistry();
227484
228115
  init_runOperatorTurn();
227485
228116
  init_nodeLocalBridge();
228117
+ init_nodeLocalBridge();
227486
228118
  }
227487
228119
  });
227488
228120
 
@@ -227652,12 +228284,12 @@ import os2 from "node:os";
227652
228284
  import path13 from "node:path";
227653
228285
  import { promisify } from "node:util";
227654
228286
  async function readStoredCliAuthSession() {
227655
- const raw = process.platform === "darwin" ? await readKeychainSecret().catch(() => null) : await readFallbackSecret().catch(() => null);
228287
+ const raw = shouldUseFallbackAuthFile() ? await readFallbackSecret().catch(() => null) : process.platform === "darwin" ? await readKeychainSecret().catch(() => null) : await readFallbackSecret().catch(() => null);
227656
228288
  return parseStoredCliAuthSession(raw);
227657
228289
  }
227658
228290
  async function writeStoredCliAuthSession(session) {
227659
228291
  const raw = JSON.stringify(session);
227660
- if (process.platform === "darwin") {
228292
+ if (process.platform === "darwin" && !shouldUseFallbackAuthFile()) {
227661
228293
  await execFileAsync("security", [
227662
228294
  "add-generic-password",
227663
228295
  "-U",
@@ -227673,7 +228305,7 @@ async function writeStoredCliAuthSession(session) {
227673
228305
  await writeFallbackSecret(raw);
227674
228306
  }
227675
228307
  async function clearStoredCliAuthSession() {
227676
- if (process.platform === "darwin") {
228308
+ if (process.platform === "darwin" && !shouldUseFallbackAuthFile()) {
227677
228309
  await execFileAsync("security", [
227678
228310
  "delete-generic-password",
227679
228311
  "-s",
@@ -227685,6 +228317,9 @@ async function clearStoredCliAuthSession() {
227685
228317
  }
227686
228318
  await fs11.rm(fallbackSessionPath(), { force: true }).catch(() => void 0);
227687
228319
  }
228320
+ function shouldUseFallbackAuthFile() {
228321
+ return Boolean(process.env.PERCH_CLI_AUTH_DIR?.trim());
228322
+ }
227688
228323
  function isStoredCliAuthSessionUsable(session, nowSeconds = Math.floor(Date.now() / 1e3)) {
227689
228324
  if (!session?.accessToken?.trim()) return false;
227690
228325
  if (!session.appUrl?.trim()) return false;
@@ -283805,6 +284440,7 @@ function parseInteractiveSlashCommand(input) {
283805
284440
  }
283806
284441
  function renderInteractiveStatus(state, connection, session, workspaceId) {
283807
284442
  const storedAuth = session === void 0 ? renderCliAuthSummary(connection) : isStoredCliAuthSessionUsable(session) ? `signed in${session.email ? ` as ${session.email}` : ""}` : "not signed in";
284443
+ const signedIn = session === void 0 ? isCliModelConnectionReady(connection) : isStoredCliAuthSessionUsable(session);
283808
284444
  const connectionStatus = isCliModelConnectionReady(connection) ? "connected" : "locked \xB7 run /login";
283809
284445
  const color = shouldUseCliColor();
283810
284446
  const lines = [
@@ -283812,6 +284448,7 @@ function renderInteractiveStatus(state, connection, session, workspaceId) {
283812
284448
  ["cwd", state.cwd],
283813
284449
  ["auth", storedAuth],
283814
284450
  ["connection", connectionStatus],
284451
+ ["memory", renderCliMemoryAvailability(state.cwd, workspaceId ?? null, signedIn)],
283815
284452
  ["tools", renderCliCapabilityCount(state, workspaceId ?? null)],
283816
284453
  ["permission", state.permissionMode],
283817
284454
  ["mode", state.chatMode],
@@ -283823,6 +284460,15 @@ function renderInteractiveStatus(state, connection, session, workspaceId) {
283823
284460
  ];
283824
284461
  return lines.map(([key, value]) => `${paint(key.padEnd(11), "muted", color)} ${value}`).join("\n") + "\n";
283825
284462
  }
284463
+ function renderCliMemoryAvailability(cwd2, workspaceId, signedIn) {
284464
+ const local = readCliProjectMemoryState(cwd2).available;
284465
+ const parts = [];
284466
+ if (signedIn && workspaceId) parts.push("server memory");
284467
+ if (local) parts.push("local project memory");
284468
+ if (parts.length) return parts.join(" + ");
284469
+ if (signedIn) return "unavailable until a workspace is available";
284470
+ return "unavailable; sign in or run from a .perch project";
284471
+ }
283826
284472
  function renderCliCapabilityCount(state, workspaceId) {
283827
284473
  const count = getExecutableToolDefinitions({
283828
284474
  surface: "cli",
@@ -284017,7 +284663,21 @@ async function fetchCliHostedContext(connection, input) {
284017
284663
  return {
284018
284664
  userId: typeof context.session?.userId === "string" ? context.session.userId : session.userId ?? null,
284019
284665
  workspaceId: typeof context.session?.workspaceId === "string" ? context.session.workspaceId : null,
284020
- permanentMemories: Array.isArray(context.permanentMemories) ? context.permanentMemories.filter(isPermanentMemoryLike) : []
284666
+ permanentMemories: Array.isArray(context.permanentMemories) ? context.permanentMemories.filter(isPermanentMemoryLike2) : []
284667
+ };
284668
+ }
284669
+ async function fetchCliHostedContextForSession(session) {
284670
+ if (!isStoredCliAuthSessionUsable(session)) return null;
284671
+ const body = await postCliJson(session.appUrl, session, "/api/perch-terminal/cli-context", {
284672
+ query: "/status",
284673
+ threadId: "cli-status",
284674
+ limitPerScope: 1
284675
+ });
284676
+ if (!body || body.ok !== true) return null;
284677
+ const context = body;
284678
+ return {
284679
+ userId: typeof context.session?.userId === "string" ? context.session.userId : session.userId ?? null,
284680
+ workspaceId: typeof context.session?.workspaceId === "string" ? context.session.workspaceId : null
284021
284681
  };
284022
284682
  }
284023
284683
  async function resolveCliMarketDeskProxy(connection) {
@@ -284025,6 +284685,8 @@ async function resolveCliMarketDeskProxy(connection) {
284025
284685
  const session = await readStoredCliAuthSession();
284026
284686
  if (!isStoredCliAuthSessionUsable(session)) return {};
284027
284687
  return {
284688
+ cliServerAppUrl: session.appUrl || connection.appUrl,
284689
+ cliServerAccessToken: session.accessToken,
284028
284690
  marketDeskProxyAppUrl: session.appUrl || connection.appUrl,
284029
284691
  marketDeskProxyAccessToken: session.accessToken
284030
284692
  };
@@ -284069,7 +284731,7 @@ async function postCliJson(appUrl, session, pathname, payload) {
284069
284731
  clearTimeout(timeout);
284070
284732
  }
284071
284733
  }
284072
- function isPermanentMemoryLike(value) {
284734
+ function isPermanentMemoryLike2(value) {
284073
284735
  if (!value || typeof value !== "object") return false;
284074
284736
  const memory = value;
284075
284737
  return typeof memory.id === "string" && typeof memory.title === "string" && typeof memory.body === "string";
@@ -284369,12 +285031,17 @@ async function runAuthCommand(parsed, writer) {
284369
285031
  if (parsed.action === "status") {
284370
285032
  const session = await readStoredCliAuthSession();
284371
285033
  if (isStoredCliAuthSessionUsable(session)) {
284372
- writer.stdout(`Perch CLI ${CLI_PACKAGE_VERSION} \xB7 Signed in${session.email ? ` as ${session.email}` : ""} \xB7 ${session.appUrl}
284373
- `);
285034
+ const hostedContext = await fetchCliHostedContextForSession(session).catch(() => null);
285035
+ writer.stdout([
285036
+ `Perch CLI ${CLI_PACKAGE_VERSION} \xB7 Signed in${session.email ? ` as ${session.email}` : ""} \xB7 ${session.appUrl}`,
285037
+ `Memory: ${renderCliMemoryAvailability(process.cwd(), hostedContext?.workspaceId ?? null, true)}`
285038
+ ].join("\n") + "\n");
284374
285039
  return 0;
284375
285040
  }
284376
- writer.stdout(`Perch CLI ${CLI_PACKAGE_VERSION} \xB7 Not signed in. Run \`perch login\`.
284377
- `);
285041
+ writer.stdout([
285042
+ `Perch CLI ${CLI_PACKAGE_VERSION} \xB7 Not signed in. Run \`perch login\`.`,
285043
+ `Memory: ${renderCliMemoryAvailability(process.cwd(), null, false)}`
285044
+ ].join("\n") + "\n");
284378
285045
  return 2;
284379
285046
  }
284380
285047
  const appUrl = resolveCliAppUrl(parsed.appUrl, DEFAULT_CLI_LOGIN_APP_URL) ?? DEFAULT_CLI_LOGIN_APP_URL;
@@ -284634,6 +285301,7 @@ var init_perch_cli = __esm({
284634
285301
  init_runRegistry();
284635
285302
  init_learningMemory();
284636
285303
  init_toolDefinitions();
285304
+ init_nodeLocalBridge();
284637
285305
  execFileAsync3 = promisify3(execFile3);
284638
285306
  DEFAULT_CLI_LOGIN_APP_URL = "https://app.perchai.app";
284639
285307
  CLI_PACKAGE_VERSION = readCliPackageVersion();