perchai-cli 2.4.18 → 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 +851 -132
  2. package/package.json +1 -1
package/dist/perch.mjs CHANGED
@@ -75900,7 +75900,6 @@ function isTurnAbortedError(error) {
75900
75900
  var TURN_STOPPED_BY_USER_MESSAGE;
75901
75901
  var init_turnAbort = __esm({
75902
75902
  "features/perchTerminal/runtime/turnAbort.ts"() {
75903
- "use strict";
75904
75903
  TURN_STOPPED_BY_USER_MESSAGE = "Turn stopped by user.";
75905
75904
  }
75906
75905
  });
@@ -80933,12 +80932,14 @@ function buildDesktopContextSection(input) {
80933
80932
  "grep",
80934
80933
  "statPath",
80935
80934
  "readLocalFile",
80935
+ "readProjectMemory",
80936
80936
  "listLocalSources",
80937
80937
  "readLocalSourceFile"
80938
80938
  ].filter((name) => enabledToolSet2.has(name));
80939
80939
  const localWriteTools = [
80940
80940
  "writeLocalFile",
80941
80941
  "editLocalFile",
80942
+ "saveToMemory",
80942
80943
  "bash",
80943
80944
  "runBashTerminalCommand",
80944
80945
  "generateAPAuditPacket",
@@ -80950,16 +80951,16 @@ function buildDesktopContextSection(input) {
80950
80951
  return {
80951
80952
  id: "desktop-context",
80952
80953
  lane: "desktop",
80953
- label: "CLI workspace",
80954
+ label: "Terminal workspace",
80954
80955
  content: [
80955
- "## CLI local workspace",
80956
- "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.",
80957
80958
  input.activeRootPath?.trim() ? `Workspace root: ${input.activeRootPath}. Treat relative paths as relative to this root.` : "Workspace root: current terminal directory.",
80958
- "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.",
80959
80960
  localReadTools.length > 0 ? `Read tools: ${localReadTools.join(", ")}.` : "Read tools: none exposed for this turn.",
80960
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."
80961
80962
  ].join("\n"),
80962
- reason: "Terminal local workspace access and CLI tool availability for this turn.",
80963
+ reason: "Terminal workspace and tool availability for this turn.",
80963
80964
  sourcePath: input.activeRootPath,
80964
80965
  metadata: {
80965
80966
  desktopConnected: false,
@@ -81018,8 +81019,14 @@ function buildDesktopContextSection(input) {
81018
81019
  (file) => `- ${file.relativePath} (${file.matchReason})`
81019
81020
  ) : [];
81020
81021
  const hasVisionSupport = input.selectedModelVisionSupport === true;
81021
- const searchHints = input.folderIndexSummary && input.folderIndexSummary.retrievalReadyFiles > 0 ? [
81022
- "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.",
81023
81030
  "Use glob for named files like *Screenshot* or exact extensions like *.png.",
81024
81031
  "Use statPath or glob before claiming a file does not exist.",
81025
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."
@@ -81028,8 +81035,16 @@ function buildDesktopContextSection(input) {
81028
81035
  "Use statPath or glob before claiming a file does not exist.",
81029
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."
81030
81037
  ];
81031
- 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.";
81032
- 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;
81033
81048
  const mcpToolNames = enabledToolSet.has("mcp__") ? [] : Array.from(enabledToolSet).filter((tool) => tool.startsWith("mcp__"));
81034
81049
  const mcpServerNames = Array.from(
81035
81050
  new Set(mcpToolNames.map((tool) => tool.split("__")[1]).filter(Boolean))
@@ -81062,7 +81077,7 @@ function buildDesktopContextSection(input) {
81062
81077
  content: [
81063
81078
  "## Desktop environment",
81064
81079
  "Local workspace access is connected. Local filesystem tools are available.",
81065
- 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).",
81066
81081
  `Visible local sources: ${totalVisibleFiles}`,
81067
81082
  input.localSourcesMeta?.refreshedAt ? `Local source snapshot refreshed: ${input.localSourcesMeta.refreshedAt}` : null,
81068
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,
@@ -81079,7 +81094,7 @@ function buildDesktopContextSection(input) {
81079
81094
  availableReadTools.length > 0 ? `Read tools: ${availableReadTools.join(", ")}.` : "Read tools: none exposed for this turn.",
81080
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."
81081
81096
  ].filter(Boolean).join("\n"),
81082
- 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.",
81083
81098
  sourcePath: input.activeRootPath,
81084
81099
  metadata: {
81085
81100
  desktopConnected: true,
@@ -85236,7 +85251,7 @@ function buildProjectMemorySections(meta) {
85236
85251
  lane: "project_memory",
85237
85252
  label: "PERCH.md",
85238
85253
  content: ["## PERCH.md", meta.perchMd.content].join("\n"),
85239
- reason: "Project operator memory loaded from the selected local folder scope.",
85254
+ reason: "Project operator memory loaded from the selected workspace folder.",
85240
85255
  sourcePath: meta.perchMd.relativePath,
85241
85256
  metadata: {
85242
85257
  relativePath: meta.perchMd.relativePath,
@@ -85273,7 +85288,7 @@ function buildProjectMemorySections(meta) {
85273
85288
  lane: "project_memory",
85274
85289
  label: `Rule: ${rule.fileName}`,
85275
85290
  content: [`## Project rule: ${rule.fileName}`, rule.content].join("\n"),
85276
- reason: "Rules-style project instruction loaded from the selected local folder scope.",
85291
+ reason: "Rules-style project instruction loaded from the selected workspace folder.",
85277
85292
  sourcePath: rule.relativePath,
85278
85293
  metadata: {
85279
85294
  relativePath: rule.relativePath,
@@ -85303,7 +85318,7 @@ function buildProjectMemorySections(meta) {
85303
85318
  content: [`## Project memory: ${memory.fileName}`, memory.content].join(
85304
85319
  "\n"
85305
85320
  ),
85306
- reason: "Typed project memory loaded from the selected local folder scope.",
85321
+ reason: "Typed project memory loaded from the selected workspace folder.",
85307
85322
  sourcePath: memory.relativePath,
85308
85323
  metadata: {
85309
85324
  relativePath: memory.relativePath,
@@ -85414,7 +85429,7 @@ Tool strengths:
85414
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.
85415
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.
85416
85431
  - Cite sources for every reported figure. If a figure can't be traced to a document, mark it [UNSOURCED] rather than omitting it
85417
- - 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
85418
85433
  - Do not run a tool just because a keyword matches; reason from the user's current request and the existing thread state
85419
85434
  - Do not redo an audit, document, or email draft that is already done unless the user asks for a revision
85420
85435
  - Summarize tool outputs; don't stream raw JSON to the user
@@ -92429,7 +92444,7 @@ function buildContextSummary(input, rows) {
92429
92444
  `mode=${input.chatMode}`,
92430
92445
  `messages=${input.recentMessages.length}`,
92431
92446
  `source=${input.selectedSourceId ?? "none"}`,
92432
- `desktop=${input.desktopConnected ? "connected" : input.cliLocalTools === true ? "cli-local" : "browser"}`,
92447
+ `desktop=${input.desktopConnected ? "connected" : input.cliLocalTools === true ? "terminal" : "browser"}`,
92433
92448
  `sent=${sentCount}`,
92434
92449
  `compacted=${compactedCount}`,
92435
92450
  `not_found=${notFoundCount}`,
@@ -92788,9 +92803,15 @@ function createEmbeddingProviderFromEnv(env4 = readEnv()) {
92788
92803
  };
92789
92804
  }
92790
92805
  function createBrowserEmbeddingProvider(route = "/api/perch-terminal/embeddings") {
92806
+ let resolvedModel = "server-configured";
92807
+ let resolvedDimensions = DEFAULT_RETRIEVAL_EMBEDDING_DIMENSIONS;
92791
92808
  return {
92792
- model: "server-configured",
92793
- dimensions: DEFAULT_RETRIEVAL_EMBEDDING_DIMENSIONS,
92809
+ get model() {
92810
+ return resolvedModel;
92811
+ },
92812
+ get dimensions() {
92813
+ return resolvedDimensions;
92814
+ },
92794
92815
  async embed(text) {
92795
92816
  return (await this.embedBatch([text]))[0];
92796
92817
  },
@@ -92813,6 +92834,12 @@ function createBrowserEmbeddingProvider(route = "/api/perch-terminal/embeddings"
92813
92834
  if (!Array.isArray(payload.embeddings)) {
92814
92835
  throw new EmbeddingProviderError("Embedding route returned no embeddings.");
92815
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
+ }
92816
92843
  return payload.embeddings;
92817
92844
  }
92818
92845
  };
@@ -92887,7 +92914,7 @@ var init_embeddingProvider = __esm({
92887
92914
 
92888
92915
  // features/perchTerminal/persistence/permanentMemoryPersistence.ts
92889
92916
  function createPermanentMemoryPersistence(supabase, options = {}) {
92890
- const embeddingProvider = options.embeddingProvider ?? createBrowserEmbeddingProvider();
92917
+ const embeddingProvider = options.embeddingProvider ?? createDefaultPermanentMemoryEmbeddingProvider();
92891
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";
92892
92919
  const api2 = {
92893
92920
  async listPermanentMemories({
@@ -93147,6 +93174,21 @@ function createPermanentMemoryPersistence(supabase, options = {}) {
93147
93174
  };
93148
93175
  return api2;
93149
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
+ }
93150
93192
  async function findSimilarPermanentMemoryForInput(input) {
93151
93193
  const queryEmbedding = await input.embeddingProvider.embed(canonicalMemoryEmbeddingText(input.input));
93152
93194
  const { data, error } = await input.supabase.rpc("perch_ai_match_permanent_memories", {
@@ -117027,7 +117069,7 @@ function createSourcePersistence(supabase) {
117027
117069
  return count ?? 0;
117028
117070
  },
117029
117071
  async countEmbeddedRetrievalChunks(workspaceId) {
117030
- 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);
117031
117073
  if (error) throw error;
117032
117074
  return count ?? 0;
117033
117075
  },
@@ -117133,18 +117175,30 @@ function createSourcePersistence(supabase) {
117133
117175
  return data ?? [];
117134
117176
  },
117135
117177
  async listIndexedSourceIds(workspaceId) {
117136
- const [chunks, retrieval, memory] = await Promise.all([
117178
+ const [chunks, retrieval, embeddedRetrieval, memory] = await Promise.all([
117137
117179
  supabase.from("perch_ai_source_chunks").select("source_id").eq("workspace_id", workspaceId),
117138
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),
117139
117182
  supabase.from("perch_ai_source_memory").select("source_id").eq("workspace_id", workspaceId)
117140
117183
  ]);
117141
117184
  if (chunks.error) throw chunks.error;
117142
117185
  if (retrieval.error) throw retrieval.error;
117186
+ if (embeddedRetrieval.error) throw embeddedRetrieval.error;
117143
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);
117144
117191
  return {
117145
- sourceChunkIds: new Set((chunks.data ?? []).map((r) => r.source_id)),
117146
- retrievalChunkIds: new Set((retrieval.data ?? []).map((r) => r.source_id)),
117147
- 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
117148
117202
  };
117149
117203
  },
117150
117204
  async keywordSearchSourcesMetadata(workspaceId, query, limit) {
@@ -117200,6 +117254,16 @@ function createSourcePersistence(supabase) {
117200
117254
  function escapeIlike2(value) {
117201
117255
  return value.replace(/[%_\\]/g, (ch) => `\\${ch}`);
117202
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
+ }
117203
117267
  var SOURCE_LIST_COLUMNS;
117204
117268
  var init_sourcePersistence = __esm({
117205
117269
  "features/perchTerminal/persistence/sourcePersistence.ts"() {
@@ -117877,21 +117941,22 @@ function getSourceRetrievalToolDefinitions() {
117877
117941
  }
117878
117942
  ];
117879
117943
  }
117880
- async function dispatchSourceRetrievalTool(toolName, args, session) {
117881
- if (!isSupabaseConfigured()) {
117882
- 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.");
117883
117947
  }
117884
117948
  if (!session.workspaceId) {
117885
- return sessionError("workspace_id is required from the authenticated session.");
117949
+ return sessionError("Sign in to a workspace before using workspace source search.");
117886
117950
  }
117887
117951
  let supabase;
117888
117952
  try {
117889
- supabase = createClient();
117953
+ supabase = options.supabase ?? createClient();
117890
117954
  } catch (err) {
117891
117955
  const message = err instanceof Error ? err.message : String(err);
117892
117956
  return sessionError(message);
117893
117957
  }
117894
117958
  const store = createSourcePersistence(supabase);
117959
+ const retrievalStore = createRetrievalPersistence(supabase);
117895
117960
  const workspaceId = session.workspaceId;
117896
117961
  switch (toolName) {
117897
117962
  case TOOL_NAMES.listSources:
@@ -117907,9 +117972,9 @@ async function dispatchSourceRetrievalTool(toolName, args, session) {
117907
117972
  case TOOL_NAMES.resolveSourceCandidates:
117908
117973
  return resolveSourceCandidatesHandler(store, workspaceId, session, args);
117909
117974
  case TOOL_NAMES.retrieveContext:
117910
- return retrieveContextHandler(store, workspaceId, session, args);
117975
+ return retrieveContextHandler(store, retrievalStore, workspaceId, session, args);
117911
117976
  case TOOL_NAMES.semanticSearch:
117912
- return semanticSearchHandler(store, workspaceId, args);
117977
+ return semanticSearchHandler(store, retrievalStore, workspaceId, args);
117913
117978
  case TOOL_NAMES.diagnoseWorkspaceAccess:
117914
117979
  return diagnoseWorkspaceAccessHandler(store, workspaceId, session);
117915
117980
  default:
@@ -118172,7 +118237,7 @@ async function resolveSourceCandidatesHandler(store, workspaceId, session, args)
118172
118237
  schemaVersion: "perch-tool-result-v1"
118173
118238
  };
118174
118239
  }
118175
- async function retrieveContextHandler(store, workspaceId, session, args) {
118240
+ async function retrieveContextHandler(store, retrievalStore, workspaceId, session, args) {
118176
118241
  const query = stringArg(args.query)?.trim();
118177
118242
  if (!query) {
118178
118243
  return { ok: false, error: "query is required", toolName: TOOL_NAMES.retrieveContext };
@@ -118198,7 +118263,6 @@ async function retrieveContextHandler(store, workspaceId, session, args) {
118198
118263
  let searchMode = "keyword_only";
118199
118264
  let fallbackUsed = false;
118200
118265
  let semanticError = null;
118201
- const retrievalStore = createRetrievalPersistence(createClient());
118202
118266
  const embeddedCount = await store.countEmbeddedRetrievalChunks(workspaceId).catch(() => 0);
118203
118267
  retrievalAttempts.push("semantic_first");
118204
118268
  try {
@@ -118396,7 +118460,7 @@ async function retrieveContextHandler(store, workspaceId, session, args) {
118396
118460
  schemaVersion: "perch-tool-result-v1"
118397
118461
  };
118398
118462
  }
118399
- async function semanticSearchHandler(store, workspaceId, args) {
118463
+ async function semanticSearchHandler(store, retrievalStore, workspaceId, args) {
118400
118464
  const query = stringArg(args.query)?.trim();
118401
118465
  if (!query) {
118402
118466
  return { ok: false, error: "query is required", toolName: TOOL_NAMES.semanticSearch };
@@ -118405,7 +118469,7 @@ async function semanticSearchHandler(store, workspaceId, args) {
118405
118469
  const sourceIds = Array.isArray(args.sourceIds) ? args.sourceIds.map(String) : void 0;
118406
118470
  const includeStale = args.includeStale === true;
118407
118471
  const embeddedCount = await store.countEmbeddedRetrievalChunks(workspaceId);
118408
- const result2 = await createRetrievalPersistence(createClient()).matchRetrievalChunks({
118472
+ const result2 = await retrievalStore.matchRetrievalChunks({
118409
118473
  workspaceId,
118410
118474
  query,
118411
118475
  limit,
@@ -118450,7 +118514,7 @@ async function semanticSearchHandler(store, workspaceId, args) {
118450
118514
  return {
118451
118515
  rankSignals: signals,
118452
118516
  reason: result2.searchMode === "hybrid" ? "hybrid_sparse_vector_rrf" : result2.searchMode === "sparse" ? "postgres_full_text_rank" : "semantic_similarity",
118453
- 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"}`
118454
118518
  };
118455
118519
  })(),
118456
118520
  rank: index + 1,
@@ -118490,7 +118554,7 @@ async function semanticSearchHandler(store, workspaceId, args) {
118490
118554
  semanticAvailable: result2.embeddingProviderConfigured,
118491
118555
  sparseAvailable: result2.sparseProviderConfigured,
118492
118556
  fallbackUsed: result2.fallbackUsed,
118493
- 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"
118494
118558
  },
118495
118559
  schemaVersion: "perch-tool-result-v1"
118496
118560
  };
@@ -118499,7 +118563,7 @@ async function diagnoseWorkspaceAccessHandler(store, workspaceId, session) {
118499
118563
  const workspace = await store.getWorkspaceRow(workspaceId);
118500
118564
  const blockers = [];
118501
118565
  if (!workspace) {
118502
- blockers.push("Workspace row not found or not accessible under RLS.");
118566
+ blockers.push("Workspace is not available to this signed-in session.");
118503
118567
  }
118504
118568
  let sourcesCount = 0;
118505
118569
  let sourceChunksCount = 0;
@@ -133502,6 +133566,8 @@ var init_toolPermissionPolicy = __esm({
133502
133566
  TOOL_NAMES.deleteLocalFile,
133503
133567
  TOOL_NAMES.editLocalFile,
133504
133568
  TOOL_NAMES.statPath,
133569
+ TOOL_NAMES.readProjectMemory,
133570
+ TOOL_NAMES.saveToMemory,
133505
133571
  TOOL_NAMES.listLocalSources,
133506
133572
  TOOL_NAMES.readLocalSourceFile,
133507
133573
  TOOL_NAMES.generateAPAuditPacket,
@@ -135199,7 +135265,7 @@ function getDesktopToolDefinitions() {
135199
135265
  type: "function",
135200
135266
  function: {
135201
135267
  name: TOOL_NAMES.readProjectMemory,
135202
- 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.",
135203
135269
  parameters: {
135204
135270
  type: "object",
135205
135271
  properties: {},
@@ -135211,7 +135277,7 @@ function getDesktopToolDefinitions() {
135211
135277
  type: "function",
135212
135278
  function: {
135213
135279
  name: TOOL_NAMES.saveToMemory,
135214
- 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.",
135215
135281
  parameters: {
135216
135282
  type: "object",
135217
135283
  properties: {
@@ -135227,6 +135293,15 @@ function getDesktopToolDefinitions() {
135227
135293
  sectionHeading: {
135228
135294
  type: "string",
135229
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."
135230
135305
  }
135231
135306
  },
135232
135307
  required: ["fileName", "mode", "content"],
@@ -135282,7 +135357,7 @@ function getDesktopToolDefinitions() {
135282
135357
  type: "function",
135283
135358
  function: {
135284
135359
  name: TOOL_NAMES.getProjectRules,
135285
- 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.",
135286
135361
  parameters: {
135287
135362
  type: "object",
135288
135363
  properties: {},
@@ -135300,7 +135375,7 @@ function getDesktopToolDefinitions() {
135300
135375
  properties: {
135301
135376
  path: {
135302
135377
  type: "string",
135303
- 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."
135304
135379
  },
135305
135380
  query: {
135306
135381
  type: "string",
@@ -136488,7 +136563,7 @@ function getNativeToolDefinitions() {
136488
136563
  type: "function",
136489
136564
  function: {
136490
136565
  name: TOOL_NAMES.ctxInspect,
136491
- 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.",
136492
136567
  parameters: {
136493
136568
  type: "object",
136494
136569
  properties: {},
@@ -136522,7 +136597,7 @@ function getNativeToolDefinitions() {
136522
136597
  type: "function",
136523
136598
  function: {
136524
136599
  name: TOOL_NAMES.configInspect,
136525
- 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.",
136526
136601
  parameters: {
136527
136602
  type: "object",
136528
136603
  properties: {},
@@ -137811,12 +137886,17 @@ async function restartBashTerminal(request) {
137811
137886
  async function runBashTerminalCommand(request) {
137812
137887
  const bridge = getDesktopBridge();
137813
137888
  if (!bridge) return null;
137814
- return bridge.runBashTerminalCommand(request);
137889
+ return bridge.runBashTerminalCommand(stripBrowserOnlyAbortSignal(request));
137815
137890
  }
137816
137891
  async function runLocalBash(request) {
137817
137892
  const bridge = getDesktopBridge();
137818
137893
  if (!bridge) return null;
137819
- return bridge.runLocalBash(withPermissionMode(request, request));
137894
+ return bridge.runLocalBash(withPermissionMode(stripBrowserOnlyAbortSignal(request), request));
137895
+ }
137896
+ function stripBrowserOnlyAbortSignal(request) {
137897
+ if (typeof window === "undefined" || !("signal" in request)) return request;
137898
+ const { signal: _signal, ...rest2 } = request;
137899
+ return rest2;
137820
137900
  }
137821
137901
  async function readWorkspaceFile(request) {
137822
137902
  const bridge = getDesktopBridge();
@@ -206110,7 +206190,7 @@ async function requireMemoryRootId(ctx, surface) {
206110
206190
  if (!rootId) {
206111
206191
  return {
206112
206192
  ok: false,
206113
- error: `No selected folder scope is available for ${surface}.`
206193
+ error: `No selected workspace folder is available for ${surface}.`
206114
206194
  };
206115
206195
  }
206116
206196
  return { ok: true, rootId };
@@ -206135,14 +206215,86 @@ var init_readProjectMemory = __esm({
206135
206215
  classification: { native: false },
206136
206216
  handler: async (_args, ctx) => {
206137
206217
  const root2 = await requireMemoryRootId(ctx, "project memory");
206138
- if (!root2.ok) return root2;
206139
- 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;
206140
206239
  }
206141
206240
  };
206142
206241
  }
206143
206242
  });
206144
206243
 
206145
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
+ }
206146
206298
  var saveToMemoryTool;
206147
206299
  var init_saveToMemory = __esm({
206148
206300
  "features/perchTerminal/runtime/toolSystem/tools/projectMemory/saveToMemory.ts"() {
@@ -206154,14 +206306,24 @@ var init_saveToMemory = __esm({
206154
206306
  name: TOOL_NAMES.saveToMemory,
206155
206307
  classification: { native: false },
206156
206308
  handler: async (args, ctx) => {
206309
+ const serverResult = hasCliServerMemory(ctx) ? await saveMemoryThroughCliServer(args, ctx) : null;
206310
+ if (serverResult?.ok) return serverResult;
206157
206311
  const root2 = await requireMemoryRootId(ctx, "project memory");
206158
- if (!root2.ok) return root2;
206159
- return writeMemoryFile(root2.rootId, {
206312
+ if (!root2.ok) return serverResult ?? memoryUnavailable();
206313
+ const localResult = await writeMemoryFile(root2.rootId, {
206160
206314
  fileName: String(args.fileName),
206161
206315
  mode: String(args.mode),
206162
206316
  content: String(args.content ?? ""),
206163
206317
  sectionHeading: typeof args.sectionHeading === "string" ? args.sectionHeading : void 0
206164
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();
206165
206327
  }
206166
206328
  };
206167
206329
  }
@@ -213107,18 +213269,24 @@ var init_terminal = __esm({
213107
213269
  command,
213108
213270
  cwd: cwd2,
213109
213271
  timeoutMs,
213110
- fsAccessOverride: ctx.fsAccessOverride
213272
+ fsAccessOverride: ctx.fsAccessOverride,
213273
+ signal: ctx.cliLocalTools ? ctx.signal : void 0
213111
213274
  });
213112
213275
  }
213113
213276
  };
213114
213277
  runBashTerminalCommandTool = {
213115
213278
  name: TOOL_NAMES.runBashTerminalCommand,
213116
213279
  classification: { native: false },
213117
- handler: async (args) => {
213280
+ handler: async (args, ctx) => {
213118
213281
  const command = String(args.command ?? "");
213119
213282
  const cwd2 = args.cwd ? String(args.cwd) : void 0;
213120
213283
  const commandArgs = Array.isArray(args.args) ? args.args.map(String) : void 0;
213121
- return runBashTerminalCommand({ command, cwd: cwd2, args: commandArgs });
213284
+ return runBashTerminalCommand({
213285
+ command,
213286
+ cwd: cwd2,
213287
+ args: commandArgs,
213288
+ signal: ctx.cliLocalTools ? ctx.signal : void 0
213289
+ });
213122
213290
  }
213123
213291
  };
213124
213292
  terminalTools = [bashTool, runBashTerminalCommandTool];
@@ -213210,7 +213378,7 @@ var init_localSources = __esm({
213210
213378
  return {
213211
213379
  ok: true,
213212
213380
  sources: [],
213213
- 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."
213214
213382
  };
213215
213383
  }
213216
213384
  const maxResults = sanitizeListLocalSourcesMaxResults(args.maxResults);
@@ -213769,6 +213937,11 @@ var init_fixtureStore = __esm({
213769
213937
  function calendarTicker() {
213770
213938
  return process.env.PERCH_MARKET_CALENDAR_TICKER || "SPY";
213771
213939
  }
213940
+ function sourceRank(source) {
213941
+ if (source === "yahoo") return 3;
213942
+ if (source === "stooq") return 2;
213943
+ return 1;
213944
+ }
213772
213945
  function createSupabaseMarketStore(supabase) {
213773
213946
  return {
213774
213947
  async listInstruments() {
@@ -213786,13 +213959,13 @@ function createSupabaseMarketStore(supabase) {
213786
213959
  async getBars(ticker, opts) {
213787
213960
  const cutoff = endOfDayUtc(opts.upTo);
213788
213961
  const wanted = opts.lookbackBars ?? Number.MAX_SAFE_INTEGER;
213789
- const rows = [];
213790
- for (let page2 = 0; rows.length < wanted; page2++) {
213791
- const { data, error } = await supabase.from("perch_ai_market_bars").select("bar_date, open, high, low, close, adj_close, volume, known_at").eq("ticker", ticker).lte("bar_date", opts.upTo).lte("known_at", cutoff).order("bar_date", { ascending: false }).range(page2 * PAGE_SIZE, page2 * PAGE_SIZE + PAGE_SIZE - 1);
213962
+ const byDate = /* @__PURE__ */ new Map();
213963
+ for (let page2 = 0; byDate.size < wanted; page2++) {
213964
+ const { data, error } = await supabase.from("perch_ai_market_bars").select("bar_date, open, high, low, close, adj_close, volume, known_at, source").eq("ticker", ticker).lte("bar_date", opts.upTo).lte("known_at", cutoff).order("bar_date", { ascending: false }).order("known_at", { ascending: false }).range(page2 * PAGE_SIZE, page2 * PAGE_SIZE + PAGE_SIZE - 1);
213792
213965
  if (error) throw new Error(`market bars read failed: ${error.message}`);
213793
213966
  const batch = data ?? [];
213794
213967
  for (const row of batch) {
213795
- rows.push({
213968
+ const bar = {
213796
213969
  ticker,
213797
213970
  date: row.bar_date,
213798
213971
  open: row.open === null ? void 0 : Number(row.open),
@@ -213801,13 +213974,17 @@ function createSupabaseMarketStore(supabase) {
213801
213974
  close: Number(row.close),
213802
213975
  adjClose: row.adj_close === null ? Number(row.close) : Number(row.adj_close),
213803
213976
  volume: row.volume === null ? void 0 : Number(row.volume),
213804
- knownAt: row.known_at
213805
- });
213806
- if (rows.length >= wanted) break;
213977
+ knownAt: row.known_at,
213978
+ source: row.source ?? void 0
213979
+ };
213980
+ const existing = byDate.get(bar.date);
213981
+ if (!existing || sourceRank(bar.source) > sourceRank(existing.source)) {
213982
+ byDate.set(bar.date, bar);
213983
+ }
213807
213984
  }
213808
213985
  if (batch.length < PAGE_SIZE) break;
213809
213986
  }
213810
- return rows.reverse();
213987
+ return [...byDate.values()].sort((a, b2) => a.date > b2.date ? -1 : a.date < b2.date ? 1 : 0).slice(0, wanted).reverse().map(({ source: _source, ...bar }) => bar);
213811
213988
  },
213812
213989
  async getTradingDates(opts) {
213813
213990
  const dates = [];
@@ -216685,6 +216862,8 @@ async function executeToolCall(call, opts) {
216685
216862
  workspaceId: opts.workspaceId,
216686
216863
  threadId: opts.threadId,
216687
216864
  supabase: opts.supabase,
216865
+ cliServerAppUrl: opts.cliServerAppUrl,
216866
+ cliServerAccessToken: opts.cliServerAccessToken,
216688
216867
  marketDeskProxyAppUrl: opts.marketDeskProxyAppUrl,
216689
216868
  marketDeskProxyAccessToken: opts.marketDeskProxyAccessToken,
216690
216869
  chatMode: opts.chatMode ?? null,
@@ -216744,7 +216923,7 @@ async function executeToolCall(call, opts) {
216744
216923
  const result2 = {
216745
216924
  ok: false,
216746
216925
  errorCode: "supabase_session_required",
216747
- 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.",
216748
216927
  schemaVersion: "perch-tool-result-v1",
216749
216928
  toolName: call.name
216750
216929
  };
@@ -216825,6 +217004,8 @@ async function executeToolCall(call, opts) {
216825
217004
  workspaceRoot: opts.workspaceRoot,
216826
217005
  threadId: opts.threadId,
216827
217006
  runId: opts.runId,
217007
+ cliServerAppUrl: opts.cliServerAppUrl,
217008
+ cliServerAccessToken: opts.cliServerAccessToken,
216828
217009
  marketDeskProxyAppUrl: opts.marketDeskProxyAppUrl,
216829
217010
  marketDeskProxyAccessToken: opts.marketDeskProxyAccessToken,
216830
217011
  onEvent: runtime.onEvent,
@@ -219025,6 +219206,8 @@ async function runModelToolLoop(input) {
219025
219206
  selectedSourceId: input.selectedSourceId,
219026
219207
  supabaseConfigured: input.supabaseConfigured,
219027
219208
  supabase: input.supabase,
219209
+ cliServerAppUrl: input.cliServerAppUrl,
219210
+ cliServerAccessToken: input.cliServerAccessToken,
219028
219211
  marketDeskProxyAppUrl: input.marketDeskProxyAppUrl,
219029
219212
  marketDeskProxyAccessToken: input.marketDeskProxyAccessToken,
219030
219213
  runId: input.runId,
@@ -219441,6 +219624,8 @@ async function executeInitialToolCall(input) {
219441
219624
  selectedSourceId: input.input.selectedSourceId,
219442
219625
  supabaseConfigured: input.input.supabaseConfigured,
219443
219626
  supabase: input.input.supabase,
219627
+ cliServerAppUrl: input.input.cliServerAppUrl,
219628
+ cliServerAccessToken: input.input.cliServerAccessToken,
219444
219629
  marketDeskProxyAppUrl: input.input.marketDeskProxyAppUrl,
219445
219630
  marketDeskProxyAccessToken: input.input.marketDeskProxyAccessToken,
219446
219631
  runId: input.input.runId,
@@ -220502,7 +220687,7 @@ async function runLiveAgentsLoop(input) {
220502
220687
  const onEv = onEvent ?? deps.onEvent;
220503
220688
  onEv({
220504
220689
  type: "activity_delta",
220505
- text: "Workspace preflight: no folder scope selected \u2014 continuing with Desktop tools.",
220690
+ text: "Workspace preflight: no workspace folder selected; continuing with Desktop tools.",
220506
220691
  ts: (/* @__PURE__ */ new Date()).toISOString()
220507
220692
  });
220508
220693
  }
@@ -220541,6 +220726,8 @@ async function runLiveAgentsLoop(input) {
220541
220726
  untrustedContextPresent: context.untrustedContextPresent,
220542
220727
  supabaseConfigured: turn.supabaseConfigured,
220543
220728
  supabase: turn.supabase,
220729
+ cliServerAppUrl: turn.cliServerAppUrl ?? null,
220730
+ cliServerAccessToken: turn.cliServerAccessToken ?? null,
220544
220731
  marketDeskProxyAppUrl: turn.marketDeskProxyAppUrl ?? null,
220545
220732
  marketDeskProxyAccessToken: turn.marketDeskProxyAccessToken ?? null,
220546
220733
  onEvent: onEvent ?? deps.onEvent,
@@ -221907,7 +222094,12 @@ async function ensureLocalSourceIndexed(input) {
221907
222094
  contentHash: null,
221908
222095
  chunksBuilt: 0,
221909
222096
  chunksUpserted: 0,
221910
- embeddingConfigured: getEmbeddingProviderStatus().configured,
222097
+ sourceChunksUpserted: 0,
222098
+ retrievalChunksBuilt: 0,
222099
+ retrievalChunksUpserted: 0,
222100
+ embeddedChunksUpserted: 0,
222101
+ vectorReady: false,
222102
+ embeddingConfigured: isBrowserRuntime() ? true : getEmbeddingProviderStatus().configured,
221911
222103
  errors: [],
221912
222104
  warnings: []
221913
222105
  };
@@ -221940,7 +222132,12 @@ async function ensureLocalSourceIndexed(input) {
221940
222132
  contentHash,
221941
222133
  chunksBuilt: 0,
221942
222134
  chunksUpserted: 0,
221943
- embeddingConfigured: getEmbeddingProviderStatus().configured,
222135
+ sourceChunksUpserted: 0,
222136
+ retrievalChunksBuilt: 0,
222137
+ retrievalChunksUpserted: 0,
222138
+ embeddedChunksUpserted: 0,
222139
+ vectorReady: true,
222140
+ embeddingConfigured: isBrowserRuntime() ? true : getEmbeddingProviderStatus().configured,
221944
222141
  errors: [],
221945
222142
  warnings: []
221946
222143
  };
@@ -222038,12 +222235,15 @@ async function ensureLocalSourceIndexed(input) {
222038
222235
  threadId: input.threadId ?? null,
222039
222236
  chunks: sourceTextChunks
222040
222237
  });
222041
- const embeddingStatus = getEmbeddingProviderStatus();
222042
222238
  let chunksBuilt = textChunks.length;
222043
222239
  let chunksUpserted = 0;
222044
222240
  const warnings = [...extracted.truncated ? ["Source text was truncated before indexing."] : []];
222045
- if (!embeddingStatus.configured) {
222046
- 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);
222047
222247
  return {
222048
222248
  ok: true,
222049
222249
  status: "indexed",
@@ -222053,38 +222253,69 @@ async function ensureLocalSourceIndexed(input) {
222053
222253
  contentHash: indexedContentHash,
222054
222254
  chunksBuilt,
222055
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,
222056
222310
  embeddingConfigured: false,
222057
222311
  errors: [],
222058
222312
  warnings
222059
222313
  };
222060
222314
  }
222061
- const indexer = createRetrievalIndexer(input.supabase);
222062
- const indexResult = pdfPages ? await indexer.indexLocalFilePages({
222063
- workspaceId: input.workspaceId,
222064
- sourceId: source.id,
222065
- fileName: source.file_name,
222066
- fileType: extracted.fileType,
222067
- mimeType: source.mime_type,
222068
- provenanceType: "desktop_local",
222069
- contentHash: indexedContentHash,
222070
- threadId: input.threadId ?? null,
222071
- pages: pdfPages,
222072
- blocks: pdfBlocks,
222073
- tables: pdfTables,
222074
- facts: factPageEntries.length ? factPageEntries : void 0
222075
- }) : await indexer.indexLocalFileText({
222076
- workspaceId: input.workspaceId,
222077
- sourceId: source.id,
222078
- fileName: source.file_name,
222079
- fileType: extracted.fileType,
222080
- mimeType: source.mime_type,
222081
- provenanceType: "desktop_local",
222082
- contentHash: indexedContentHash,
222083
- threadId: input.threadId ?? null,
222084
- text: extracted.text
222085
- });
222086
222315
  chunksBuilt = indexResult.chunksBuilt;
222087
222316
  chunksUpserted = indexResult.chunksUpserted;
222317
+ const embeddedChunksUpserted = indexResult.chunksEmbedded;
222318
+ const vectorReady = embeddedChunksUpserted > 0;
222088
222319
  if (!indexResult.ok) {
222089
222320
  return {
222090
222321
  ok: false,
@@ -222095,6 +222326,11 @@ async function ensureLocalSourceIndexed(input) {
222095
222326
  contentHash: indexedContentHash,
222096
222327
  chunksBuilt,
222097
222328
  chunksUpserted,
222329
+ sourceChunksUpserted: sourceTextChunks.length,
222330
+ retrievalChunksBuilt: indexResult.chunksBuilt,
222331
+ retrievalChunksUpserted: indexResult.chunksUpserted,
222332
+ embeddedChunksUpserted,
222333
+ vectorReady,
222098
222334
  embeddingConfigured: true,
222099
222335
  errors: indexResult.errors,
222100
222336
  warnings: [...warnings, ...indexResult.warnings]
@@ -222105,10 +222341,15 @@ async function ensureLocalSourceIndexed(input) {
222105
222341
  status: "indexed",
222106
222342
  localSourceId: input.localSourceId,
222107
222343
  supabaseSourceId: source.id,
222108
- retrievalSourceId: source.id,
222344
+ retrievalSourceId: vectorReady ? source.id : null,
222109
222345
  contentHash: indexedContentHash,
222110
222346
  chunksBuilt,
222111
222347
  chunksUpserted,
222348
+ sourceChunksUpserted: sourceTextChunks.length,
222349
+ retrievalChunksBuilt: indexResult.chunksBuilt,
222350
+ retrievalChunksUpserted: indexResult.chunksUpserted,
222351
+ embeddedChunksUpserted,
222352
+ vectorReady,
222112
222353
  embeddingConfigured: true,
222113
222354
  errors: [],
222114
222355
  warnings
@@ -222124,6 +222365,12 @@ async function ensureLocalSourceIndexed(input) {
222124
222365
  };
222125
222366
  }
222126
222367
  }
222368
+ function createDefaultLocalSourceEmbeddingProvider() {
222369
+ return isBrowserRuntime() ? createBrowserEmbeddingProvider() : createEmbeddingProviderFromEnv();
222370
+ }
222371
+ function isBrowserRuntime() {
222372
+ return typeof window !== "undefined";
222373
+ }
222127
222374
  async function resolveRetrievalSourceIdForTurn(input) {
222128
222375
  const selected = input.selectedSourceId?.trim() ?? null;
222129
222376
  if (!selected) return { retrievalSourceId: null, indexOutcome: null };
@@ -222399,7 +222646,11 @@ async function loadFolderIndexSummary(input) {
222399
222646
  }
222400
222647
  const files = input.localSources.filter((entry) => entry.rootId === input.rootId && isAutoIndexableLocalSource(entry)).map((entry) => {
222401
222648
  const row = rowByLocalId.get(entry.localSourceId) ?? null;
222402
- 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;
222403
222654
  const stale = row ? Date.parse(entry.modifiedAt) > Date.parse(row.updated_at) : true;
222404
222655
  return {
222405
222656
  localSourceId: entry.localSourceId,
@@ -222408,19 +222659,30 @@ async function loadFolderIndexSummary(input) {
222408
222659
  mimeType: entry.mimeType,
222409
222660
  modifiedAt: entry.modifiedAt,
222410
222661
  sizeBytes: entry.sizeBytes,
222411
- status: row ? retrievalReady ? "indexed" : "pending" : "pending",
222662
+ status: row ? sourceChunkCount2 > 0 || retrievalReady ? "indexed" : "pending" : "pending",
222412
222663
  supabaseSourceId: row?.id ?? null,
222413
222664
  retrievalReady,
222665
+ vectorReady,
222666
+ sourceChunkCount: sourceChunkCount2,
222667
+ retrievalChunkCount: retrievalChunkCount2,
222668
+ embeddedChunkCount: embeddedChunkCount2,
222414
222669
  contentHash: row?.content_hash ?? null,
222415
222670
  message: stale && row ? "Changed since last indexing pass." : void 0
222416
222671
  };
222417
222672
  });
222418
222673
  const staleFiles = files.filter((file) => file.message?.includes("Changed since last indexing pass")).length;
222419
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);
222420
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");
222421
222680
  const lastIndexedAt = rows[0]?.updated_at ?? null;
222422
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,
222423
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,
222424
222686
  staleFiles > 0 ? `${staleFiles} file(s) changed since the last index update.` : null
222425
222687
  ].filter(Boolean);
222426
222688
  return {
@@ -222430,8 +222692,14 @@ async function loadFolderIndexSummary(input) {
222430
222692
  indexedFiles,
222431
222693
  skippedFiles: 0,
222432
222694
  failedFiles: 0,
222695
+ registeredSources: rows.length,
222696
+ sourceChunkCount,
222697
+ sourceChunkFiles: files.filter((file) => file.sourceChunkCount > 0).length,
222698
+ retrievalChunkCount,
222433
222699
  staleFiles,
222434
222700
  retrievalReadyFiles: retrievalReadySourceIds.length,
222701
+ embeddedChunkCount,
222702
+ vectorReadyFiles: vectorReadySourceIds.length,
222435
222703
  filesNeedingOcr: files.filter((file) => needsOcrHint(file)).length,
222436
222704
  lastIndexedAt,
222437
222705
  status: files.length === 0 ? "idle" : staleFiles > 0 || indexedFiles < files.length ? "partial" : "ready",
@@ -222439,6 +222707,7 @@ async function loadFolderIndexSummary(input) {
222439
222707
  localOnlyEmbeddingsEnabled: false,
222440
222708
  remoteIndexArtifacts: [...DEFAULT_REMOTE_INDEX_ARTIFACTS],
222441
222709
  retrievalReadySourceIds,
222710
+ vectorReadySourceIds,
222442
222711
  diagnostics,
222443
222712
  manifestPath: DEFAULT_MANIFEST_PATH,
222444
222713
  files
@@ -222458,6 +222727,9 @@ function needsOcrHint(file) {
222458
222727
  const message = file.message?.toLowerCase() ?? "";
222459
222728
  return message.includes("ocr") || message.includes("scanned") || message.includes("no readable text");
222460
222729
  }
222730
+ function sumFiles(files, field) {
222731
+ return files.reduce((total, file) => total + file[field], 0);
222732
+ }
222461
222733
  var DEFAULT_MANIFEST_PATH, AUTO_INDEX_EXCLUDED_PATH_PREFIXES, AUTO_INDEX_EXCLUDED_EXACT_PATHS, DEFAULT_STORAGE_MODE, DEFAULT_REMOTE_INDEX_ARTIFACTS;
222462
222734
  var init_folderIndexing = __esm({
222463
222735
  "features/perchTerminal/runtime/folderIndexing.ts"() {
@@ -224713,6 +224985,54 @@ var init_objectiveClassifier = __esm({
224713
224985
  });
224714
224986
 
224715
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
+ }
224716
225036
  async function runOperatorTurn(input, deps) {
224717
225037
  const runId = input.clientRunId?.trim() || makeRunId();
224718
225038
  const startMs = Date.now();
@@ -224778,21 +225098,15 @@ async function runOperatorTurn(input, deps) {
224778
225098
  });
224779
225099
  }
224780
225100
  try {
225101
+ const memoryRetriever = buildTurnMemoryRetriever(input);
224781
225102
  const memoryContext = await buildMemoryContext({
224782
225103
  query: input.trimmedInput,
224783
- userId: input.userId,
225104
+ userId: input.userId ?? (memoryRetriever && input.cliServerAccessToken?.trim() ? "cli_authenticated_user" : null),
224784
225105
  workspaceId: input.workspaceId,
224785
225106
  threadId: input.threadId,
224786
225107
  permanentMemories: input.permanentMemories,
224787
225108
  userMemories: input.userMemories,
224788
- retriever: input.supabase ? async ({ userId, workspaceId, limitPerScope, query }) => createPermanentMemoryPersistence(
224789
- input.supabase
224790
- ).retrievePermanentMemoriesForTurn({
224791
- userId,
224792
- workspaceId,
224793
- limitPerScope,
224794
- query
224795
- }) : null
225109
+ retriever: memoryRetriever
224796
225110
  });
224797
225111
  const earlySession = input.threadId ? await loadThreadSession(input.threadId, { supabase: input.supabase ?? null }) : null;
224798
225112
  const persona = getPersona(input.personaId ?? earlySession?.personaId);
@@ -225080,8 +225394,15 @@ ${planStateLines}`
225080
225394
  if (perchMdRow?.status === "not_found") {
225081
225395
  extendedWarnings.push("No PERCH.md \u2014 add an operator playbook to .perch/PERCH.md to customise behaviour.");
225082
225396
  }
225083
- if (!enrichedInput.supabase) {
225084
- 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.");
225085
225406
  }
225086
225407
  const contextJob = resolveJobContextTokens({
225087
225408
  threadContextTokens: threadContextAccounting.threadContextTokens,
@@ -226493,6 +226814,21 @@ function installCliNodeLocalBridge(input) {
226493
226814
  }
226494
226815
  };
226495
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
+ }
226496
226832
  function createCliNodeLocalBridge(input) {
226497
226833
  const workspaceRoot = path11.resolve(input.workspaceRoot);
226498
226834
  const now13 = () => (/* @__PURE__ */ new Date()).toISOString();
@@ -226567,7 +226903,8 @@ function createCliNodeLocalBridge(input) {
226567
226903
  workspaceRoot: resolveRequestRoot(workspaceRoot, request.workspaceRoot),
226568
226904
  command: request.command,
226569
226905
  cwd: request.cwd,
226570
- timeoutMs: request.timeoutMs
226906
+ timeoutMs: request.timeoutMs,
226907
+ signal: request.signal
226571
226908
  }),
226572
226909
  runBashTerminalCommand: async (request) => {
226573
226910
  const startedAt = Date.now();
@@ -226575,7 +226912,8 @@ function createCliNodeLocalBridge(input) {
226575
226912
  const result2 = await runShellCommand({
226576
226913
  workspaceRoot,
226577
226914
  command: request.args?.length ? shellQuote([request.command, ...request.args]) : request.command,
226578
- cwd: request.cwd
226915
+ cwd: request.cwd,
226916
+ signal: request.signal
226579
226917
  });
226580
226918
  return {
226581
226919
  ok: result2.ok,
@@ -226773,10 +227111,20 @@ function createCliNodeLocalBridge(input) {
226773
227111
  encoding: "base64"
226774
227112
  };
226775
227113
  },
226776
- getProjectRules: async () => [],
226777
- readProjectMemory: async () => ({ ok: false, error: "Project memory is not available in this CLI package yet." }),
226778
- writeProjectMemory: async () => ({ ok: false, error: "Project memory writes are not available in this CLI package yet." }),
226779
- 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),
226780
227128
  writeRule: async () => ({ ok: false, error: "Project rule writes are not available in this CLI package yet." }),
226781
227129
  writePerchMd: async () => ({ ok: false, error: "PERCH.md writes are not available in this CLI package yet." }),
226782
227130
  readGlobalPerchMd: async () => ({ ok: false, error: "Global PERCH.md is not available in this CLI package yet." }),
@@ -227055,6 +227403,292 @@ function guessCliFileType(filePath) {
227055
227403
  if ([".doc", ".docx", ".txt", ".md", ".rtf"].includes(ext)) return "document";
227056
227404
  return "unknown";
227057
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
+ }
227058
227692
  async function runShellCommand(input) {
227059
227693
  const startedAt = Date.now();
227060
227694
  const cwd2 = input.cwd ? resolveReadPath(input.workspaceRoot, input.cwd) : input.workspaceRoot;
@@ -227063,15 +227697,28 @@ async function runShellCommand(input) {
227063
227697
  const child = spawn2(process.env.SHELL || "/bin/zsh", ["-lc", input.command], {
227064
227698
  cwd: cwd2,
227065
227699
  env: process.env,
227066
- stdio: ["ignore", "pipe", "pipe"]
227700
+ stdio: ["ignore", "pipe", "pipe"],
227701
+ detached: true
227067
227702
  });
227068
227703
  let stdout = "";
227069
227704
  let stderr = "";
227070
227705
  let timedOut = false;
227706
+ let aborted = false;
227707
+ let settled = false;
227708
+ const stopChild = (reason) => {
227709
+ if (reason === "abort") aborted = true;
227710
+ if (reason === "timeout") timedOut = true;
227711
+ terminateProcessGroup(child, "SIGTERM");
227712
+ setTimeout(() => {
227713
+ if (!settled) terminateProcessGroup(child, "SIGKILL");
227714
+ }, 1e3).unref?.();
227715
+ };
227071
227716
  const timer = setTimeout(() => {
227072
- timedOut = true;
227073
- child.kill("SIGTERM");
227717
+ stopChild("timeout");
227074
227718
  }, timeoutMs);
227719
+ const onAbort = () => stopChild("abort");
227720
+ if (input.signal?.aborted) onAbort();
227721
+ input.signal?.addEventListener("abort", onAbort, { once: true });
227075
227722
  child.stdout.on("data", (chunk2) => {
227076
227723
  stdout += String(chunk2);
227077
227724
  });
@@ -227079,7 +227726,9 @@ async function runShellCommand(input) {
227079
227726
  stderr += String(chunk2);
227080
227727
  });
227081
227728
  child.on("error", (error) => {
227729
+ settled = true;
227082
227730
  clearTimeout(timer);
227731
+ input.signal?.removeEventListener("abort", onAbort);
227083
227732
  resolve5({
227084
227733
  ok: false,
227085
227734
  stdout,
@@ -227095,10 +227744,12 @@ async function runShellCommand(input) {
227095
227744
  });
227096
227745
  });
227097
227746
  child.on("close", (code, signal) => {
227747
+ settled = true;
227098
227748
  clearTimeout(timer);
227749
+ input.signal?.removeEventListener("abort", onAbort);
227099
227750
  const capped = capOutput(stdout, stderr);
227100
227751
  resolve5({
227101
- ok: code === 0 && !timedOut,
227752
+ ok: code === 0 && !timedOut && !aborted,
227102
227753
  stdout: capped.stdout,
227103
227754
  stderr: capped.stderr,
227104
227755
  exitCode: code,
@@ -227109,11 +227760,25 @@ async function runShellCommand(input) {
227109
227760
  command: input.command,
227110
227761
  cwd: cwd2,
227111
227762
  executionHost: "electron_desktop",
227112
- ...timedOut ? { errorCode: "timed_out", errorMessage: `Command timed out after ${timeoutMs}ms.` } : {}
227763
+ ...timedOut ? { errorCode: "timed_out", errorMessage: `Command timed out after ${timeoutMs}ms.` } : {},
227764
+ ...aborted ? { errorCode: "aborted", errorMessage: "Command stopped by user." } : {}
227113
227765
  });
227114
227766
  });
227115
227767
  });
227116
227768
  }
227769
+ function terminateProcessGroup(child, signal) {
227770
+ if (child.pid) {
227771
+ try {
227772
+ process.kill(-child.pid, signal);
227773
+ return;
227774
+ } catch {
227775
+ }
227776
+ }
227777
+ try {
227778
+ child.kill(signal);
227779
+ } catch {
227780
+ }
227781
+ }
227117
227782
  function resolveReadPath(root2, inputPath) {
227118
227783
  const expanded = expandHome4(inputPath || ".");
227119
227784
  return path11.resolve(path11.isAbsolute(expanded) ? expanded : path11.join(root2, expanded));
@@ -227261,7 +227926,7 @@ function capOutput(stdout, stderr) {
227261
227926
  function shellQuote(parts) {
227262
227927
  return parts.map((part) => `'${part.replace(/'/g, "'\\''")}'`).join(" ");
227263
227928
  }
227264
- 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;
227265
227930
  var init_nodeLocalBridge = __esm({
227266
227931
  "features/perchTerminal/runtime/cliHost/nodeLocalBridge.ts"() {
227267
227932
  "use strict";
@@ -227272,6 +227937,20 @@ var init_nodeLocalBridge = __esm({
227272
227937
  DEFAULT_MAX_RESULTS = 200;
227273
227938
  MAX_READ_BYTES = 2e6;
227274
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
+ ];
227275
227954
  }
227276
227955
  });
227277
227956
 
@@ -227353,10 +228032,13 @@ function buildCliTurnInput(input, resolved) {
227353
228032
  activeRootPath: input.activeRootPath ?? resolved.cwd,
227354
228033
  localSources: [],
227355
228034
  localSourcesMeta: null,
228035
+ projectMeta: readCliProjectMemoryState(resolved.cwd).meta,
227356
228036
  permanentMemories: input.permanentMemories ?? [],
227357
228037
  userMemories: input.userMemories ?? [],
227358
228038
  supabase: null,
227359
228039
  supabaseConfigured: false,
228040
+ cliServerAppUrl: input.cliServerAppUrl ?? input.marketDeskProxyAppUrl ?? input.appUrl ?? null,
228041
+ cliServerAccessToken: input.cliServerAccessToken ?? input.marketDeskProxyAccessToken ?? null,
227360
228042
  marketDeskProxyAppUrl: input.marketDeskProxyAppUrl ?? input.appUrl ?? null,
227361
228043
  marketDeskProxyAccessToken: input.marketDeskProxyAccessToken ?? null,
227362
228044
  permissionMode: normalizePermissionMode(input.permissionMode ?? "default"),
@@ -227432,6 +228114,7 @@ var init_runCliTurn = __esm({
227432
228114
  init_personaRegistry();
227433
228115
  init_runOperatorTurn();
227434
228116
  init_nodeLocalBridge();
228117
+ init_nodeLocalBridge();
227435
228118
  }
227436
228119
  });
227437
228120
 
@@ -227601,12 +228284,12 @@ import os2 from "node:os";
227601
228284
  import path13 from "node:path";
227602
228285
  import { promisify } from "node:util";
227603
228286
  async function readStoredCliAuthSession() {
227604
- 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);
227605
228288
  return parseStoredCliAuthSession(raw);
227606
228289
  }
227607
228290
  async function writeStoredCliAuthSession(session) {
227608
228291
  const raw = JSON.stringify(session);
227609
- if (process.platform === "darwin") {
228292
+ if (process.platform === "darwin" && !shouldUseFallbackAuthFile()) {
227610
228293
  await execFileAsync("security", [
227611
228294
  "add-generic-password",
227612
228295
  "-U",
@@ -227622,7 +228305,7 @@ async function writeStoredCliAuthSession(session) {
227622
228305
  await writeFallbackSecret(raw);
227623
228306
  }
227624
228307
  async function clearStoredCliAuthSession() {
227625
- if (process.platform === "darwin") {
228308
+ if (process.platform === "darwin" && !shouldUseFallbackAuthFile()) {
227626
228309
  await execFileAsync("security", [
227627
228310
  "delete-generic-password",
227628
228311
  "-s",
@@ -227634,6 +228317,9 @@ async function clearStoredCliAuthSession() {
227634
228317
  }
227635
228318
  await fs11.rm(fallbackSessionPath(), { force: true }).catch(() => void 0);
227636
228319
  }
228320
+ function shouldUseFallbackAuthFile() {
228321
+ return Boolean(process.env.PERCH_CLI_AUTH_DIR?.trim());
228322
+ }
227637
228323
  function isStoredCliAuthSessionUsable(session, nowSeconds = Math.floor(Date.now() / 1e3)) {
227638
228324
  if (!session?.accessToken?.trim()) return false;
227639
228325
  if (!session.appUrl?.trim()) return false;
@@ -283754,6 +284440,7 @@ function parseInteractiveSlashCommand(input) {
283754
284440
  }
283755
284441
  function renderInteractiveStatus(state, connection, session, workspaceId) {
283756
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);
283757
284444
  const connectionStatus = isCliModelConnectionReady(connection) ? "connected" : "locked \xB7 run /login";
283758
284445
  const color = shouldUseCliColor();
283759
284446
  const lines = [
@@ -283761,6 +284448,7 @@ function renderInteractiveStatus(state, connection, session, workspaceId) {
283761
284448
  ["cwd", state.cwd],
283762
284449
  ["auth", storedAuth],
283763
284450
  ["connection", connectionStatus],
284451
+ ["memory", renderCliMemoryAvailability(state.cwd, workspaceId ?? null, signedIn)],
283764
284452
  ["tools", renderCliCapabilityCount(state, workspaceId ?? null)],
283765
284453
  ["permission", state.permissionMode],
283766
284454
  ["mode", state.chatMode],
@@ -283772,6 +284460,15 @@ function renderInteractiveStatus(state, connection, session, workspaceId) {
283772
284460
  ];
283773
284461
  return lines.map(([key, value]) => `${paint(key.padEnd(11), "muted", color)} ${value}`).join("\n") + "\n";
283774
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
+ }
283775
284472
  function renderCliCapabilityCount(state, workspaceId) {
283776
284473
  const count = getExecutableToolDefinitions({
283777
284474
  surface: "cli",
@@ -283966,7 +284663,21 @@ async function fetchCliHostedContext(connection, input) {
283966
284663
  return {
283967
284664
  userId: typeof context.session?.userId === "string" ? context.session.userId : session.userId ?? null,
283968
284665
  workspaceId: typeof context.session?.workspaceId === "string" ? context.session.workspaceId : null,
283969
- 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
283970
284681
  };
283971
284682
  }
283972
284683
  async function resolveCliMarketDeskProxy(connection) {
@@ -283974,6 +284685,8 @@ async function resolveCliMarketDeskProxy(connection) {
283974
284685
  const session = await readStoredCliAuthSession();
283975
284686
  if (!isStoredCliAuthSessionUsable(session)) return {};
283976
284687
  return {
284688
+ cliServerAppUrl: session.appUrl || connection.appUrl,
284689
+ cliServerAccessToken: session.accessToken,
283977
284690
  marketDeskProxyAppUrl: session.appUrl || connection.appUrl,
283978
284691
  marketDeskProxyAccessToken: session.accessToken
283979
284692
  };
@@ -284018,7 +284731,7 @@ async function postCliJson(appUrl, session, pathname, payload) {
284018
284731
  clearTimeout(timeout);
284019
284732
  }
284020
284733
  }
284021
- function isPermanentMemoryLike(value) {
284734
+ function isPermanentMemoryLike2(value) {
284022
284735
  if (!value || typeof value !== "object") return false;
284023
284736
  const memory = value;
284024
284737
  return typeof memory.id === "string" && typeof memory.title === "string" && typeof memory.body === "string";
@@ -284318,12 +285031,17 @@ async function runAuthCommand(parsed, writer) {
284318
285031
  if (parsed.action === "status") {
284319
285032
  const session = await readStoredCliAuthSession();
284320
285033
  if (isStoredCliAuthSessionUsable(session)) {
284321
- writer.stdout(`Perch CLI ${CLI_PACKAGE_VERSION} \xB7 Signed in${session.email ? ` as ${session.email}` : ""} \xB7 ${session.appUrl}
284322
- `);
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");
284323
285039
  return 0;
284324
285040
  }
284325
- writer.stdout(`Perch CLI ${CLI_PACKAGE_VERSION} \xB7 Not signed in. Run \`perch login\`.
284326
- `);
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");
284327
285045
  return 2;
284328
285046
  }
284329
285047
  const appUrl = resolveCliAppUrl(parsed.appUrl, DEFAULT_CLI_LOGIN_APP_URL) ?? DEFAULT_CLI_LOGIN_APP_URL;
@@ -284583,6 +285301,7 @@ var init_perch_cli = __esm({
284583
285301
  init_runRegistry();
284584
285302
  init_learningMemory();
284585
285303
  init_toolDefinitions();
285304
+ init_nodeLocalBridge();
284586
285305
  execFileAsync3 = promisify3(execFile3);
284587
285306
  DEFAULT_CLI_LOGIN_APP_URL = "https://app.perchai.app";
284588
285307
  CLI_PACKAGE_VERSION = readCliPackageVersion();
@@ -284616,7 +285335,7 @@ Usage:
284616
285335
  perch login
284617
285336
  perch status
284618
285337
  perch logout
284619
- perch run "<task>" [--json] [--cwd <dir>] [--mode ask|agents|plan] [--persona saffron|quill]
285338
+ perch run "<task>" [--json] [--thread <id>] [--cwd <dir>] [--mode ask|agents|plan] [--persona saffron|quill]
284620
285339
  perch ap evidence <folder> [--json] [--out <dir>] [--timestamp <id>]
284621
285340
  perch ap packet <folder> [--json] [--out <dir>] [--timestamp <id>]
284622
285341
  perch test ap <folder> [--json] [--out <dir>] [--timestamp <id>] [--profile financial-lab-v2] [--expect key=value]