om-memory-system 3.6.0-next.42 → 3.6.0-next.44
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.
- package/dist/cli/web-command.js +9 -1
- package/dist/cli/web-power.d.ts +20 -5
- package/dist/cli/web-power.js +106 -26
- package/dist/config.d.ts +5 -1
- package/dist/config.js +11 -0
- package/dist/importer/claude-hook-api.js +3 -1
- package/dist/importer/claude-reader.d.ts +2 -2
- package/dist/importer/claude-reader.js +6 -6
- package/dist/importer/import-readiness.js +1 -1
- package/dist/importer/import-sessions.js +1 -1
- package/dist/importer/import-sources.d.ts +4 -8
- package/dist/importer/import-sources.js +10 -11
- package/dist/importer/run-import.js +11 -1
- package/dist/services/claude-folder.d.ts +16 -0
- package/dist/services/claude-folder.js +24 -0
- package/dist/services/global-config-writer.js +5 -0
- package/dist/services/settings-snapshot.d.ts +5 -0
- package/dist/services/settings-snapshot.js +8 -0
- package/dist/services/web-ensure.d.ts +19 -1
- package/dist/services/web-ensure.js +57 -8
- package/dist/services/web-server.d.ts +1 -0
- package/dist/services/web-server.js +13 -2
- package/dist/web/assets/index-Bh5aFNPX.js +124 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/index-Ctzj_Zg0.js +0 -124
package/dist/cli/web-command.js
CHANGED
|
@@ -173,7 +173,7 @@ export async function runWebCommand(args, options = {}, online, handover = {}) {
|
|
|
173
173
|
});
|
|
174
174
|
console.log(`OMMS web app: ${server.getUrl()}`);
|
|
175
175
|
// The Node HTTP adapter unrefs its socket for plugin use. Keep the CLI alive.
|
|
176
|
-
|
|
176
|
+
let keeper = setInterval(() => { }, 60_000);
|
|
177
177
|
const stop = () => {
|
|
178
178
|
clearInterval(keeper);
|
|
179
179
|
process.off("SIGINT", stop);
|
|
@@ -191,6 +191,14 @@ export async function runWebCommand(args, options = {}, online, handover = {}) {
|
|
|
191
191
|
process.off("SIGTERM", stop);
|
|
192
192
|
await server.stop();
|
|
193
193
|
},
|
|
194
|
+
// A restart whose copy failed serves again from this process.
|
|
195
|
+
resumeServer: async () => {
|
|
196
|
+
keeper = setInterval(() => { }, 60_000);
|
|
197
|
+
process.once("SIGINT", stop);
|
|
198
|
+
process.once("SIGTERM", stop);
|
|
199
|
+
await server.start();
|
|
200
|
+
},
|
|
201
|
+
baseUrl: server.getUrl(),
|
|
194
202
|
loginItem: action === "--login-item",
|
|
195
203
|
autostart: options,
|
|
196
204
|
}));
|
package/dist/cli/web-power.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import type { WebAutostartOptions } from "../services/web-autostart.js";
|
|
2
|
+
/** The two choices the page offers; the web server passes them through unchanged. */
|
|
2
3
|
export type PowerAction = "stop" | "restart";
|
|
3
4
|
interface SpawnedChild {
|
|
4
5
|
pid?: number;
|
|
5
6
|
unref(): void;
|
|
6
|
-
on(event: "error", listener: (
|
|
7
|
+
on(event: "error" | "exit", listener: () => void): unknown;
|
|
8
|
+
kill(signal?: NodeJS.Signals): boolean;
|
|
7
9
|
}
|
|
10
|
+
/** How long the old process waits for the copy to answer before it serves again. */
|
|
11
|
+
export declare const HANDOFF_WAIT_MS = 15000;
|
|
12
|
+
/** Injected so tests check the restart order without spawning or exiting. */
|
|
8
13
|
export interface PowerDeps {
|
|
9
14
|
spawn: (command: string, args: string[], options: {
|
|
10
15
|
detached: true;
|
|
@@ -16,6 +21,12 @@ export interface PowerDeps {
|
|
|
16
21
|
restartLoginItem: () => boolean | Promise<boolean>;
|
|
17
22
|
/** Write the start lock, naming `pid`, so a host that starts meanwhile waits. */
|
|
18
23
|
writeStartLock: (pid: number) => void | Promise<void>;
|
|
24
|
+
/** Remove the start lock when it names `pid`. */
|
|
25
|
+
removeStartLock: (pid: number) => void | Promise<void>;
|
|
26
|
+
/** True when an OMMS web app answers health at `baseUrl`. */
|
|
27
|
+
probe: (baseUrl: string) => Promise<boolean>;
|
|
28
|
+
sleep: (ms: number) => Promise<void>;
|
|
29
|
+
now: () => number;
|
|
19
30
|
exit: (code: number) => void;
|
|
20
31
|
/** The runtime and arguments of this process, for a detached copy. */
|
|
21
32
|
execPath: string;
|
|
@@ -26,13 +37,17 @@ export interface PowerDeps {
|
|
|
26
37
|
}
|
|
27
38
|
/**
|
|
28
39
|
* The callback the web server runs after it replies to Stop or Restart.
|
|
29
|
-
* `stopServer` stops serving and any keep-alive timer
|
|
30
|
-
* copy on the same port: the service manager for the
|
|
31
|
-
* copy otherwise. A detached child of the login item
|
|
32
|
-
* launchd job or systemd control group.
|
|
40
|
+
* `stopServer` stops serving and any keep-alive timer; `resumeServer` undoes it.
|
|
41
|
+
* Restart starts a fresh copy on the same port: the service manager for the
|
|
42
|
+
* login item, and a detached copy otherwise. A detached child of the login item
|
|
43
|
+
* would die with its launchd job or systemd control group. A restart never
|
|
44
|
+
* leaves the port empty because the copy failed: the old process serves again.
|
|
33
45
|
*/
|
|
34
46
|
export declare function createPowerAction(options: {
|
|
35
47
|
stopServer: () => Promise<void>;
|
|
48
|
+
resumeServer: () => Promise<void>;
|
|
49
|
+
/** The URL this web app serves, where the copy must answer after the handoff. */
|
|
50
|
+
baseUrl: string;
|
|
36
51
|
loginItem: boolean;
|
|
37
52
|
autostart?: WebAutostartOptions;
|
|
38
53
|
deps?: Partial<PowerDeps>;
|
package/dist/cli/web-power.js
CHANGED
|
@@ -1,15 +1,33 @@
|
|
|
1
1
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
+
/** How long a fresh copy must stay up before the old process stops serving. */
|
|
4
|
+
const SPAWN_CHECK_MS = 1_000;
|
|
5
|
+
/** How long the old process waits for the copy to answer before it serves again. */
|
|
6
|
+
export const HANDOFF_WAIT_MS = 15_000;
|
|
7
|
+
const HANDOFF_POLL_MS = 250;
|
|
3
8
|
function productionDeps(options) {
|
|
4
9
|
return {
|
|
5
10
|
spawn: nodeSpawn,
|
|
6
11
|
restartLoginItem: async () => (await import("../services/web-autostart.js")).restartWebAutostart(options),
|
|
7
12
|
writeStartLock: async (pid) => {
|
|
8
13
|
const { nodeLockFs, startLockPath } = await import("../services/web-ensure.js");
|
|
9
|
-
|
|
10
|
-
nodeLockFs.remove(path);
|
|
11
|
-
nodeLockFs.createExclusive(path, JSON.stringify({ pid, at: Date.now() }));
|
|
14
|
+
nodeLockFs.replace(startLockPath(), JSON.stringify({ pid, at: Date.now() }));
|
|
12
15
|
},
|
|
16
|
+
removeStartLock: async (pid) => (await import("../services/web-ensure.js")).removeStartLockFor(pid),
|
|
17
|
+
probe: async (baseUrl) => {
|
|
18
|
+
try {
|
|
19
|
+
const response = await fetch(`${baseUrl}/api/health`, {
|
|
20
|
+
signal: AbortSignal.timeout(1_000),
|
|
21
|
+
});
|
|
22
|
+
const body = (await response.json());
|
|
23
|
+
return response.ok && body.success === true && body.status === "ok";
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
30
|
+
now: () => Date.now(),
|
|
13
31
|
exit: (code) => process.exit(code),
|
|
14
32
|
execPath: process.execPath,
|
|
15
33
|
args: process.argv.slice(1),
|
|
@@ -20,42 +38,104 @@ function productionDeps(options) {
|
|
|
20
38
|
}
|
|
21
39
|
/**
|
|
22
40
|
* The callback the web server runs after it replies to Stop or Restart.
|
|
23
|
-
* `stopServer` stops serving and any keep-alive timer
|
|
24
|
-
* copy on the same port: the service manager for the
|
|
25
|
-
* copy otherwise. A detached child of the login item
|
|
26
|
-
* launchd job or systemd control group.
|
|
41
|
+
* `stopServer` stops serving and any keep-alive timer; `resumeServer` undoes it.
|
|
42
|
+
* Restart starts a fresh copy on the same port: the service manager for the
|
|
43
|
+
* login item, and a detached copy otherwise. A detached child of the login item
|
|
44
|
+
* would die with its launchd job or systemd control group. A restart never
|
|
45
|
+
* leaves the port empty because the copy failed: the old process serves again.
|
|
27
46
|
*/
|
|
28
47
|
export function createPowerAction(options) {
|
|
29
48
|
const deps = { ...productionDeps(options.autostart ?? {}), ...options.deps };
|
|
49
|
+
const logFailure = (code) => Promise.resolve(deps.log("Web app restart failed", { code })).catch(() => undefined);
|
|
50
|
+
let stopped = false;
|
|
51
|
+
/** Start the copy, check it, stop serving, and wait until the copy answers. */
|
|
52
|
+
async function handOff() {
|
|
53
|
+
let child;
|
|
54
|
+
try {
|
|
55
|
+
child = deps.spawn(deps.execPath, deps.args, {
|
|
56
|
+
detached: true,
|
|
57
|
+
stdio: "ignore",
|
|
58
|
+
cwd: deps.cwd,
|
|
59
|
+
windowsHide: true,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return giveUp("spawn-error");
|
|
64
|
+
}
|
|
65
|
+
let failed = false;
|
|
66
|
+
let exited = false;
|
|
67
|
+
child.on("error", () => (failed = true));
|
|
68
|
+
child.on("exit", () => (exited = true));
|
|
69
|
+
child.unref();
|
|
70
|
+
const copyPid = child.pid;
|
|
71
|
+
if (!copyPid)
|
|
72
|
+
return giveUp("spawn-error");
|
|
73
|
+
// A missing script or a broken runtime ends the copy within this time.
|
|
74
|
+
await deps.sleep(SPAWN_CHECK_MS);
|
|
75
|
+
if (failed)
|
|
76
|
+
return giveUp("spawn-error");
|
|
77
|
+
if (exited)
|
|
78
|
+
return giveUp("copy-exit");
|
|
79
|
+
await deps.writeStartLock(copyPid);
|
|
80
|
+
if (!stopped) {
|
|
81
|
+
await options.stopServer();
|
|
82
|
+
stopped = true;
|
|
83
|
+
}
|
|
84
|
+
// The copy waits as a non-owner and takes the port within about 7 seconds.
|
|
85
|
+
const began = deps.now();
|
|
86
|
+
while (deps.now() - began < HANDOFF_WAIT_MS) {
|
|
87
|
+
if (exited)
|
|
88
|
+
return giveUp("copy-exit", copyPid);
|
|
89
|
+
if (await deps.probe(options.baseUrl))
|
|
90
|
+
return deps.exit(0);
|
|
91
|
+
await deps.sleep(HANDOFF_POLL_MS);
|
|
92
|
+
}
|
|
93
|
+
if (exited)
|
|
94
|
+
return giveUp("copy-exit", copyPid);
|
|
95
|
+
// A copy that never answers would wait as a non-owner for ever.
|
|
96
|
+
child.kill("SIGTERM");
|
|
97
|
+
return giveUp("handoff", copyPid);
|
|
98
|
+
}
|
|
99
|
+
/** Keep or resume serving on the old process, and release the lock it wrote. */
|
|
100
|
+
async function giveUp(code, copyPid) {
|
|
101
|
+
if (copyPid !== undefined)
|
|
102
|
+
await deps.removeStartLock(copyPid);
|
|
103
|
+
else if (!options.loginItem)
|
|
104
|
+
await deps.removeStartLock(deps.pid);
|
|
105
|
+
await logFailure(code);
|
|
106
|
+
if (!stopped)
|
|
107
|
+
return;
|
|
108
|
+
try {
|
|
109
|
+
await options.resumeServer();
|
|
110
|
+
stopped = false;
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
await logFailure(error instanceof Error ? error.name : "resume-error");
|
|
114
|
+
deps.exit(1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
30
117
|
return async (action) => {
|
|
31
118
|
if (action === "stop") {
|
|
32
119
|
await options.stopServer();
|
|
33
120
|
deps.exit(0);
|
|
34
121
|
return;
|
|
35
122
|
}
|
|
123
|
+
stopped = false;
|
|
36
124
|
try {
|
|
37
|
-
|
|
38
|
-
|
|
125
|
+
if (options.loginItem) {
|
|
126
|
+
await options.stopServer();
|
|
127
|
+
stopped = true;
|
|
128
|
+
if (await deps.restartLoginItem())
|
|
129
|
+
return deps.exit(0);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// Hold other hosts off until the copy owns the port.
|
|
39
133
|
await deps.writeStartLock(deps.pid);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return deps.exit(0);
|
|
43
|
-
const child = deps.spawn(deps.execPath, deps.args, {
|
|
44
|
-
detached: true,
|
|
45
|
-
stdio: "ignore",
|
|
46
|
-
cwd: deps.cwd,
|
|
47
|
-
windowsHide: true,
|
|
48
|
-
});
|
|
49
|
-
child.on("error", () => undefined);
|
|
50
|
-
child.unref();
|
|
51
|
-
if (child.pid)
|
|
52
|
-
await deps.writeStartLock(child.pid);
|
|
134
|
+
}
|
|
135
|
+
await handOff();
|
|
53
136
|
}
|
|
54
137
|
catch (error) {
|
|
55
|
-
await
|
|
56
|
-
code: error instanceof Error ? error.name : "unknown",
|
|
57
|
-
})).catch(() => undefined);
|
|
138
|
+
await giveUp(error instanceof Error ? error.name : "unknown");
|
|
58
139
|
}
|
|
59
|
-
deps.exit(0);
|
|
60
140
|
};
|
|
61
141
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -52,6 +52,8 @@ interface OmmsConfig {
|
|
|
52
52
|
from: string;
|
|
53
53
|
to: string;
|
|
54
54
|
}>;
|
|
55
|
+
/** Claude Code's folder (its transcripts are in `<folder>/projects`). Empty means `CLAUDE_CONFIG_DIR`, then `~/.claude`. Global config only. */
|
|
56
|
+
claudeConfigDir?: string;
|
|
55
57
|
webServerAutoStart?: boolean;
|
|
56
58
|
webServerEnabled?: boolean;
|
|
57
59
|
webServerPort?: number;
|
|
@@ -100,7 +102,7 @@ interface OmmsConfig {
|
|
|
100
102
|
injectOn?: "first" | "always";
|
|
101
103
|
};
|
|
102
104
|
}
|
|
103
|
-
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 BOTH embeddingApiUrl and embeddingApiKey (OpenAI-compatible /embeddings).\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 (both URL and key required)\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\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 // Required when webServerHost is not loopback. Protects /api/* with Bearer / X-Omms-Token (legacy X-Opencode-Mem-Token still works).\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 \"maxMemories\": 10,\n\n // ============================================\n // Advanced Settings\n // ============================================\n \n // Inject user profile into AI context (preferences, patterns, workflows)\n \"injectProfile\": true\n}\n";
|
|
105
|
+
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 BOTH embeddingApiUrl and embeddingApiKey (OpenAI-compatible /embeddings).\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 (both URL and key required)\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 // Required when webServerHost is not loopback. Protects /api/* with Bearer / X-Omms-Token (legacy X-Opencode-Mem-Token still works).\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 \"maxMemories\": 10,\n\n // ============================================\n // Advanced Settings\n // ============================================\n \n // Inject user profile into AI context (preferences, patterns, workflows)\n \"injectProfile\": true\n}\n";
|
|
104
106
|
export declare function normalizeAutoCaptureMaxContextBytes(value: number): number;
|
|
105
107
|
/** Whole hours from 0 to 720. A value that is not a number gives the default. */
|
|
106
108
|
export declare function normalizeCaptureRetryRetentionHours(value: unknown): number;
|
|
@@ -145,6 +147,7 @@ declare function buildConfig(fileConfig: OmmsConfig): {
|
|
|
145
147
|
opencodeBackfillModel: string;
|
|
146
148
|
piBackfillModel: string;
|
|
147
149
|
importPathMaps: import("./importer/importer.js").ImportPathMap[];
|
|
150
|
+
claudeConfigDir: string;
|
|
148
151
|
webServerAutoStart: boolean;
|
|
149
152
|
webServerEnabled: boolean;
|
|
150
153
|
webServerPort: number;
|
|
@@ -239,6 +242,7 @@ export declare let CONFIG: {
|
|
|
239
242
|
opencodeBackfillModel: string;
|
|
240
243
|
piBackfillModel: string;
|
|
241
244
|
importPathMaps: import("./importer/importer.js").ImportPathMap[];
|
|
245
|
+
claudeConfigDir: string;
|
|
242
246
|
webServerAutoStart: boolean;
|
|
243
247
|
webServerEnabled: boolean;
|
|
244
248
|
webServerPort: number;
|
package/dist/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, mkdirSync, writeFileSync, statSync } from "no
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { stripJsoncComments } from "./services/jsonc.js";
|
|
5
|
+
import { isValidClaudeConfigDir } from "./services/claude-folder.js";
|
|
5
6
|
import { log } from "./services/logger.js";
|
|
6
7
|
import { resolveSecretValue } from "./services/secret-resolver.js";
|
|
7
8
|
import { isPlaceholderApiKey } from "./services/ai/api-key-placeholder.js";
|
|
@@ -58,6 +59,7 @@ const DEFAULTS = {
|
|
|
58
59
|
opencodeBackfillModel: "inherit",
|
|
59
60
|
piBackfillModel: "inherit",
|
|
60
61
|
importPathMaps: [],
|
|
62
|
+
claudeConfigDir: "",
|
|
61
63
|
webServerAutoStart: true,
|
|
62
64
|
webServerEnabled: true,
|
|
63
65
|
webServerPort: 4747,
|
|
@@ -205,6 +207,8 @@ export const CONFIG_TEMPLATE = `{
|
|
|
205
207
|
// "opencodeBackfillModel": "inherit", // or "external", or "provider/model"
|
|
206
208
|
// Directory maps for history recorded in moved or deleted directories.
|
|
207
209
|
// "importPathMaps": [{ "from": "~/code/app-feat-x", "to": "~/code/app" }],
|
|
210
|
+
// Claude Code's folder, when it is not ~/.claude (like CLAUDE_CONFIG_DIR).
|
|
211
|
+
// "claudeConfigDir": "~/.claude-work",
|
|
208
212
|
|
|
209
213
|
// Register the web app to start when you log in.
|
|
210
214
|
"webServerAutoStart": true,
|
|
@@ -599,6 +603,10 @@ function buildConfig(fileConfig) {
|
|
|
599
603
|
parseBackfillModel(fileConfig, "pi");
|
|
600
604
|
parseBackfillModel(fileConfig, "opencode");
|
|
601
605
|
const importPathMaps = parseImportPathMaps(fileConfig.importPathMaps);
|
|
606
|
+
if (fileConfig.claudeConfigDir !== undefined &&
|
|
607
|
+
!isValidClaudeConfigDir(fileConfig.claudeConfigDir)) {
|
|
608
|
+
throw new Error("Invalid claudeConfigDir config");
|
|
609
|
+
}
|
|
602
610
|
for (const key of ["autoBackfill", "webServerAutoStart"]) {
|
|
603
611
|
if (fileConfig[key] !== undefined && typeof fileConfig[key] !== "boolean") {
|
|
604
612
|
throw new Error(`Invalid ${key} config`);
|
|
@@ -663,6 +671,7 @@ function buildConfig(fileConfig) {
|
|
|
663
671
|
opencodeBackfillModel: fileConfig.opencodeBackfillModel ?? DEFAULTS.opencodeBackfillModel,
|
|
664
672
|
piBackfillModel: fileConfig.piBackfillModel ?? DEFAULTS.piBackfillModel,
|
|
665
673
|
importPathMaps,
|
|
674
|
+
claudeConfigDir: expandPath(fileConfig.claudeConfigDir?.trim() ?? DEFAULTS.claudeConfigDir),
|
|
666
675
|
webServerAutoStart: fileConfig.webServerAutoStart ?? DEFAULTS.webServerAutoStart,
|
|
667
676
|
webServerEnabled: fileConfig.webServerEnabled ?? DEFAULTS.webServerEnabled,
|
|
668
677
|
webServerPort: fileConfig.webServerPort ?? DEFAULTS.webServerPort,
|
|
@@ -820,6 +829,8 @@ export function initConfig(directory, options = {}) {
|
|
|
820
829
|
delete projectOverrides.opencodeBackfillModel;
|
|
821
830
|
delete projectOverrides.piBackfillModel;
|
|
822
831
|
delete projectOverrides.importPathMaps;
|
|
832
|
+
// A checked-in project file must not point the web app at another folder.
|
|
833
|
+
delete projectOverrides.claudeConfigDir;
|
|
823
834
|
delete projectOverrides.webServerAutoStart;
|
|
824
835
|
delete projectOverrides.webServerEnabled;
|
|
825
836
|
const merged = { ...globalConfig, ...projectOverrides };
|
|
@@ -92,7 +92,7 @@ function checkedTranscriptPath(transcriptPath, sessionId) {
|
|
|
92
92
|
if (basename(transcriptPath) !== fileName || basename(real) !== fileName) {
|
|
93
93
|
throw new ClaudeHookRequestError("transcript_path must be named <session_id>.jsonl");
|
|
94
94
|
}
|
|
95
|
-
const root = realPathOrResolved(defaultClaudeProjectsRoot());
|
|
95
|
+
const root = realPathOrResolved(defaultClaudeProjectsRoot(CONFIG.claudeConfigDir));
|
|
96
96
|
if (!real.startsWith(root + sep)) {
|
|
97
97
|
throw new ClaudeHookRequestError("transcript_path must be under the Claude projects folder");
|
|
98
98
|
}
|
|
@@ -107,6 +107,8 @@ function parseCaptureRequest(body) {
|
|
|
107
107
|
throw new ClaudeHookRequestError("transcript_path must be an absolute .jsonl path");
|
|
108
108
|
}
|
|
109
109
|
const { sessionId, cwd } = sessionAndCwd(input);
|
|
110
|
+
// The path check uses `claudeConfigDir`, which the Settings page may just have changed.
|
|
111
|
+
refreshConfigIfChanged(cwd);
|
|
110
112
|
return {
|
|
111
113
|
sessionId,
|
|
112
114
|
cwd,
|
|
@@ -16,8 +16,8 @@ import { type ImportPathMap, type ImportProjectReport, type LazyImportSource } f
|
|
|
16
16
|
* time.
|
|
17
17
|
*/
|
|
18
18
|
export declare const CLAUDE_HEADER_READ_LIMIT: number;
|
|
19
|
-
/**
|
|
20
|
-
export declare function defaultClaudeProjectsRoot(): string;
|
|
19
|
+
/** The Claude projects folder; `claudeConfigDir` is `CONFIG.claudeConfigDir`. */
|
|
20
|
+
export declare function defaultClaudeProjectsRoot(claudeConfigDir?: string): string;
|
|
21
21
|
export interface DiscoveredClaudeSession {
|
|
22
22
|
file: string;
|
|
23
23
|
/** Path relative to the root, with `/` separators; the file name for a file root. */
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, statSync, } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
2
|
import { basename, join } from "node:path";
|
|
4
3
|
import { StringDecoder } from "node:string_decoder";
|
|
4
|
+
import { CONFIG } from "../config.js";
|
|
5
|
+
import { claudeProjectsRoot } from "../services/claude-folder.js";
|
|
5
6
|
import { extractScopeFromContainerTag } from "../services/memory-scope.js";
|
|
6
7
|
import { getTags } from "../services/tags.js";
|
|
7
8
|
import { extractClaudeConversationWindows, parseClaudeTranscript, parseClaudeTranscriptLine, } from "./claude-conversation.js";
|
|
@@ -23,10 +24,9 @@ import { countImportableUnits, projectFilterTag, selectImportWindows, } from "./
|
|
|
23
24
|
*/
|
|
24
25
|
export const CLAUDE_HEADER_READ_LIMIT = 1024 * 1024;
|
|
25
26
|
const READ_CHUNK = 64 * 1024;
|
|
26
|
-
/**
|
|
27
|
-
export function defaultClaudeProjectsRoot() {
|
|
28
|
-
|
|
29
|
-
return join(configDir || join(homedir(), ".claude"), "projects");
|
|
27
|
+
/** The Claude projects folder; `claudeConfigDir` is `CONFIG.claudeConfigDir`. */
|
|
28
|
+
export function defaultClaudeProjectsRoot(claudeConfigDir) {
|
|
29
|
+
return claudeProjectsRoot(claudeConfigDir);
|
|
30
30
|
}
|
|
31
31
|
/** A missing folder is an empty history. Any other read error is reported, not hidden. */
|
|
32
32
|
function readEntries(dir, unreadable) {
|
|
@@ -128,7 +128,7 @@ function isFile(path) {
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
export function discoverClaudeSessions(options = {}) {
|
|
131
|
-
const root = options.root ?? defaultClaudeProjectsRoot();
|
|
131
|
+
const root = options.root ?? defaultClaudeProjectsRoot(CONFIG.claudeConfigDir);
|
|
132
132
|
const sessions = [];
|
|
133
133
|
const unrecognized = [];
|
|
134
134
|
if (!existsSync(root))
|
|
@@ -37,7 +37,7 @@ export async function importReadiness(deps = {}) {
|
|
|
37
37
|
external: { state, provider: CONFIG.memoryProvider, model: CONFIG.memoryModel ?? null },
|
|
38
38
|
opencode: { available: opencode.available, models: opencode.models ?? [] },
|
|
39
39
|
piReader,
|
|
40
|
-
claudeCode: claudeReadiness(deps.claudeRoot?.() ?? defaultClaudeSourcePath()),
|
|
40
|
+
claudeCode: claudeReadiness(deps.claudeRoot?.() ?? defaultClaudeSourcePath(CONFIG.claudeConfigDir)),
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
function claudeReadiness(defaultRoot) {
|
|
@@ -136,7 +136,7 @@ export function validateSessionListRequest(value) {
|
|
|
136
136
|
/** One page of session metadata; no prompts, replies, or tool data. */
|
|
137
137
|
export async function listImportSessions(request, options) {
|
|
138
138
|
const source = request.sourceToken === undefined
|
|
139
|
-
? validateImportSource(options.host, defaultImportSourcePath(options.host))
|
|
139
|
+
? validateImportSource(options.host, defaultImportSourcePath(options.host, (await import("../config.js")).CONFIG.claudeConfigDir))
|
|
140
140
|
: null;
|
|
141
141
|
const token = source?.sourceToken ?? request.sourceToken;
|
|
142
142
|
const identity = readImportSourceToken(token, options.host);
|
|
@@ -22,13 +22,9 @@ export declare function readImportSourceToken(token: unknown, host: ImportHost):
|
|
|
22
22
|
export declare function importSourceKey(identity: ImportSourceIdentity): string;
|
|
23
23
|
/** Refuse a source whose path now names another file, such as a swapped-in symlink. */
|
|
24
24
|
export declare function assertImportSourceUnchanged(identity: ImportSourceIdentity): void;
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
* `tests/import-sources.test.ts` checks that the two agree.
|
|
29
|
-
*/
|
|
30
|
-
export declare function defaultClaudeSourcePath(): string;
|
|
31
|
-
export declare function defaultImportSourcePath(host: ImportHost): string;
|
|
25
|
+
/** The Claude projects folder; `claudeConfigDir` is `CONFIG.claudeConfigDir`. */
|
|
26
|
+
export declare function defaultClaudeSourcePath(claudeConfigDir?: string): string;
|
|
27
|
+
export declare function defaultImportSourcePath(host: ImportHost, claudeConfigDir?: string): string;
|
|
32
28
|
export interface ValidatedImportSource {
|
|
33
29
|
kind: ImportSourceKind;
|
|
34
30
|
displayPath: string;
|
|
@@ -45,7 +41,7 @@ export interface BrowseEntry {
|
|
|
45
41
|
* One folder at a time, with no recursion: subfolders and files the host can
|
|
46
42
|
* import. A Claude Code source is always a folder, so it lists folders only. Symlinked entries are hidden. Only offered on loopback binds.
|
|
47
43
|
*/
|
|
48
|
-
export declare function browseImportSources(host: ImportHost, path?: unknown): {
|
|
44
|
+
export declare function browseImportSources(host: ImportHost, path?: unknown, claudeConfigDir?: string): {
|
|
49
45
|
path: string;
|
|
50
46
|
parent: string | null;
|
|
51
47
|
entries: BrowseEntry[];
|
|
@@ -2,6 +2,7 @@ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
|
2
2
|
import { closeSync, existsSync, openSync, readdirSync, readSync, realpathSync, statSync, } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, isAbsolute, join } from "node:path";
|
|
5
|
+
import { claudeProjectsRoot } from "../services/claude-folder.js";
|
|
5
6
|
import { DEFAULT_PI_SESSION_ROOT, readPiSessionHeader } from "./discovery.js";
|
|
6
7
|
import { DEFAULT_OPENCODE_DB } from "./opencode-reader.js";
|
|
7
8
|
export class ImportSourceError extends Error {
|
|
@@ -79,19 +80,15 @@ function isSqliteFile(path) {
|
|
|
79
80
|
closeSync(fd);
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
* `tests/import-sources.test.ts` checks that the two agree.
|
|
86
|
-
*/
|
|
87
|
-
export function defaultClaudeSourcePath() {
|
|
88
|
-
return join(homedir(), ".claude", "projects");
|
|
83
|
+
/** The Claude projects folder; `claudeConfigDir` is `CONFIG.claudeConfigDir`. */
|
|
84
|
+
export function defaultClaudeSourcePath(claudeConfigDir) {
|
|
85
|
+
return claudeProjectsRoot(claudeConfigDir);
|
|
89
86
|
}
|
|
90
|
-
export function defaultImportSourcePath(host) {
|
|
87
|
+
export function defaultImportSourcePath(host, claudeConfigDir) {
|
|
91
88
|
if (host === "pi")
|
|
92
89
|
return DEFAULT_PI_SESSION_ROOT;
|
|
93
90
|
if (host === "claude-code")
|
|
94
|
-
return defaultClaudeSourcePath();
|
|
91
|
+
return defaultClaudeSourcePath(claudeConfigDir);
|
|
95
92
|
return DEFAULT_OPENCODE_DB;
|
|
96
93
|
}
|
|
97
94
|
/** Check an absolute path's kind and format; never returns file contents. */
|
|
@@ -130,10 +127,12 @@ const BROWSE_LIMIT = 500;
|
|
|
130
127
|
* One folder at a time, with no recursion: subfolders and files the host can
|
|
131
128
|
* import. A Claude Code source is always a folder, so it lists folders only. Symlinked entries are hidden. Only offered on loopback binds.
|
|
132
129
|
*/
|
|
133
|
-
export function browseImportSources(host, path) {
|
|
130
|
+
export function browseImportSources(host, path, claudeConfigDir) {
|
|
134
131
|
let start = path;
|
|
135
132
|
if (start === undefined || start === null || start === "") {
|
|
136
|
-
const preferred = host === "opencode"
|
|
133
|
+
const preferred = host === "opencode"
|
|
134
|
+
? dirname(DEFAULT_OPENCODE_DB)
|
|
135
|
+
: defaultImportSourcePath(host, claudeConfigDir);
|
|
137
136
|
start = existsSync(preferred) ? preferred : homedir();
|
|
138
137
|
}
|
|
139
138
|
const folder = checkedPath(start);
|
|
@@ -150,7 +150,17 @@ async function runUntracked(host, args, run) {
|
|
|
150
150
|
};
|
|
151
151
|
if (host === "claude-code") {
|
|
152
152
|
const { importClaudeHistory } = await import("./claude-import.js");
|
|
153
|
-
|
|
153
|
+
if (args.source)
|
|
154
|
+
return importClaudeHistory(deps, filters);
|
|
155
|
+
// Without --root, read the folder live capture uses.
|
|
156
|
+
const [{ CONFIG }, { claudeProjectsRoot }] = await Promise.all([
|
|
157
|
+
import("../config.js"),
|
|
158
|
+
import("../services/claude-folder.js"),
|
|
159
|
+
]);
|
|
160
|
+
return importClaudeHistory(deps, {
|
|
161
|
+
...filters,
|
|
162
|
+
root: claudeProjectsRoot(CONFIG.claudeConfigDir),
|
|
163
|
+
});
|
|
154
164
|
}
|
|
155
165
|
const { importPiHistory } = await import("./importer.js");
|
|
156
166
|
const { loadPiSessionForImport } = await import("./session-loader.js");
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Claude Code folder, in one place for every caller: the `claudeConfigDir`
|
|
3
|
+
* setting, then `CLAUDE_CONFIG_DIR`, then `~/.claude`. Pure on purpose: callers
|
|
4
|
+
* pass `CONFIG.claudeConfigDir`, because many tests stub `src/config.js`.
|
|
5
|
+
* It sits in services so the settings snapshot can use it too.
|
|
6
|
+
*/
|
|
7
|
+
export type ClaudeFolderSource = "setting" | "env" | "default";
|
|
8
|
+
export interface ClaudeFolder {
|
|
9
|
+
folder: string;
|
|
10
|
+
source: ClaudeFolderSource;
|
|
11
|
+
}
|
|
12
|
+
/** Empty, an absolute path, or `~/...`. A relative path would depend on the web app's working directory. */
|
|
13
|
+
export declare function isValidClaudeConfigDir(value: unknown): value is string;
|
|
14
|
+
export declare function resolveClaudeFolder(configured: string | undefined, env?: string | undefined, home?: string): ClaudeFolder;
|
|
15
|
+
/** `<Claude folder>/projects`, where Claude Code writes its transcripts. */
|
|
16
|
+
export declare function claudeProjectsRoot(configured: string | undefined, env?: string, home?: string): string;
|