om-memory-system 4.8.0 → 4.10.0

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.
@@ -123,7 +123,7 @@ export async function runWebCommand(args, options = {}, online, handover = {}) {
123
123
  process.once("SIGTERM", stop);
124
124
  // The page's Stop and Restart buttons reach the same process through this callback.
125
125
  const { createPowerAction } = await import("./web-power.js");
126
- server.setOnPowerAction(createPowerAction({
126
+ const powerAction = createPowerAction({
127
127
  stopServer: async () => {
128
128
  clearInterval(keeper);
129
129
  process.off("SIGINT", stop);
@@ -140,7 +140,16 @@ export async function runWebCommand(args, options = {}, online, handover = {}) {
140
140
  baseUrl: server.getUrl(),
141
141
  loginItem: action === "--login-item",
142
142
  autostart: options,
143
- }));
143
+ });
144
+ server.setOnPowerAction(powerAction);
145
+ // The update button installs the npm release and restarts onto it through the launcher.
146
+ const { startWebUpdate } = await import("../services/web-update-runtime.js");
147
+ const { launcherPath } = await import("../services/runtime-record.js");
148
+ const { ommsDir } = await import("../services/runtime-handoff.js");
149
+ const { existsSync } = await import("node:fs");
150
+ // Every restart onto the new copy runs the launcher, the login item's included.
151
+ const canRestart = () => existsSync(launcherPath(ommsDir()));
152
+ server.setWebUpdate(startWebUpdate(() => powerAction("update"), canRestart));
144
153
  // A newer `web install` asked this web app to give up the port.
145
154
  server.setOnStepAside(async () => {
146
155
  clearInterval(keeper);
@@ -1,6 +1,8 @@
1
1
  import type { WebAutostartOptions } from "../services/web-autostart.js";
2
2
  /** The two choices the page offers; the web server passes them through unchanged. */
3
3
  export type PowerAction = "stop" | "restart";
4
+ /** A restart through the launcher, so the newest copy on the machine serves after an update. */
5
+ export type ProcessAction = PowerAction | "update";
4
6
  interface SpawnedChild {
5
7
  pid?: number;
6
8
  unref(): void;
@@ -16,6 +18,7 @@ export interface PowerDeps {
16
18
  stdio: "ignore";
17
19
  cwd: string;
18
20
  windowsHide: true;
21
+ env: NodeJS.ProcessEnv;
19
22
  }) => SpawnedChild;
20
23
  /** Restart the login item through the service manager. False when it cannot. */
21
24
  restartLoginItem: () => boolean | Promise<boolean>;
@@ -23,14 +26,29 @@ export interface PowerDeps {
23
26
  writeStartLock: (pid: number) => void | Promise<void>;
24
27
  /** Remove the start lock when it names `pid`. */
25
28
  removeStartLock: (pid: number) => void | Promise<void>;
26
- /** True when an OMMS web app answers health at `baseUrl`. */
27
- probe: (baseUrl: string) => Promise<boolean>;
29
+ /**
30
+ * The OMMS web app that answers at `baseUrl`, or null when none does. The
31
+ * version is null when this process may not read it, for example behind basic auth.
32
+ */
33
+ readOwner: (baseUrl: string) => Promise<{
34
+ instance: string;
35
+ version: string | null;
36
+ } | null>;
37
+ /** Ask the web app at `baseUrl` to step aside for `version`. True when it agrees. */
38
+ stepAside: (baseUrl: string, version: string) => Promise<boolean>;
39
+ /** The instance id the copy reports, so the handoff knows its own copy. */
40
+ newInstance: () => string;
41
+ /** The version on disk now; an in-place upgrade changes it after this process started. */
42
+ version: () => string;
43
+ env: NodeJS.ProcessEnv;
28
44
  sleep: (ms: number) => Promise<void>;
29
45
  now: () => number;
30
46
  exit: (code: number) => void;
31
47
  /** The runtime and arguments of this process, for a detached copy. */
32
48
  execPath: string;
33
49
  args: string[];
50
+ /** The launcher at `~/.omms/bin`, or null when it is missing. */
51
+ launcher: string | null;
34
52
  cwd: string;
35
53
  pid: number;
36
54
  log: (message: string, data: Record<string, unknown>) => void | Promise<void>;
@@ -51,5 +69,5 @@ export declare function createPowerAction(options: {
51
69
  loginItem: boolean;
52
70
  autostart?: WebAutostartOptions;
53
71
  deps?: Partial<PowerDeps>;
54
- }): (action: PowerAction) => Promise<void>;
72
+ }): (action: ProcessAction) => Promise<string | undefined>;
55
73
  export {};
@@ -1,10 +1,24 @@
1
1
  import { spawn as nodeSpawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { existsSync } from "node:fs";
2
4
  import { homedir } from "node:os";
5
+ import { packageVersion } from "../services/package-version.js";
6
+ import { ommsDir } from "../services/runtime-handoff.js";
7
+ import { launcherPath } from "../services/runtime-record.js";
8
+ import { compareVersions } from "../services/version-compare.js";
3
9
  /** How long a fresh copy must stay up before the old process stops serving. */
4
10
  const SPAWN_CHECK_MS = 1_000;
5
11
  /** How long the old process waits for the copy to answer before it serves again. */
6
12
  export const HANDOFF_WAIT_MS = 15_000;
7
13
  const HANDOFF_POLL_MS = 250;
14
+ let tokenHeader = null;
15
+ /** The web app runs on this machine, so the local token file is enough. Read once. */
16
+ function localTokenHeader() {
17
+ tokenHeader ??= import("../services/auth-token.js").then(({ AUTH_HEADER, getOrCreateAuthToken }) => ({
18
+ [AUTH_HEADER]: getOrCreateAuthToken(),
19
+ }));
20
+ return tokenHeader;
21
+ }
8
22
  function productionDeps(options) {
9
23
  return {
10
24
  spawn: nodeSpawn,
@@ -14,23 +28,55 @@ function productionDeps(options) {
14
28
  nodeLockFs.replace(startLockPath(), JSON.stringify({ pid, at: Date.now() }));
15
29
  },
16
30
  removeStartLock: async (pid) => (await import("../services/web-ensure.js")).removeStartLockFor(pid),
17
- probe: async (baseUrl) => {
31
+ readOwner: async (baseUrl) => {
32
+ // Health skips basic auth, so a restart can find its copy with basic auth on.
33
+ let instance;
34
+ try {
35
+ const health = await fetch(`${baseUrl}/api/health`, { signal: AbortSignal.timeout(1_000) });
36
+ const body = (await health.json());
37
+ if (!health.ok || typeof body.instance !== "string")
38
+ return null;
39
+ instance = body.instance;
40
+ }
41
+ catch {
42
+ return null;
43
+ }
18
44
  try {
19
- const response = await fetch(`${baseUrl}/api/health`, {
45
+ const response = await fetch(`${baseUrl}/api/web/status`, {
46
+ headers: await localTokenHeader(),
20
47
  signal: AbortSignal.timeout(1_000),
21
48
  });
22
49
  const body = (await response.json());
23
- return response.ok && body.success === true && body.status === "ok";
50
+ const version = response.ok && typeof body.version === "string" ? body.version : null;
51
+ return { instance, version };
52
+ }
53
+ catch {
54
+ return { instance, version: null };
55
+ }
56
+ },
57
+ stepAside: async (baseUrl, version) => {
58
+ try {
59
+ const response = await fetch(`${baseUrl}/api/web/step-aside`, {
60
+ method: "POST",
61
+ headers: { ...(await localTokenHeader()), "content-type": "application/json" },
62
+ body: JSON.stringify({ version }),
63
+ signal: AbortSignal.timeout(1_000),
64
+ });
65
+ return response.status === 202;
24
66
  }
25
67
  catch {
26
68
  return false;
27
69
  }
28
70
  },
71
+ newInstance: () => randomUUID(),
72
+ version: () => packageVersion(),
73
+ env: process.env,
29
74
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
30
75
  now: () => Date.now(),
31
76
  exit: (code) => process.exit(code),
32
77
  execPath: process.execPath,
33
78
  args: process.argv.slice(1),
79
+ launcher: existsSync(launcherPath(ommsDir())) ? launcherPath(ommsDir()) : null,
34
80
  cwd: homedir(),
35
81
  pid: process.pid,
36
82
  log: async (message, data) => (await import("../services/logger.js")).log(message, data),
@@ -49,14 +95,16 @@ export function createPowerAction(options) {
49
95
  const logFailure = (code) => Promise.resolve(deps.log("Web app restart failed", { code })).catch(() => undefined);
50
96
  let stopped = false;
51
97
  /** Start the copy, check it, stop serving, and wait until the copy answers. */
52
- async function handOff() {
98
+ async function handOff(args) {
53
99
  let child;
100
+ const instance = deps.newInstance();
54
101
  try {
55
- child = deps.spawn(deps.execPath, deps.args, {
102
+ child = deps.spawn(deps.execPath, args, {
56
103
  detached: true,
57
104
  stdio: "ignore",
58
105
  cwd: deps.cwd,
59
106
  windowsHide: true,
107
+ env: { ...deps.env, OMMS_WEB_INSTANCE: instance },
60
108
  });
61
109
  }
62
110
  catch {
@@ -82,12 +130,30 @@ export function createPowerAction(options) {
82
130
  stopped = true;
83
131
  }
84
132
  // The copy waits as a non-owner and takes the port within about 7 seconds.
85
- const began = deps.now();
133
+ let began = deps.now();
134
+ // Each older owner is asked once; another older waiter can take the port after it.
135
+ const askedAside = new Set();
86
136
  while (deps.now() - began < HANDOFF_WAIT_MS) {
87
137
  if (exited)
88
138
  return giveUp("copy-exit", copyPid);
89
- if (await deps.probe(options.baseUrl))
90
- return deps.exit(0);
139
+ const owner = await deps.readOwner(options.baseUrl);
140
+ if (owner?.instance === instance) {
141
+ deps.exit(0);
142
+ return undefined;
143
+ }
144
+ // Another web app that waited for the port took it first.
145
+ if (owner) {
146
+ const ownVersion = deps.version();
147
+ const order = owner.version === null ? null : compareVersions(owner.version, ownVersion);
148
+ if (order === null || order >= 0)
149
+ return yieldTo(child, copyPid);
150
+ if (!askedAside.has(owner.instance)) {
151
+ askedAside.add(owner.instance);
152
+ // The copy needs a fresh wait once the older web app leaves.
153
+ if (await deps.stepAside(options.baseUrl, ownVersion))
154
+ began = deps.now();
155
+ }
156
+ }
91
157
  await deps.sleep(HANDOFF_POLL_MS);
92
158
  }
93
159
  if (exited)
@@ -96,7 +162,15 @@ export function createPowerAction(options) {
96
162
  child.kill("SIGTERM");
97
163
  return giveUp("handoff", copyPid);
98
164
  }
99
- /** Keep or resume serving on the old process, and release the lock it wrote. */
165
+ /** A web app of the same or a newer version serves, so the copy would only wait. */
166
+ async function yieldTo(child, copyPid) {
167
+ child.kill("SIGTERM");
168
+ await deps.removeStartLock(copyPid);
169
+ await logFailure("other-owner");
170
+ deps.exit(0);
171
+ return undefined;
172
+ }
173
+ /** Keep or resume serving on the old process, and release the lock it wrote. Returns the code. */
100
174
  async function giveUp(code, copyPid) {
101
175
  if (copyPid !== undefined)
102
176
  await deps.removeStartLock(copyPid);
@@ -104,7 +178,7 @@ export function createPowerAction(options) {
104
178
  await deps.removeStartLock(deps.pid);
105
179
  await logFailure(code);
106
180
  if (!stopped)
107
- return;
181
+ return code;
108
182
  try {
109
183
  await options.resumeServer();
110
184
  stopped = false;
@@ -113,29 +187,39 @@ export function createPowerAction(options) {
113
187
  await logFailure(error instanceof Error ? error.name : "resume-error");
114
188
  deps.exit(1);
115
189
  }
190
+ return code;
116
191
  }
117
192
  return async (action) => {
118
193
  if (action === "stop") {
119
194
  await options.stopServer();
120
195
  deps.exit(0);
121
- return;
196
+ return undefined;
197
+ }
198
+ // After an update the copy of this process is the old version; the launcher picks the newest.
199
+ if (action === "update" && !deps.launcher) {
200
+ await logFailure("no-launcher");
201
+ return "no-launcher";
122
202
  }
203
+ const args = action === "update" ? [deps.launcher, "web"] : deps.args;
123
204
  stopped = false;
124
205
  try {
125
206
  if (options.loginItem) {
207
+ // The login item runs the launcher, so the service manager restart covers an update too.
126
208
  await options.stopServer();
127
209
  stopped = true;
128
- if (await deps.restartLoginItem())
129
- return deps.exit(0);
210
+ if (await deps.restartLoginItem()) {
211
+ deps.exit(0);
212
+ return undefined;
213
+ }
130
214
  }
131
215
  else {
132
216
  // Hold other hosts off until the copy owns the port.
133
217
  await deps.writeStartLock(deps.pid);
134
218
  }
135
- await handOff();
219
+ return await handOff(args);
136
220
  }
137
221
  catch (error) {
138
- await giveUp(error instanceof Error ? error.name : "unknown");
222
+ return giveUp(error instanceof Error ? error.name : "unknown");
139
223
  }
140
224
  };
141
225
  }
package/dist/config.d.ts CHANGED
@@ -54,6 +54,8 @@ interface OmmsConfig {
54
54
  from: string;
55
55
  to: string;
56
56
  }>;
57
+ /** Unresolved directories the Directory maps page hides. Display only. Global config only. */
58
+ importIgnoredDirectories?: string[];
57
59
  /** Claude Code's folder (its transcripts are in `<folder>/projects`). Empty means `CLAUDE_CONFIG_DIR`, then `~/.claude`. Global config only. */
58
60
  claudeConfigDir?: string;
59
61
  webServerAutoStart?: boolean;
@@ -104,7 +106,7 @@ interface OmmsConfig {
104
106
  injectOn?: "first" | "always";
105
107
  };
106
108
  }
107
- export declare const CONFIG_TEMPLATE = "{\n // ============================================\n // omms (Opinionated Modular Memory System) Configuration\n // ============================================\n \n // Storage location for the vector database. Leave unset to use the\n // default ~/.omms/data (a legacy ~/.opencode-mem/data store is migrated\n // there automatically on first start; see docs/omms-migration.md).\n // \"storagePath\": \"~/.omms/data\",\n\n \"userEmailOverride\": \"\",\n \"userNameOverride\": \"\",\n \n // ============================================\n // Embedding Model (for similarity search)\n // ============================================\n // Local = Hugging Face / ONNX via @huggingface/transformers (not Apple MLX).\n // Remote = set embeddingApiUrl (OpenAI-compatible /embeddings); embeddingApiKey is optional.\n \n // Default: Nomic Embed v1 (768 dimensions, 8192 context, multilingual)\n \"embeddingModel\": \"Xenova/nomic-embed-text-v1\",\n\n // Opt-in Nomic task prefixes (search_document: / search_query:). After enabling,\n // re-index existing memories so store and query vectors stay aligned.\n // \"embeddingUseTaskPrefixes\": true,\n \n // Auto-detected dimensions (no need to set manually)\n // \"embeddingDimensions\": 768,\n \n // Other recommended local models:\n // \"embeddingModel\": \"Xenova/jina-embeddings-v2-base-en\", // 768 dims, English-only, 8192 context\n // \"embeddingModel\": \"Xenova/jina-embeddings-v2-small-en\", // 512 dims, faster, 8192 context\n // \"embeddingModel\": \"Xenova/all-MiniLM-L6-v2\", // 384 dims, very fast, 512 context\n // \"embeddingModel\": \"Xenova/all-mpnet-base-v2\", // 768 dims, good quality, 512 context\n \n // Optional: OpenAI-compatible API for embeddings (URL required; key only if the server needs one)\n // \"embeddingApiUrl\": \"https://api.openai.com/v1\",\n // \"embeddingApiKey\": \"env://OPENAI_API_KEY\", // or \"sk-...\" / \"file:///path/to/key\"\n // \"embeddingModel\": \"text-embedding-3-small\", // 1536 dims, auto-detected\n \n // ============================================\n // Web Server Settings\n // ============================================\n \n // Start a background import of past chats on each host's next start.\n \"autoBackfill\": true,\n // \"piBackfillModel\": \"inherit\", // or \"external\", or \"provider/model\"\n // \"opencodeBackfillModel\": \"inherit\", // or \"external\", or \"provider/model\"\n // Directory maps for history recorded in moved or deleted directories.\n // \"importPathMaps\": [{ \"from\": \"~/code/app-feat-x\", \"to\": \"~/code/app\" }],\n // Claude Code's folder, when it is not ~/.claude (like CLAUDE_CONFIG_DIR).\n // \"claudeConfigDir\": \"~/.claude-work\",\n\n // Register the web app to start when you log in.\n \"webServerAutoStart\": true,\n // Enable web UI for managing memories (accessible at http://localhost:4747)\n \"webServerEnabled\": true,\n \n // Port for web UI server\n \"webServerPort\": 4747,\n \n // Host address for web UI (use 127.0.0.1 for local only, 0.0.0.0 for network access)\n \"webServerHost\": \"127.0.0.1\",\n\n // Optional HTTP Basic Auth for the web UI. Accepts literal, env://, or file:// secrets.\n // \"webServerAuthPassword\": \"\",\n // \"webServerAuthUsername\": \"\",\n\n // Deprecated: imported once into the API token table, then no longer read.\n // Create API tokens on the Settings page (Keys and access) instead.\n // \"webServerApiToken\": \"env://OMMS_WEB_TOKEN\",\n \n // ============================================\n // Database Settings\n // ============================================\n \n // Maximum vectors per database shard (auto-creates new shard when limit reached)\n \"maxVectorsPerShard\": 50000,\n \n // Automatically delete old memories based on retention period\n \"autoCleanupEnabled\": true,\n \n // Days to keep memories before auto-cleanup (only if autoCleanupEnabled is true)\n \"autoCleanupRetentionDays\": 30,\n \n // Automatically detect and remove duplicate memories\n \"deduplicationEnabled\": true,\n \n // Similarity threshold (0-1) for detecting duplicates (higher = stricter)\n \"deduplicationSimilarityThreshold\": 0.90,\n \n // ============================================\n // Memory Scope Settings\n // ============================================\n\n // Default scope for memory list/search queries\n // \"project\" keeps queries within the current project, \"all-projects\" searches across all project shards\n \"memory\": {\n \"defaultScope\": \"project\"\n },\n\n // ============================================\n // Model for auto-capture and profile learning (same rule in OpenCode and Pi)\n // ============================================\n\n // Which model summarises your work, in this order:\n // 1. The host model below: opencodeProvider/opencodeModel in OpenCode,\n // piProvider/piModel in Pi. Set the model to \"inherit\" to follow\n // whatever model the session is using, or to \"external\" to send\n // every call to the external API below.\n // 2. If no host model is set: the external API (memoryModel/memoryApiUrl/\n // memoryApiKey further down).\n // 3. If neither is set: the session's own model.\n // If the host model fails and the external API is configured, the external\n // API is used instead.\n //\n // Host models go through OpenCode's or Pi's own sign-in (OAuth like Claude\n // Pro/Max, GitHub Copilot, ChatGPT, or your own key there), so no separate API\n // key is needed here.\n //\n // Examples (OpenCode names from 'opencode providers list'; Pi names from its model list):\n // \"opencodeProvider\": \"anthropic\", \"opencodeModel\": \"claude-haiku-4-5-20251001\"\n // \"opencodeProvider\": \"openai\", \"opencodeModel\": \"inherit\"\n // \"piProvider\": \"openai-codex\", \"piModel\": \"gpt-5.6-luna\"\n //\n // \"opencodeProvider\": \"anthropic\",\n // \"opencodeModel\": \"claude-haiku-4-5-20251001\",\n // \"piProvider\": \"openai-codex\",\n // \"piModel\": \"gpt-5.6-luna\",\n\n // ============================================\n // Auto-Capture Settings\n // ============================================\n \n // Auto-capture runs in the background without blocking your main session.\n // Note: Ollama may not support tool calling. Use OpenAI, Anthropic, or Groq for best results.\n \n \"autoCaptureEnabled\": true,\n \n // Provider type: \"openai-chat\" | \"openai-responses\" | \"anthropic\" | \"minimax\" | \"orcarouter\" | \"google-gemini\"\n // Note: \"openai-chat\" is a generic OpenAI API-compatible mode.\n // Any service that follows the OpenAI Chat Completions API can use it via custom \"memoryApiUrl\".\n \"memoryProvider\": \"openai-chat\",\n \n // External API (step 2 above, and the fallback when a host model fails).\n // Uncomment all 3 lines and replace memoryApiKey before use:\n // \"memoryModel\": \"gpt-4o-mini\",\n // \"memoryApiUrl\": \"https://api.openai.com/v1\",\n // \"memoryApiKey\": \"sk-...\",\n\n // API Key Formats:\n // Direct value: \"sk-...\"\n // From file: \"file://~/.config/litellm-key.txt\"\n // From env variable: \"env://LITELLM_API_KEY\"\n \n // Examples for different providers:\n // Any OpenAI-compatible endpoint can use the \"openai-chat\" provider pattern below.\n // Common examples: DeepSeek, Qwen (via Alibaba Cloud ModelStudio),\n // Zhipu GLM (BigModel platform), and Kimi (Moonshot AI platform).\n\n // OpenAI Chat Completion (default, backward compatible):\n // \"memoryProvider\": \"openai-chat\"\n // \"memoryModel\": \"gpt-4o-mini\"\n // \"memoryApiUrl\": \"https://api.openai.com/v1\"\n // \"memoryApiKey\": \"sk-...\"\n\n // DeepSeek (OpenAI-compatible example):\n // \"memoryProvider\": \"openai-chat\"\n // \"memoryModel\": \"deepseek-chat\"\n // \"memoryApiUrl\": \"https://api.deepseek.com/v1\"\n // \"memoryApiKey\": \"sk-...\"\n \n // OpenAI Responses API (recommended, with session support):\n // \"memoryProvider\": \"openai-responses\"\n // \"memoryModel\": \"gpt-4o\"\n // \"memoryApiUrl\": \"https://api.openai.com/v1\"\n // \"memoryApiKey\": \"sk-...\"\n \n // Anthropic (with session support):\n // \"memoryProvider\": \"anthropic\"\n // \"memoryModel\": \"claude-3-5-haiku-20241022\"\n // \"memoryApiUrl\": \"https://api.anthropic.com/v1\"\n // \"memoryApiKey\": \"sk-ant-...\"\n\n // MiniMax (Anthropic Messages-compatible endpoint, with session support):\n // \"memoryProvider\": \"minimax\"\n // \"memoryModel\": \"MiniMax-M3\"\n // \"memoryApiUrl\": \"https://api.minimax.io\" // global endpoint\n // \"memoryApiKey\": \"<MiniMax API key>\"\n // // China endpoint: \"memoryApiUrl\": \"https://api.minimaxi.com\"\n // // Optional adaptive thinking for MiniMax-M3:\n // \"memoryExtraParams\": { \"thinking\": { \"type\": \"adaptive\" } }\n\n // OrcaRouter (OpenAI-compatible gateway, namespaced model IDs, with session support):\n // \"memoryProvider\": \"orcarouter\"\n // \"memoryApiKey\": \"<OrcaRouter API key>\"\n // // memoryApiUrl and memoryModel are optional \u2014 they default to\n // // https://api.orcarouter.ai/v1 and \"orcarouter/auto\" (a routing alias).\n // // OrcaRouter rejects bare model names, so if you set memoryModel, use a\n // // namespaced ID such as \"openai/gpt-5.5\" or \"deepseek/deepseek-v4-flash\".\n // \"memoryModel\": \"openai/gpt-5.5\"\n\n // Groq (OpenAI-compatible, use openai-chat provider):\n // \"memoryProvider\": \"openai-chat\"\n // \"memoryModel\": \"llama-3.3-70b-versatile\"\n // \"memoryApiUrl\": \"https://api.groq.com/openai/v1\"\n // \"memoryApiKey\": \"gsk_...\"\n \n // Maximum iterations for multi-turn AI analysis (for openai-responses, anthropic, and minimax)\n \"autoCaptureMaxIterations\": 5,\n \n // Timeout per iteration in milliseconds (30 seconds default)\n \"autoCaptureIterationTimeout\": 30000,\n\n // Tries for one turn's capture, on Pi and OpenCode, 2 and 4 seconds apart. After the\n // last one fails, a turn the model could not reach goes to the capture retry queue.\n \"autoCaptureMaxRetries\": 3,\n\n // Maximum UTF-8 bytes for the auto-capture markdown context sent to the summary model.\n // Prevents HTTP 400 context overflows on models with ~131K token windows (e.g. Groq Llama).\n // Rough guide: tokens \u2248 bytes / 4 for mixed code/prose.\n \"autoCaptureMaxContextBytes\": 131072,\n \n // Days to keep AI session history before cleanup\n \"aiSessionRetentionDays\": 7,\n\n // Capture diagnostics: every capture attempt always writes one metadata line\n // (model, stop reason, sizes, outcome) to ~/.omms/omms.log, with no\n // conversation text. Set captureTrace to true to also write each attempt's\n // prompt and raw reply to ~/.omms/traces/ for debugging. Traces can contain\n // conversation content: <private> text and common API key formats are\n // redacted, files are readable only by you, and they are deleted after\n // captureTraceRetentionDays. Only this global file can turn tracing on.\n // \"captureTrace\": false,\n // \"captureTraceRetentionDays\": 7,\n // \"captureAttemptRetentionDays\": 30,\n\n // Capture retry queue: when a live capture fails because the model cannot be\n // reached (network error, timeout, HTTP 408, 429 or 5xx), OMMS keeps a copy of\n // the turn in ~/.omms/data and tries it again later. The copy has <private>\n // text and common API key formats removed. Turns are deleted when the retry\n // ends or after captureRetryRetentionHours (0 to 720). 0 turns the queue off.\n // Only this global file can set it.\n // \"captureRetryRetentionHours\": 72,\n\n // Temperature for AI API requests (set to false to omit parameter for models that don't support it)\n // Some reasoning models (like o1, o3, gpt-5) don't support temperature parameter\n // Set to false and add \"memoryTemperature\": false in config when using such models\n \"memoryTemperature\": 0.3,\n\n // Extra parameters to include in API request body\n // Useful for local inference servers (e.g. llama-server with --jinja) that support\n // additional parameters like disabling thinking/reasoning mode\n // Example for Qwen3 models: { \"enable_thinking\": false }\n // \"memoryExtraParams\": {},\n\n // Language for auto-capture summaries (default: \"auto\" for auto-detection)\n // Options: \"auto\", \"en\", \"id\", \"zh\", \"ja\", \"es\", \"fr\", \"de\", \"ru\", \"pt\", \"ar\", \"ko\"\n // \"autoCaptureLanguage\": \"auto\",\n\n // ============================================\n // Toast Notifications\n // ============================================\n\n // Show toast when memory is auto-captured\n \"showAutoCaptureToasts\": true,\n\n // Show toast when user profile is updated\n \"showUserProfileToasts\": true,\n\n // Show toast for error messages\n \"showErrorToasts\": true,\n\n // ============================================\n // User Profile System\n // ============================================\n\n // Analyze user prompts every N prompts to build/update your user profile\n // When N uncaptured prompts accumulate, AI will analyze them to identify:\n // - User preferences (code style, communication style, tool preferences)\n // - User patterns (recurring topics, problem domains, technical interests)\n // - User workflows (development habits, sequences, learning style)\n // - Skill level (overall and per-domain assessment)\n \"userProfileAnalysisInterval\": 10,\n\n // Days before inactive items (all types) are eligible for removal\n \"userProfileStaleDays\": 2,\n\n // Number of preferences shown in UI\n \"userProfileDisplayPreferences\": 20,\n \n // Number of patterns shown in UI\n \"userProfileDisplayPatterns\": 15,\n \n // Number of workflows shown in UI\n \"userProfileDisplayWorkflows\": 10,\n \n // Number of preferences injected into LLM conversation context\n // Keep this small \u2014 the strongest signals are enough; more dilute LLM attention\n \"userProfileInjectPreferences\": 5,\n \n // Number of patterns injected into LLM conversation context\n \"userProfileInjectPatterns\": 5,\n \n // Number of workflows injected into LLM conversation context\n \"userProfileInjectWorkflows\": 3,\n \n // Days before preference confidence starts to decay (if not reinforced)\n // Preferences that aren't seen again will gradually lose confidence and be removed\n \"userProfileConfidenceDecayDays\": 30,\n \n // Number of profile versions to keep in changelog (for rollback/debugging)\n // Older versions are automatically cleaned up\n \"userProfileChangelogRetentionCount\": 5,\n\n // Minimum evidence count for a preference/pattern to survive confidence decay\n // Items confirmed fewer times are more likely to be pruned when confidence decays\n \"userProfileMinEvidenceForRetention\": 3,\n\n // Periodically merge duplicate or irrelevant profile items with the configured AI provider\n \"userProfileAutoCleanupEnabled\": true,\n // Number of analyzed user prompts between automatic AI cleanup runs\n \"userProfileAutoCleanupInterval\": 100,\n\n // Enable LLM validation of existing preferences against recent behavior.\n // When enabled, each analysis round checks if top-5 preferences still match recent prompts.\n // Experimental \u2014 disabled by default.\n \"userProfileValidationEnabled\": false,\n\n // ============================================\n // Search Settings\n // ============================================\n \n // Minimum similarity score (0-1) for memory search results\n \"similarityThreshold\": 0.6,\n\n // Maximum number of memories to return in search results.\n // Manual searches can ask for fewer; prompt retrieval uses this ceiling.\n \"maxMemories\": 10,\n\n // ============================================\n // Memory Context Controls\n // ============================================\n\n // Recent memories added at fresh session start (OpenCode V1 and Claude Code).\n // Pi and OpenCode V2 search the store with each prompt instead.\n \"chatMessage\": {\n \"enabled\": true,\n // Recent memories injected at session start.\n \"maxMemories\": 3,\n \"excludeCurrentSession\": true,\n // When to inject: \"first\" message of a session or \"always\".\n // \"injectOn\": \"first\",\n },\n\n // OpenCode profile-learning input limit, in UTF-8 bytes. Smaller values can\n // omit prompts from that input. Does not limit Pi or Claude Code profile input.\n \"userProfileMaxContextBytes\": 32768,\n\n // Shared budget for automatic memory context (profile text, memories, and\n // formatting) across all hosts, in approximate tokens: ceil(UTF-8 bytes / 4).\n // Covers prompt retrieval, recent-memory injection, and session-memory\n // restoration. Not an exact provider token limit or a spending limit.\n \"retrievalMaxTokens\": 2000,\n\n // ============================================\n // Advanced Settings\n // ============================================\n \n // Inject user profile into AI context (preferences, patterns, workflows)\n \"injectProfile\": true\n}\n";
109
+ export declare const CONFIG_TEMPLATE = "{\n // ============================================\n // omms (Opinionated Modular Memory System) Configuration\n // ============================================\n \n // Storage location for the vector database. Leave unset to use the\n // default ~/.omms/data (a legacy ~/.opencode-mem/data store is migrated\n // there automatically on first start; see docs/omms-migration.md).\n // \"storagePath\": \"~/.omms/data\",\n\n \"userEmailOverride\": \"\",\n \"userNameOverride\": \"\",\n \n // ============================================\n // Embedding Model (for similarity search)\n // ============================================\n // Local = Hugging Face / ONNX via @huggingface/transformers (not Apple MLX).\n // Remote = set embeddingApiUrl (OpenAI-compatible /embeddings); embeddingApiKey is optional.\n \n // Default: Nomic Embed v1 (768 dimensions, 8192 context, multilingual)\n \"embeddingModel\": \"Xenova/nomic-embed-text-v1\",\n\n // Opt-in Nomic task prefixes (search_document: / search_query:). After enabling,\n // re-index existing memories so store and query vectors stay aligned.\n // \"embeddingUseTaskPrefixes\": true,\n \n // Auto-detected dimensions (no need to set manually)\n // \"embeddingDimensions\": 768,\n \n // Other recommended local models:\n // \"embeddingModel\": \"Xenova/jina-embeddings-v2-base-en\", // 768 dims, English-only, 8192 context\n // \"embeddingModel\": \"Xenova/jina-embeddings-v2-small-en\", // 512 dims, faster, 8192 context\n // \"embeddingModel\": \"Xenova/all-MiniLM-L6-v2\", // 384 dims, very fast, 512 context\n // \"embeddingModel\": \"Xenova/all-mpnet-base-v2\", // 768 dims, good quality, 512 context\n \n // Optional: OpenAI-compatible API for embeddings (URL required; key only if the server needs one)\n // \"embeddingApiUrl\": \"https://api.openai.com/v1\",\n // \"embeddingApiKey\": \"env://OPENAI_API_KEY\", // or \"sk-...\" / \"file:///path/to/key\"\n // \"embeddingModel\": \"text-embedding-3-small\", // 1536 dims, auto-detected\n \n // ============================================\n // Web Server Settings\n // ============================================\n \n // Start a background import of past chats on each host's next start.\n \"autoBackfill\": true,\n // \"piBackfillModel\": \"inherit\", // or \"external\", or \"provider/model\"\n // \"opencodeBackfillModel\": \"inherit\", // or \"external\", or \"provider/model\"\n // Directory maps for history recorded in moved or deleted directories.\n // \"importPathMaps\": [{ \"from\": \"~/code/app-feat-x\", \"to\": \"~/code/app\" }],\n // Unresolved directories to hide on the Directory maps page. They stay unimported.\n // \"importIgnoredDirectories\": [\"/private/tmp/scratch\"],\n // Claude Code's folder, when it is not ~/.claude (like CLAUDE_CONFIG_DIR).\n // \"claudeConfigDir\": \"~/.claude-work\",\n\n // Register the web app to start when you log in.\n \"webServerAutoStart\": true,\n // Enable web UI for managing memories (accessible at http://localhost:4747)\n \"webServerEnabled\": true,\n \n // Port for web UI server\n \"webServerPort\": 4747,\n \n // Host address for web UI (use 127.0.0.1 for local only, 0.0.0.0 for network access)\n \"webServerHost\": \"127.0.0.1\",\n\n // Optional HTTP Basic Auth for the web UI. Accepts literal, env://, or file:// secrets.\n // \"webServerAuthPassword\": \"\",\n // \"webServerAuthUsername\": \"\",\n\n // Deprecated: imported once into the API token table, then no longer read.\n // Create API tokens on the Settings page (Keys and access) instead.\n // \"webServerApiToken\": \"env://OMMS_WEB_TOKEN\",\n \n // ============================================\n // Database Settings\n // ============================================\n \n // Maximum vectors per database shard (auto-creates new shard when limit reached)\n \"maxVectorsPerShard\": 50000,\n \n // Automatically delete old memories based on retention period\n \"autoCleanupEnabled\": true,\n \n // Days to keep memories before auto-cleanup (only if autoCleanupEnabled is true)\n \"autoCleanupRetentionDays\": 30,\n \n // Automatically detect and remove duplicate memories\n \"deduplicationEnabled\": true,\n \n // Similarity threshold (0-1) for detecting duplicates (higher = stricter)\n \"deduplicationSimilarityThreshold\": 0.90,\n \n // ============================================\n // Memory Scope Settings\n // ============================================\n\n // Default scope for memory list/search queries\n // \"project\" keeps queries within the current project, \"all-projects\" searches across all project shards\n \"memory\": {\n \"defaultScope\": \"project\"\n },\n\n // ============================================\n // Model for auto-capture and profile learning (same rule in OpenCode and Pi)\n // ============================================\n\n // Which model summarises your work, in this order:\n // 1. The host model below: opencodeProvider/opencodeModel in OpenCode,\n // piProvider/piModel in Pi. Set the model to \"inherit\" to follow\n // whatever model the session is using, or to \"external\" to send\n // every call to the external API below.\n // 2. If no host model is set: the external API (memoryModel/memoryApiUrl/\n // memoryApiKey further down).\n // 3. If neither is set: the session's own model.\n // If the host model fails and the external API is configured, the external\n // API is used instead.\n //\n // Host models go through OpenCode's or Pi's own sign-in (OAuth like Claude\n // Pro/Max, GitHub Copilot, ChatGPT, or your own key there), so no separate API\n // key is needed here.\n //\n // Examples (OpenCode names from 'opencode providers list'; Pi names from its model list):\n // \"opencodeProvider\": \"anthropic\", \"opencodeModel\": \"claude-haiku-4-5-20251001\"\n // \"opencodeProvider\": \"openai\", \"opencodeModel\": \"inherit\"\n // \"piProvider\": \"openai-codex\", \"piModel\": \"gpt-5.6-luna\"\n //\n // \"opencodeProvider\": \"anthropic\",\n // \"opencodeModel\": \"claude-haiku-4-5-20251001\",\n // \"piProvider\": \"openai-codex\",\n // \"piModel\": \"gpt-5.6-luna\",\n\n // ============================================\n // Auto-Capture Settings\n // ============================================\n \n // Auto-capture runs in the background without blocking your main session.\n // Note: Ollama may not support tool calling. Use OpenAI, Anthropic, or Groq for best results.\n \n \"autoCaptureEnabled\": true,\n \n // Provider type: \"openai-chat\" | \"openai-responses\" | \"anthropic\" | \"minimax\" | \"orcarouter\" | \"google-gemini\"\n // Note: \"openai-chat\" is a generic OpenAI API-compatible mode.\n // Any service that follows the OpenAI Chat Completions API can use it via custom \"memoryApiUrl\".\n \"memoryProvider\": \"openai-chat\",\n \n // External API (step 2 above, and the fallback when a host model fails).\n // Uncomment all 3 lines and replace memoryApiKey before use:\n // \"memoryModel\": \"gpt-4o-mini\",\n // \"memoryApiUrl\": \"https://api.openai.com/v1\",\n // \"memoryApiKey\": \"sk-...\",\n\n // API Key Formats:\n // Direct value: \"sk-...\"\n // From file: \"file://~/.config/litellm-key.txt\"\n // From env variable: \"env://LITELLM_API_KEY\"\n \n // Examples for different providers:\n // Any OpenAI-compatible endpoint can use the \"openai-chat\" provider pattern below.\n // Common examples: DeepSeek, Qwen (via Alibaba Cloud ModelStudio),\n // Zhipu GLM (BigModel platform), and Kimi (Moonshot AI platform).\n\n // OpenAI Chat Completion (default, backward compatible):\n // \"memoryProvider\": \"openai-chat\"\n // \"memoryModel\": \"gpt-4o-mini\"\n // \"memoryApiUrl\": \"https://api.openai.com/v1\"\n // \"memoryApiKey\": \"sk-...\"\n\n // DeepSeek (OpenAI-compatible example):\n // \"memoryProvider\": \"openai-chat\"\n // \"memoryModel\": \"deepseek-chat\"\n // \"memoryApiUrl\": \"https://api.deepseek.com/v1\"\n // \"memoryApiKey\": \"sk-...\"\n \n // OpenAI Responses API (recommended, with session support):\n // \"memoryProvider\": \"openai-responses\"\n // \"memoryModel\": \"gpt-4o\"\n // \"memoryApiUrl\": \"https://api.openai.com/v1\"\n // \"memoryApiKey\": \"sk-...\"\n \n // Anthropic (with session support):\n // \"memoryProvider\": \"anthropic\"\n // \"memoryModel\": \"claude-3-5-haiku-20241022\"\n // \"memoryApiUrl\": \"https://api.anthropic.com/v1\"\n // \"memoryApiKey\": \"sk-ant-...\"\n\n // MiniMax (Anthropic Messages-compatible endpoint, with session support):\n // \"memoryProvider\": \"minimax\"\n // \"memoryModel\": \"MiniMax-M3\"\n // \"memoryApiUrl\": \"https://api.minimax.io\" // global endpoint\n // \"memoryApiKey\": \"<MiniMax API key>\"\n // // China endpoint: \"memoryApiUrl\": \"https://api.minimaxi.com\"\n // // Optional adaptive thinking for MiniMax-M3:\n // \"memoryExtraParams\": { \"thinking\": { \"type\": \"adaptive\" } }\n\n // OrcaRouter (OpenAI-compatible gateway, namespaced model IDs, with session support):\n // \"memoryProvider\": \"orcarouter\"\n // \"memoryApiKey\": \"<OrcaRouter API key>\"\n // // memoryApiUrl and memoryModel are optional \u2014 they default to\n // // https://api.orcarouter.ai/v1 and \"orcarouter/auto\" (a routing alias).\n // // OrcaRouter rejects bare model names, so if you set memoryModel, use a\n // // namespaced ID such as \"openai/gpt-5.5\" or \"deepseek/deepseek-v4-flash\".\n // \"memoryModel\": \"openai/gpt-5.5\"\n\n // Groq (OpenAI-compatible, use openai-chat provider):\n // \"memoryProvider\": \"openai-chat\"\n // \"memoryModel\": \"llama-3.3-70b-versatile\"\n // \"memoryApiUrl\": \"https://api.groq.com/openai/v1\"\n // \"memoryApiKey\": \"gsk_...\"\n \n // Maximum iterations for multi-turn AI analysis (for openai-responses, anthropic, and minimax)\n \"autoCaptureMaxIterations\": 5,\n \n // Timeout per iteration in milliseconds (30 seconds default)\n \"autoCaptureIterationTimeout\": 30000,\n\n // Tries for one turn's capture, on Pi and OpenCode, 2 and 4 seconds apart. After the\n // last one fails, a turn the model could not reach goes to the capture retry queue.\n \"autoCaptureMaxRetries\": 3,\n\n // Maximum UTF-8 bytes for the auto-capture markdown context sent to the summary model.\n // Prevents HTTP 400 context overflows on models with ~131K token windows (e.g. Groq Llama).\n // Rough guide: tokens \u2248 bytes / 4 for mixed code/prose.\n \"autoCaptureMaxContextBytes\": 131072,\n \n // Days to keep AI session history before cleanup\n \"aiSessionRetentionDays\": 7,\n\n // Capture diagnostics: every capture attempt always writes one metadata line\n // (model, stop reason, sizes, outcome) to ~/.omms/omms.log, with no\n // conversation text. Set captureTrace to true to also write each attempt's\n // prompt and raw reply to ~/.omms/traces/ for debugging. Traces can contain\n // conversation content: <private> text and common API key formats are\n // redacted, files are readable only by you, and they are deleted after\n // captureTraceRetentionDays. Only this global file can turn tracing on.\n // \"captureTrace\": false,\n // \"captureTraceRetentionDays\": 7,\n // \"captureAttemptRetentionDays\": 30,\n\n // Capture retry queue: when a live capture fails because the model cannot be\n // reached (network error, timeout, HTTP 408, 429 or 5xx), OMMS keeps a copy of\n // the turn in ~/.omms/data and tries it again later. The copy has <private>\n // text and common API key formats removed. Turns are deleted when the retry\n // ends or after captureRetryRetentionHours (0 to 720). 0 turns the queue off.\n // Only this global file can set it.\n // \"captureRetryRetentionHours\": 72,\n\n // Temperature for AI API requests (set to false to omit parameter for models that don't support it)\n // Some reasoning models (like o1, o3, gpt-5) don't support temperature parameter\n // Set to false and add \"memoryTemperature\": false in config when using such models\n \"memoryTemperature\": 0.3,\n\n // Extra parameters to include in API request body\n // Useful for local inference servers (e.g. llama-server with --jinja) that support\n // additional parameters like disabling thinking/reasoning mode\n // Example for Qwen3 models: { \"enable_thinking\": false }\n // \"memoryExtraParams\": {},\n\n // Language for auto-capture summaries (default: \"auto\" for auto-detection)\n // Options: \"auto\", \"en\", \"id\", \"zh\", \"ja\", \"es\", \"fr\", \"de\", \"ru\", \"pt\", \"ar\", \"ko\"\n // \"autoCaptureLanguage\": \"auto\",\n\n // ============================================\n // Toast Notifications\n // ============================================\n\n // Show toast when memory is auto-captured\n \"showAutoCaptureToasts\": true,\n\n // Show toast when user profile is updated\n \"showUserProfileToasts\": true,\n\n // Show toast for error messages\n \"showErrorToasts\": true,\n\n // ============================================\n // User Profile System\n // ============================================\n\n // Analyze user prompts every N prompts to build/update your user profile\n // When N uncaptured prompts accumulate, AI will analyze them to identify:\n // - User preferences (code style, communication style, tool preferences)\n // - User patterns (recurring topics, problem domains, technical interests)\n // - User workflows (development habits, sequences, learning style)\n // - Skill level (overall and per-domain assessment)\n \"userProfileAnalysisInterval\": 10,\n\n // Days before inactive items (all types) are eligible for removal\n \"userProfileStaleDays\": 2,\n\n // Number of preferences shown in UI\n \"userProfileDisplayPreferences\": 20,\n \n // Number of patterns shown in UI\n \"userProfileDisplayPatterns\": 15,\n \n // Number of workflows shown in UI\n \"userProfileDisplayWorkflows\": 10,\n \n // Number of preferences injected into LLM conversation context\n // Keep this small \u2014 the strongest signals are enough; more dilute LLM attention\n \"userProfileInjectPreferences\": 5,\n \n // Number of patterns injected into LLM conversation context\n \"userProfileInjectPatterns\": 5,\n \n // Number of workflows injected into LLM conversation context\n \"userProfileInjectWorkflows\": 3,\n \n // Days before preference confidence starts to decay (if not reinforced)\n // Preferences that aren't seen again will gradually lose confidence and be removed\n \"userProfileConfidenceDecayDays\": 30,\n \n // Number of profile versions to keep in changelog (for rollback/debugging)\n // Older versions are automatically cleaned up\n \"userProfileChangelogRetentionCount\": 5,\n\n // Minimum evidence count for a preference/pattern to survive confidence decay\n // Items confirmed fewer times are more likely to be pruned when confidence decays\n \"userProfileMinEvidenceForRetention\": 3,\n\n // Periodically merge duplicate or irrelevant profile items with the configured AI provider\n \"userProfileAutoCleanupEnabled\": true,\n // Number of analyzed user prompts between automatic AI cleanup runs\n \"userProfileAutoCleanupInterval\": 100,\n\n // Enable LLM validation of existing preferences against recent behavior.\n // When enabled, each analysis round checks if top-5 preferences still match recent prompts.\n // Experimental \u2014 disabled by default.\n \"userProfileValidationEnabled\": false,\n\n // ============================================\n // Search Settings\n // ============================================\n \n // Minimum similarity score (0-1) for memory search results\n \"similarityThreshold\": 0.6,\n\n // Maximum number of memories to return in search results.\n // Manual searches can ask for fewer; prompt retrieval uses this ceiling.\n \"maxMemories\": 10,\n\n // ============================================\n // Memory Context Controls\n // ============================================\n\n // Recent memories added at fresh session start (OpenCode V1 and Claude Code).\n // Pi and OpenCode V2 search the store with each prompt instead.\n \"chatMessage\": {\n \"enabled\": true,\n // Recent memories injected at session start.\n \"maxMemories\": 3,\n \"excludeCurrentSession\": true,\n // When to inject: \"first\" message of a session or \"always\".\n // \"injectOn\": \"first\",\n },\n\n // OpenCode profile-learning input limit, in UTF-8 bytes. Smaller values can\n // omit prompts from that input. Does not limit Pi or Claude Code profile input.\n \"userProfileMaxContextBytes\": 32768,\n\n // Shared budget for automatic memory context (profile text, memories, and\n // formatting) across all hosts, in approximate tokens: ceil(UTF-8 bytes / 4).\n // Covers prompt retrieval, recent-memory injection, and session-memory\n // restoration. Not an exact provider token limit or a spending limit.\n \"retrievalMaxTokens\": 2000,\n\n // ============================================\n // Advanced Settings\n // ============================================\n \n // Inject user profile into AI context (preferences, patterns, workflows)\n \"injectProfile\": true\n}\n";
108
110
  export declare function normalizeAutoCaptureMaxContextBytes(value: number): number;
109
111
  /** Whole hours from 0 to 720. A value that is not a number gives the default. */
110
112
  export declare function normalizeCaptureRetryRetentionHours(value: unknown): number;
@@ -150,6 +152,7 @@ declare function buildConfig(fileConfig: OmmsConfig): {
150
152
  opencodeBackfillModel: string;
151
153
  piBackfillModel: string;
152
154
  importPathMaps: import("./importer/importer.js").ImportPathMap[];
155
+ importIgnoredDirectories: string[];
153
156
  claudeConfigDir: string;
154
157
  webServerAutoStart: boolean;
155
158
  webServerEnabled: boolean;
@@ -246,6 +249,7 @@ export declare let CONFIG: {
246
249
  opencodeBackfillModel: string;
247
250
  piBackfillModel: string;
248
251
  importPathMaps: import("./importer/importer.js").ImportPathMap[];
252
+ importIgnoredDirectories: string[];
249
253
  claudeConfigDir: string;
250
254
  webServerAutoStart: boolean;
251
255
  webServerEnabled: boolean;
package/dist/config.js CHANGED
@@ -8,7 +8,7 @@ import { resolveSecretValue } from "./services/secret-resolver.js";
8
8
  import { isPlaceholderApiKey } from "./services/ai/api-key-placeholder.js";
9
9
  import { getAutoCaptureProviderStatus } from "./services/ai/live-model-choice.js";
10
10
  import { parseBackfillModel } from "./importer/backfill-model.js";
11
- import { parseImportPathMaps } from "./importer/import-path-maps.js";
11
+ import { parseIgnoredDirectories, parseImportPathMaps } from "./importer/import-path-maps.js";
12
12
  import { checkedMemoryLimit, MEMORY_LIMITS } from "./utils/memory-limits.js";
13
13
  import { resolveDefaultStoragePath, runLegacyStoreMigration, legacyMigrationPaths, } from "./services/legacy-migration.js";
14
14
  const OMMS_CONFIG_DIR = join(homedir(), ".config", "omms");
@@ -61,6 +61,7 @@ const DEFAULTS = {
61
61
  opencodeBackfillModel: "inherit",
62
62
  piBackfillModel: "inherit",
63
63
  importPathMaps: [],
64
+ importIgnoredDirectories: [],
64
65
  claudeConfigDir: "",
65
66
  webServerAutoStart: true,
66
67
  webServerEnabled: true,
@@ -209,6 +210,8 @@ export const CONFIG_TEMPLATE = `{
209
210
  // "opencodeBackfillModel": "inherit", // or "external", or "provider/model"
210
211
  // Directory maps for history recorded in moved or deleted directories.
211
212
  // "importPathMaps": [{ "from": "~/code/app-feat-x", "to": "~/code/app" }],
213
+ // Unresolved directories to hide on the Directory maps page. They stay unimported.
214
+ // "importIgnoredDirectories": ["/private/tmp/scratch"],
212
215
  // Claude Code's folder, when it is not ~/.claude (like CLAUDE_CONFIG_DIR).
213
216
  // "claudeConfigDir": "~/.claude-work",
214
217
 
@@ -646,6 +649,7 @@ function buildConfig(fileConfig) {
646
649
  parseBackfillModel(fileConfig, "pi");
647
650
  parseBackfillModel(fileConfig, "opencode");
648
651
  const importPathMaps = parseImportPathMaps(fileConfig.importPathMaps);
652
+ const importIgnoredDirectories = parseIgnoredDirectories(fileConfig.importIgnoredDirectories);
649
653
  if (fileConfig.claudeConfigDir !== undefined &&
650
654
  !isValidClaudeConfigDir(fileConfig.claudeConfigDir)) {
651
655
  throw new Error("Invalid claudeConfigDir config");
@@ -716,6 +720,7 @@ function buildConfig(fileConfig) {
716
720
  opencodeBackfillModel: fileConfig.opencodeBackfillModel ?? DEFAULTS.opencodeBackfillModel,
717
721
  piBackfillModel: fileConfig.piBackfillModel ?? DEFAULTS.piBackfillModel,
718
722
  importPathMaps,
723
+ importIgnoredDirectories,
719
724
  claudeConfigDir: expandPath(fileConfig.claudeConfigDir?.trim() ?? DEFAULTS.claudeConfigDir),
720
725
  webServerAutoStart: fileConfig.webServerAutoStart ?? DEFAULTS.webServerAutoStart,
721
726
  webServerEnabled: fileConfig.webServerEnabled ?? DEFAULTS.webServerEnabled,
@@ -874,6 +879,7 @@ export function initConfig(directory, options = {}) {
874
879
  delete projectOverrides.opencodeBackfillModel;
875
880
  delete projectOverrides.piBackfillModel;
876
881
  delete projectOverrides.importPathMaps;
882
+ delete projectOverrides.importIgnoredDirectories;
877
883
  // A checked-in project file must not point the web app at another folder.
878
884
  delete projectOverrides.claudeConfigDir;
879
885
  delete projectOverrides.webServerAutoStart;
@@ -5,6 +5,11 @@ import type { ImportPathMap } from "./importer.js";
5
5
  * the first invalid entry, like the rest of config validation.
6
6
  */
7
7
  export declare function parseImportPathMaps(value: unknown): ImportPathMap[];
8
+ /**
9
+ * Validate and normalise `importIgnoredDirectories` from the global config:
10
+ * absolute directories after `~` expansion. Throws on the first invalid entry.
11
+ */
12
+ export declare function parseIgnoredDirectories(value: unknown): string[];
8
13
  /** Saved maps with the run's own maps on top; a run map wins for the same `from`. */
9
14
  export declare function mergeImportPathMaps(saved: readonly ImportPathMap[], run: readonly ImportPathMap[]): ImportPathMap[];
10
15
  /**
@@ -30,6 +30,26 @@ export function parseImportPathMaps(value) {
30
30
  return { from: resolve(expanded.from), to: resolve(expanded.to) };
31
31
  });
32
32
  }
33
+ /**
34
+ * Validate and normalise `importIgnoredDirectories` from the global config:
35
+ * absolute directories after `~` expansion. Throws on the first invalid entry.
36
+ */
37
+ export function parseIgnoredDirectories(value) {
38
+ if (value === undefined)
39
+ return [];
40
+ if (!Array.isArray(value)) {
41
+ throw new Error("Invalid importIgnoredDirectories config: must be a list");
42
+ }
43
+ const paths = value.map((entry, index) => {
44
+ const expanded = typeof entry === "string" ? expandHome(entry.trim()) : "";
45
+ if (!expanded || !isAbsolute(expanded)) {
46
+ throw new Error(`Invalid importIgnoredDirectories config: entry ${index} needs an absolute path`);
47
+ }
48
+ return resolve(expanded);
49
+ });
50
+ // One folder written in two forms is one entry.
51
+ return [...new Set(paths)];
52
+ }
33
53
  /** Saved maps with the run's own maps on top; a run map wins for the same `from`. */
34
54
  export function mergeImportPathMaps(saved, run) {
35
55
  const byFrom = new Map();
@@ -1,23 +1,50 @@
1
1
  import type { UnresolvedDirectory } from "../services/backfill-state.js";
2
+ export type MapConfidence = "exact" | "name" | "guess";
3
+ export type IgnoreReason = "temporary" | "node_modules" | "app-data" | "skills";
4
+ export type MapSuggestion = {
5
+ kind: "map";
6
+ target: string;
7
+ confidence: MapConfidence;
8
+ } | {
9
+ kind: "ignore";
10
+ reason: IgnoreReason;
11
+ };
12
+ /** A project the user has history for. Store projects carry their recorded paths and remote. */
13
+ export interface KnownProject {
14
+ path: string;
15
+ /** Other directories the memory store recorded for the same project. */
16
+ candidates?: readonly string[];
17
+ /** The git remote the memory store recorded for the project. */
18
+ remote?: string | null;
19
+ }
2
20
  export interface MapSuggestionContext {
3
21
  /** Existing project directories the user has history for; their parents are searched too. */
4
- knownProjects?: readonly string[];
22
+ knownProjects?: readonly KnownProject[];
5
23
  /** OpenCode's recorded project worktree for a session directory, read without writing. */
6
24
  opencodeWorktree?: (directory: string) => string | null | undefined;
7
25
  /** The user's home folder; the search never climbs above it. Defaults to `homedir()`. */
8
26
  home?: string;
27
+ /** Claude Code's folder, from `resolveClaudeFolder`. Defaults to `<home>/.claude`. */
28
+ claudeFolder?: string;
9
29
  /** Folder listings already read in this request. */
10
30
  cache?: Map<string, string[]>;
31
+ /** Project-marker checks already made in this request. */
32
+ projects?: Map<string, boolean>;
33
+ /** Parent folders of the known projects, worked out once per request. */
34
+ searchRoots?: ReadonlySet<string>;
11
35
  }
12
36
  /**
13
- * Suggest an existing target for a directory that no longer exists: first the
14
- * main repository of a deleted worktree, by the longest leading part of the
15
- * directory's name or one of its parents' names, then OpenCode's recorded
16
- * project worktree. Returns null when there is no candidate.
37
+ * Suggest what to do with a directory that no longer exists. Rules run in
38
+ * order and the first result wins: (1) ignore a folder that is not a project,
39
+ * (2) the project with the same stored git remote, (3) OpenCode's recorded
40
+ * project folder, or rules 2 and 4 to 6 on it when it is gone too, (4) the
41
+ * main repository of a deleted worktree, (5) the one known project with the
42
+ * same folder name, (6) a rename guess by name parts and initials. Returns
43
+ * null when no rule gives a result. Reads folders and `.git` files only.
17
44
  */
18
- export declare function suggestMapTarget(missing: string, context?: MapSuggestionContext): string | null;
45
+ export declare function suggestMapTarget(missing: string, context?: MapSuggestionContext): MapSuggestion | null;
19
46
  export interface SuggestedDirectory extends UnresolvedDirectory {
20
- suggestion: string | null;
47
+ suggestion: MapSuggestion | null;
21
48
  }
22
49
  export declare function suggestMapTargets(directories: readonly UnresolvedDirectory[], context?: MapSuggestionContext): SuggestedDirectory[];
23
50
  /**
@@ -26,14 +53,22 @@ export declare function suggestMapTargets(directories: readonly UnresolvedDirect
26
53
  */
27
54
  export declare function readOpencodeWorktrees(dbPath: string): Promise<Map<string, string>>;
28
55
  /**
29
- * The Directory maps list: saved maps and each host's unresolved directories
30
- * from its latest run, with suggestions. Paths and counts only.
56
+ * The memory store's projects with their recorded paths and git remote, read
57
+ * once through the shard inventory. Empty when there is no store or the read
58
+ * fails, so the other sources still give suggestions. Never creates a store.
59
+ */
60
+ export declare function readStoreProjects(currentDirectory: string): Promise<KnownProject[]>;
61
+ /**
62
+ * The Directory maps list: saved maps, ignored directories, and each host's
63
+ * unresolved directories from its latest run, with suggestions. Paths and
64
+ * counts only.
31
65
  */
32
66
  export declare function directoryMapsView(options?: {
33
67
  opencodeDbPath?: string;
34
- knownProjects?: readonly string[];
68
+ knownProjects?: readonly KnownProject[];
35
69
  }): Promise<{
36
70
  saved: import("./importer.js").ImportPathMap[];
71
+ ignored: string[];
37
72
  pi: SuggestedDirectory[];
38
73
  opencode: SuggestedDirectory[];
39
74
  "claude-code": SuggestedDirectory[];