om-memory-system 3.0.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.
- package/LICENSE +21 -0
- package/README.md +587 -0
- package/dist/adapters/opencode/auto-capture-host.d.ts +3 -0
- package/dist/adapters/opencode/auto-capture-host.js +29 -0
- package/dist/adapters/opencode/auto-capture-summary.d.ts +4 -0
- package/dist/adapters/opencode/auto-capture-summary.js +147 -0
- package/dist/adapters/opencode/conversation.d.ts +2 -0
- package/dist/adapters/opencode/conversation.js +50 -0
- package/dist/adapters/opencode/user-prompt.d.ts +9 -0
- package/dist/adapters/opencode/user-prompt.js +26 -0
- package/dist/adapters/pi/capture.d.ts +41 -0
- package/dist/adapters/pi/capture.js +91 -0
- package/dist/adapters/pi/conversation.d.ts +38 -0
- package/dist/adapters/pi/conversation.js +118 -0
- package/dist/adapters/pi/extension.d.ts +18 -0
- package/dist/adapters/pi/extension.js +234 -0
- package/dist/adapters/pi/import-command.d.ts +29 -0
- package/dist/adapters/pi/import-command.js +232 -0
- package/dist/adapters/pi/profile.d.ts +39 -0
- package/dist/adapters/pi/profile.js +148 -0
- package/dist/adapters/pi/provider.d.ts +50 -0
- package/dist/adapters/pi/provider.js +77 -0
- package/dist/config.d.ts +290 -0
- package/dist/config.js +742 -0
- package/dist/core/capture-context.d.ts +4 -0
- package/dist/core/capture-context.js +132 -0
- package/dist/core/capture.d.ts +15 -0
- package/dist/core/capture.js +68 -0
- package/dist/core/extraction.d.ts +80 -0
- package/dist/core/extraction.js +140 -0
- package/dist/core/host.d.ts +52 -0
- package/dist/core/host.js +1 -0
- package/dist/core/memory-operations.d.ts +27 -0
- package/dist/core/memory-operations.js +247 -0
- package/dist/core/retrieval.d.ts +12 -0
- package/dist/core/retrieval.js +72 -0
- package/dist/importer/discovery.d.ts +31 -0
- package/dist/importer/discovery.js +88 -0
- package/dist/importer/importer.d.ts +87 -0
- package/dist/importer/importer.js +260 -0
- package/dist/importer/ledger.d.ts +44 -0
- package/dist/importer/ledger.js +137 -0
- package/dist/importer/session-loader.d.ts +19 -0
- package/dist/importer/session-loader.js +12 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +601 -0
- package/dist/plugin.d.ts +9 -0
- package/dist/plugin.js +11 -0
- package/dist/services/ai/ai-provider-factory.d.ts +7 -0
- package/dist/services/ai/ai-provider-factory.js +41 -0
- package/dist/services/ai/api-key-placeholder.d.ts +1 -0
- package/dist/services/ai/api-key-placeholder.js +14 -0
- package/dist/services/ai/internal-capture-sessions.d.ts +6 -0
- package/dist/services/ai/internal-capture-sessions.js +33 -0
- package/dist/services/ai/opencode-diagnostics.d.ts +7 -0
- package/dist/services/ai/opencode-diagnostics.js +36 -0
- package/dist/services/ai/opencode-host-config.d.ts +10 -0
- package/dist/services/ai/opencode-host-config.js +51 -0
- package/dist/services/ai/opencode-provider-loader.d.ts +3 -0
- package/dist/services/ai/opencode-provider-loader.js +4 -0
- package/dist/services/ai/opencode-provider.d.ts +92 -0
- package/dist/services/ai/opencode-provider.js +419 -0
- package/dist/services/ai/opencode-sdk-client.d.ts +6 -0
- package/dist/services/ai/opencode-sdk-client.js +38 -0
- package/dist/services/ai/profile-llm-client.d.ts +2 -0
- package/dist/services/ai/profile-llm-client.js +27 -0
- package/dist/services/ai/provider-config.d.ts +17 -0
- package/dist/services/ai/provider-config.js +30 -0
- package/dist/services/ai/providers/anthropic-messages.d.ts +34 -0
- package/dist/services/ai/providers/anthropic-messages.js +220 -0
- package/dist/services/ai/providers/base-provider.d.ts +24 -0
- package/dist/services/ai/providers/base-provider.js +24 -0
- package/dist/services/ai/providers/google-gemini.d.ts +15 -0
- package/dist/services/ai/providers/google-gemini.js +228 -0
- package/dist/services/ai/providers/minimax.d.ts +35 -0
- package/dist/services/ai/providers/minimax.js +55 -0
- package/dist/services/ai/providers/openai-chat-completion.d.ts +22 -0
- package/dist/services/ai/providers/openai-chat-completion.js +387 -0
- package/dist/services/ai/providers/openai-responses.d.ts +13 -0
- package/dist/services/ai/providers/openai-responses.js +182 -0
- package/dist/services/ai/providers/orcarouter.d.ts +51 -0
- package/dist/services/ai/providers/orcarouter.js +66 -0
- package/dist/services/ai/session/ai-session-manager.d.ts +24 -0
- package/dist/services/ai/session/ai-session-manager.js +211 -0
- package/dist/services/ai/session/session-types.d.ts +42 -0
- package/dist/services/ai/session/session-types.js +1 -0
- package/dist/services/ai/tools/tool-schema.d.ts +40 -0
- package/dist/services/ai/tools/tool-schema.js +24 -0
- package/dist/services/ai/validators/user-profile-validator.d.ts +12 -0
- package/dist/services/ai/validators/user-profile-validator.js +114 -0
- package/dist/services/api-handlers.d.ts +186 -0
- package/dist/services/api-handlers.js +1284 -0
- package/dist/services/auth-token.d.ts +14 -0
- package/dist/services/auth-token.js +59 -0
- package/dist/services/auto-capture.d.ts +3 -0
- package/dist/services/auto-capture.js +127 -0
- package/dist/services/cleanup-service.d.ts +22 -0
- package/dist/services/cleanup-service.js +109 -0
- package/dist/services/client.d.ts +134 -0
- package/dist/services/client.js +327 -0
- package/dist/services/context.d.ts +10 -0
- package/dist/services/context.js +27 -0
- package/dist/services/cors.d.ts +12 -0
- package/dist/services/cors.js +52 -0
- package/dist/services/deduplication-service.d.ts +28 -0
- package/dist/services/deduplication-service.js +111 -0
- package/dist/services/embedding.d.ts +36 -0
- package/dist/services/embedding.js +200 -0
- package/dist/services/jsonc.d.ts +6 -0
- package/dist/services/jsonc.js +76 -0
- package/dist/services/language-detector.d.ts +2 -0
- package/dist/services/language-detector.js +33 -0
- package/dist/services/legacy-migration.d.ts +77 -0
- package/dist/services/legacy-migration.js +343 -0
- package/dist/services/logger.d.ts +1 -0
- package/dist/services/logger.js +119 -0
- package/dist/services/memory-portability-service.d.ts +37 -0
- package/dist/services/memory-portability-service.js +340 -0
- package/dist/services/memory-scope.d.ts +16 -0
- package/dist/services/memory-scope.js +43 -0
- package/dist/services/migration-service.d.ts +42 -0
- package/dist/services/migration-service.js +307 -0
- package/dist/services/onnxruntime-resolve.d.ts +24 -0
- package/dist/services/onnxruntime-resolve.js +174 -0
- package/dist/services/privacy.d.ts +26 -0
- package/dist/services/privacy.js +69 -0
- package/dist/services/request-timeouts.d.ts +9 -0
- package/dist/services/request-timeouts.js +11 -0
- package/dist/services/runtime-require.d.ts +17 -0
- package/dist/services/runtime-require.js +75 -0
- package/dist/services/secret-resolver.d.ts +1 -0
- package/dist/services/secret-resolver.js +65 -0
- package/dist/services/shard-inventory-service.d.ts +69 -0
- package/dist/services/shard-inventory-service.js +315 -0
- package/dist/services/shard-path-migration-service.d.ts +31 -0
- package/dist/services/shard-path-migration-service.js +367 -0
- package/dist/services/tag-prefix-migration.d.ts +60 -0
- package/dist/services/tag-prefix-migration.js +439 -0
- package/dist/services/tag-prefix-scan.d.ts +26 -0
- package/dist/services/tag-prefix-scan.js +91 -0
- package/dist/services/tags.d.ts +29 -0
- package/dist/services/tags.js +251 -0
- package/dist/services/turso/connection-manager.d.ts +15 -0
- package/dist/services/turso/connection-manager.js +163 -0
- package/dist/services/turso/cross-process-write-lock.d.ts +1 -0
- package/dist/services/turso/cross-process-write-lock.js +89 -0
- package/dist/services/turso/legacy-migrator.d.ts +1 -0
- package/dist/services/turso/legacy-migrator.js +540 -0
- package/dist/services/turso/lifecycle.d.ts +1 -0
- package/dist/services/turso/lifecycle.js +14 -0
- package/dist/services/turso/operation-lock.d.ts +2 -0
- package/dist/services/turso/operation-lock.js +61 -0
- package/dist/services/turso/ready.d.ts +2 -0
- package/dist/services/turso/ready.js +36 -0
- package/dist/services/turso/shard-manager.d.ts +38 -0
- package/dist/services/turso/shard-manager.js +496 -0
- package/dist/services/turso/sqlite-handle-release.d.ts +16 -0
- package/dist/services/turso/sqlite-handle-release.js +56 -0
- package/dist/services/turso/turso-db.d.ts +18 -0
- package/dist/services/turso/turso-db.js +53 -0
- package/dist/services/turso/types.d.ts +42 -0
- package/dist/services/turso/types.js +1 -0
- package/dist/services/turso/vector-search.d.ts +41 -0
- package/dist/services/turso/vector-search.js +384 -0
- package/dist/services/turso/vector-utils.d.ts +7 -0
- package/dist/services/turso/vector-utils.js +67 -0
- package/dist/services/user-memory-learning.d.ts +97 -0
- package/dist/services/user-memory-learning.js +673 -0
- package/dist/services/user-profile/ai-cleanup.d.ts +58 -0
- package/dist/services/user-profile/ai-cleanup.js +486 -0
- package/dist/services/user-profile/profile-context.d.ts +1 -0
- package/dist/services/user-profile/profile-context.js +105 -0
- package/dist/services/user-profile/profile-utils.d.ts +2 -0
- package/dist/services/user-profile/profile-utils.js +45 -0
- package/dist/services/user-profile/types.d.ts +92 -0
- package/dist/services/user-profile/types.js +1 -0
- package/dist/services/user-profile/user-profile-manager.d.ts +65 -0
- package/dist/services/user-profile/user-profile-manager.js +1660 -0
- package/dist/services/user-prompt/user-prompt-manager.d.ts +53 -0
- package/dist/services/user-prompt/user-prompt-manager.js +303 -0
- package/dist/services/web-api-auth.d.ts +3 -0
- package/dist/services/web-api-auth.js +25 -0
- package/dist/services/web-auth.d.ts +46 -0
- package/dist/services/web-auth.js +105 -0
- package/dist/services/web-server.d.ts +57 -0
- package/dist/services/web-server.js +659 -0
- package/dist/shared/api/index.d.ts +4 -0
- package/dist/shared/api/index.js +2 -0
- package/dist/shared/api/portability-schemas.d.ts +56 -0
- package/dist/shared/api/portability-schemas.js +39 -0
- package/dist/shared/api/schemas.d.ts +295 -0
- package/dist/shared/api/schemas.js +134 -0
- package/dist/shared/api/types.d.ts +28 -0
- package/dist/shared/api/types.js +1 -0
- package/dist/types/index.d.ts +27 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/context-limit.d.ts +10 -0
- package/dist/utils/context-limit.js +43 -0
- package/dist/utils/math.d.ts +7 -0
- package/dist/utils/math.js +48 -0
- package/dist/utils/profile.d.ts +2 -0
- package/dist/utils/profile.js +15 -0
- package/dist/v2/adapter.d.ts +5 -0
- package/dist/v2/adapter.js +218 -0
- package/dist/v2/legacy-client.d.ts +15 -0
- package/dist/v2/legacy-client.js +257 -0
- package/dist/v2/memory-bridge.d.ts +18 -0
- package/dist/v2/memory-bridge.js +29 -0
- package/dist/v2/plugin.d.ts +3 -0
- package/dist/v2/plugin.js +24 -0
- package/dist/web/assets/index-DPjEpD1h.css +2 -0
- package/dist/web/assets/index-do26c4JJ.js +122 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/index.html +14 -0
- package/dist/web/omms-icon.png +0 -0
- package/package.json +138 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { CONFIG } from "../config.js";
|
|
4
|
+
import { normalize, resolve, isAbsolute, basename, dirname, join, delimiter, relative, } from "node:path";
|
|
5
|
+
import { accessSync, constants, realpathSync, existsSync } from "node:fs";
|
|
6
|
+
function sha256(input) {
|
|
7
|
+
return createHash("sha256").update(input).digest("hex").slice(0, 16);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Marker files whose presence pins a directory as the omms project root.
|
|
11
|
+
*
|
|
12
|
+
* A multi-repo workspace (e.g. a tree managed by Google `repo`, a monorepo,
|
|
13
|
+
* or any layout where several nested git repositories should share one memory
|
|
14
|
+
* store) drops this file at the workspace root. Every session started anywhere
|
|
15
|
+
* underneath then resolves onto that root instead of onto whichever physical
|
|
16
|
+
* git repository the working directory happens to live in.
|
|
17
|
+
*
|
|
18
|
+
* Unlike an environment variable or a config-file value, the marker is found
|
|
19
|
+
* by walking up from the working directory that every code path already passes
|
|
20
|
+
* in (the plugin's `ctx.directory`, the web API's `process.cwd()`), so project
|
|
21
|
+
* identity never depends on which long-lived opencode process happens to own
|
|
22
|
+
* the shared web server.
|
|
23
|
+
*/
|
|
24
|
+
const PROJECT_MARKERS = [
|
|
25
|
+
".omms-project",
|
|
26
|
+
// Legacy marker from the opencode-mem days; still honoured.
|
|
27
|
+
".opencode-mem-project",
|
|
28
|
+
];
|
|
29
|
+
function hasProjectMarker(dir) {
|
|
30
|
+
return PROJECT_MARKERS.some((marker) => existsSync(join(dir, marker)));
|
|
31
|
+
}
|
|
32
|
+
function canonicalPath(path) {
|
|
33
|
+
try {
|
|
34
|
+
return process.platform === "win32"
|
|
35
|
+
? normalize(realpathSync.native(path))
|
|
36
|
+
: normalize(realpathSync(path));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return normalize(resolve(path));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function isPathInside(root, candidate) {
|
|
43
|
+
const rel = relative(root, candidate);
|
|
44
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
45
|
+
}
|
|
46
|
+
function findUntrustedProjectRoot(directory) {
|
|
47
|
+
let current = canonicalPath(directory);
|
|
48
|
+
while (true) {
|
|
49
|
+
if (existsSync(join(current, ".git")) || hasProjectMarker(current)) {
|
|
50
|
+
return current;
|
|
51
|
+
}
|
|
52
|
+
const parent = dirname(current);
|
|
53
|
+
if (parent === current)
|
|
54
|
+
return canonicalPath(directory);
|
|
55
|
+
current = parent;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function resolveTrustedWindowsShell(untrustedRoot) {
|
|
59
|
+
const candidates = [
|
|
60
|
+
process.env.ComSpec,
|
|
61
|
+
process.env.SystemRoot ? join(process.env.SystemRoot, "System32", "cmd.exe") : undefined,
|
|
62
|
+
];
|
|
63
|
+
for (const path of candidates) {
|
|
64
|
+
if (!path || !isAbsolute(path))
|
|
65
|
+
continue;
|
|
66
|
+
try {
|
|
67
|
+
accessSync(path, constants.X_OK);
|
|
68
|
+
const candidate = canonicalPath(path);
|
|
69
|
+
if (!isPathInside(untrustedRoot, candidate))
|
|
70
|
+
return candidate;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// ignore unreadable or untrusted PATH entries
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
function resolveTrustedGitCommand(directory) {
|
|
79
|
+
const untrustedRoot = findUntrustedProjectRoot(directory);
|
|
80
|
+
const executableNames = process.platform === "win32" ? ["git.exe", "git.cmd", "git.bat"] : ["git"];
|
|
81
|
+
for (const rawEntry of (process.env.PATH ?? "").split(delimiter)) {
|
|
82
|
+
const entry = rawEntry.trim().replace(/^"(.*)"$/, "$1");
|
|
83
|
+
if (!entry || !isAbsolute(entry))
|
|
84
|
+
continue;
|
|
85
|
+
for (const executableName of executableNames) {
|
|
86
|
+
const candidatePath = join(entry, executableName);
|
|
87
|
+
try {
|
|
88
|
+
accessSync(candidatePath, constants.X_OK);
|
|
89
|
+
const executable = canonicalPath(candidatePath);
|
|
90
|
+
if (isPathInside(untrustedRoot, executable))
|
|
91
|
+
continue;
|
|
92
|
+
if (executableName === "git.exe" || process.platform !== "win32") {
|
|
93
|
+
return { executable, shell: false };
|
|
94
|
+
}
|
|
95
|
+
const shell = resolveTrustedWindowsShell(untrustedRoot);
|
|
96
|
+
if (shell)
|
|
97
|
+
return { executable, shell };
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// ignore unreadable or untrusted PATH entries
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
function runGit(args, directory = process.cwd()) {
|
|
107
|
+
const gitCommand = resolveTrustedGitCommand(directory);
|
|
108
|
+
if (!gitCommand)
|
|
109
|
+
return null;
|
|
110
|
+
try {
|
|
111
|
+
const output = execFileSync(gitCommand.executable, args, {
|
|
112
|
+
encoding: "utf-8",
|
|
113
|
+
cwd: directory,
|
|
114
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
115
|
+
shell: gitCommand.shell,
|
|
116
|
+
}).trim();
|
|
117
|
+
return output || null;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Walk up from `directory` (inclusive) to the filesystem root looking for the
|
|
125
|
+
* {@link PROJECT_MARKERS}. Returns the first directory that contains it, or
|
|
126
|
+
* `null` when no marker is found so the caller can fall back to git detection.
|
|
127
|
+
*/
|
|
128
|
+
export function findMarkerProjectRoot(directory) {
|
|
129
|
+
let dir = resolve(directory);
|
|
130
|
+
while (true) {
|
|
131
|
+
if (hasProjectMarker(dir)) {
|
|
132
|
+
return dir;
|
|
133
|
+
}
|
|
134
|
+
const parent = dirname(dir);
|
|
135
|
+
if (parent === dir) {
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
dir = parent;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
export function getGitEmail(directory = process.cwd()) {
|
|
143
|
+
return runGit(["config", "user.email"], directory);
|
|
144
|
+
}
|
|
145
|
+
export function getGitName(directory = process.cwd()) {
|
|
146
|
+
return runGit(["config", "user.name"], directory);
|
|
147
|
+
}
|
|
148
|
+
export function getGitRepoUrl(directory) {
|
|
149
|
+
return runGit(["config", "--get", "remote.origin.url"], directory);
|
|
150
|
+
}
|
|
151
|
+
export function getGitCommonDir(directory) {
|
|
152
|
+
try {
|
|
153
|
+
const commonDir = runGit(["rev-parse", "--git-common-dir"], directory);
|
|
154
|
+
if (!commonDir)
|
|
155
|
+
return null;
|
|
156
|
+
const resolved = isAbsolute(commonDir)
|
|
157
|
+
? normalize(commonDir)
|
|
158
|
+
: normalize(resolve(directory, commonDir));
|
|
159
|
+
if (existsSync(resolved)) {
|
|
160
|
+
const canonical = process.platform === "win32" ? realpathSync.native(resolved) : realpathSync(resolved);
|
|
161
|
+
return normalize(canonical);
|
|
162
|
+
}
|
|
163
|
+
return resolved;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
export function getGitTopLevel(directory) {
|
|
170
|
+
return runGit(["rev-parse", "--show-toplevel"], directory);
|
|
171
|
+
}
|
|
172
|
+
// Git-only fallbacks, kept separate so the marker-aware entry points below
|
|
173
|
+
// can short-circuit on a marker and reuse these without re-running detection.
|
|
174
|
+
function getGitProjectRoot(directory) {
|
|
175
|
+
const commonDir = getGitCommonDir(directory);
|
|
176
|
+
if (commonDir && basename(commonDir) === ".git") {
|
|
177
|
+
return dirname(commonDir);
|
|
178
|
+
}
|
|
179
|
+
const topLevel = getGitTopLevel(directory);
|
|
180
|
+
if (topLevel) {
|
|
181
|
+
return topLevel;
|
|
182
|
+
}
|
|
183
|
+
return directory;
|
|
184
|
+
}
|
|
185
|
+
function getGitProjectIdentity(directory) {
|
|
186
|
+
const commonDir = getGitCommonDir(directory);
|
|
187
|
+
if (commonDir) {
|
|
188
|
+
return `git-common:${commonDir}`;
|
|
189
|
+
}
|
|
190
|
+
const gitRepoUrl = getGitRepoUrl(directory);
|
|
191
|
+
if (gitRepoUrl) {
|
|
192
|
+
return `remote:${gitRepoUrl}`;
|
|
193
|
+
}
|
|
194
|
+
return `path:${normalize(directory)}`;
|
|
195
|
+
}
|
|
196
|
+
export function getProjectRoot(directory) {
|
|
197
|
+
return findMarkerProjectRoot(directory) ?? getGitProjectRoot(directory);
|
|
198
|
+
}
|
|
199
|
+
export function getProjectIdentity(directory) {
|
|
200
|
+
const markerRoot = findMarkerProjectRoot(directory);
|
|
201
|
+
return markerRoot ? `path:${markerRoot}` : getGitProjectIdentity(directory);
|
|
202
|
+
}
|
|
203
|
+
export function getProjectName(directory) {
|
|
204
|
+
const normalized = normalize(directory).replace(/\\/g, "/");
|
|
205
|
+
const parts = normalized.split("/").filter((p) => p && p !== ".");
|
|
206
|
+
return parts[parts.length - 1] || directory;
|
|
207
|
+
}
|
|
208
|
+
export function getUserTagInfo(directory = process.cwd()) {
|
|
209
|
+
const email = CONFIG.userEmailOverride || getGitEmail(directory);
|
|
210
|
+
const name = CONFIG.userNameOverride || getGitName(directory);
|
|
211
|
+
if (email) {
|
|
212
|
+
return {
|
|
213
|
+
tag: `${CONFIG.containerTagPrefix}_user_${sha256(email)}`,
|
|
214
|
+
displayName: name || email,
|
|
215
|
+
userName: name || undefined,
|
|
216
|
+
userEmail: email,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
const fallback = name || process.env.USER || process.env.USERNAME || "anonymous";
|
|
220
|
+
return {
|
|
221
|
+
tag: `${CONFIG.containerTagPrefix}_user_${sha256(fallback)}`,
|
|
222
|
+
displayName: fallback,
|
|
223
|
+
userName: fallback,
|
|
224
|
+
userEmail: undefined,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
export function getProjectTagInfo(directory) {
|
|
228
|
+
// Resolve the marker exactly once and derive root + identity from it, so a
|
|
229
|
+
// single getProjectTagInfo call never walks the ancestry more than once.
|
|
230
|
+
const markerRoot = findMarkerProjectRoot(directory);
|
|
231
|
+
const projectRoot = markerRoot ?? getGitProjectRoot(directory);
|
|
232
|
+
const projectName = getProjectName(projectRoot);
|
|
233
|
+
// When a marker pins the project root, any git remote belongs to a single
|
|
234
|
+
// nested sub-repo and would be misleading for the grouped workspace, so
|
|
235
|
+
// leave it unset.
|
|
236
|
+
const gitRepoUrl = markerRoot ? null : getGitRepoUrl(directory);
|
|
237
|
+
const projectIdentity = markerRoot ? `path:${markerRoot}` : getGitProjectIdentity(projectRoot);
|
|
238
|
+
return {
|
|
239
|
+
tag: `${CONFIG.containerTagPrefix}_project_${sha256(projectIdentity)}`,
|
|
240
|
+
displayName: projectRoot,
|
|
241
|
+
projectPath: projectRoot,
|
|
242
|
+
projectName,
|
|
243
|
+
gitRepoUrl: gitRepoUrl || undefined,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
export function getTags(directory) {
|
|
247
|
+
return {
|
|
248
|
+
user: getUserTagInfo(directory),
|
|
249
|
+
project: getProjectTagInfo(directory),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createClient } from "@libsql/client";
|
|
2
|
+
import { TursoDb } from "./turso-db.js";
|
|
3
|
+
export declare class TursoConnectionManager {
|
|
4
|
+
private readonly clientFactory;
|
|
5
|
+
private readonly connections;
|
|
6
|
+
private readonly pending;
|
|
7
|
+
private readonly closingConnections;
|
|
8
|
+
private closingPromise;
|
|
9
|
+
constructor(clientFactory?: typeof createClient);
|
|
10
|
+
getConnection(dbPath: string): Promise<TursoDb>;
|
|
11
|
+
closeConnection(dbPath: string): Promise<void>;
|
|
12
|
+
closeAll(): Promise<void>;
|
|
13
|
+
closeAllSync(): void;
|
|
14
|
+
}
|
|
15
|
+
export declare const tursoConnectionManager: TursoConnectionManager;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { createClient } from "@libsql/client";
|
|
2
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve, relative, isAbsolute, sep } from "node:path";
|
|
4
|
+
import { CONFIG } from "../../config.js";
|
|
5
|
+
import { log } from "../logger.js";
|
|
6
|
+
import { collectReleasedSqliteHandles } from "./sqlite-handle-release.js";
|
|
7
|
+
import { TursoDb } from "./turso-db.js";
|
|
8
|
+
function toFileUrl(dbPath) {
|
|
9
|
+
return dbPath.startsWith("file:") ? dbPath : `file:${dbPath}`;
|
|
10
|
+
}
|
|
11
|
+
function assertPathInsideStorage(dbPath) {
|
|
12
|
+
const storageRoot = resolve(CONFIG.storagePath);
|
|
13
|
+
const resolvedPath = resolve(dbPath);
|
|
14
|
+
const relativePath = relative(storageRoot, resolvedPath);
|
|
15
|
+
// Only treat path-segment traversal as escape (not filenames containing "..").
|
|
16
|
+
if (relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
|
|
17
|
+
throw new Error(`Refusing to open database outside storagePath: ${dbPath}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export class TursoConnectionManager {
|
|
21
|
+
clientFactory;
|
|
22
|
+
connections = new Map();
|
|
23
|
+
pending = new Map();
|
|
24
|
+
closingConnections = new Map();
|
|
25
|
+
closingPromise = null;
|
|
26
|
+
constructor(clientFactory = createClient) {
|
|
27
|
+
this.clientFactory = clientFactory;
|
|
28
|
+
}
|
|
29
|
+
async getConnection(dbPath) {
|
|
30
|
+
if (this.closingPromise) {
|
|
31
|
+
await this.closingPromise;
|
|
32
|
+
}
|
|
33
|
+
const closingConnection = this.closingConnections.get(dbPath);
|
|
34
|
+
if (closingConnection) {
|
|
35
|
+
await closingConnection;
|
|
36
|
+
}
|
|
37
|
+
assertPathInsideStorage(dbPath);
|
|
38
|
+
const existing = this.connections.get(dbPath);
|
|
39
|
+
if (existing) {
|
|
40
|
+
return existing;
|
|
41
|
+
}
|
|
42
|
+
const inFlight = this.pending.get(dbPath);
|
|
43
|
+
if (inFlight) {
|
|
44
|
+
return inFlight;
|
|
45
|
+
}
|
|
46
|
+
const openPromise = (async () => {
|
|
47
|
+
const dir = dirname(dbPath);
|
|
48
|
+
if (!existsSync(dir)) {
|
|
49
|
+
mkdirSync(dir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
// One pooled handle per client: the pragmas below are per-connection,
|
|
52
|
+
// so a larger pool would mint unconfigured handles that fail with
|
|
53
|
+
// SQLITE_BUSY under cross-process contention. Writes are serialised
|
|
54
|
+
// per scope anyway (withScopeWriteLock); WAL keeps cross-process
|
|
55
|
+
// readers concurrent.
|
|
56
|
+
const client = this.clientFactory({ url: toFileUrl(dbPath), concurrency: 1 });
|
|
57
|
+
try {
|
|
58
|
+
const db = new TursoDb(client);
|
|
59
|
+
// Cross-process safety: busy_timeout makes this connection queue
|
|
60
|
+
// instead of failing with SQLITE_BUSY when another process holds the
|
|
61
|
+
// write lock, and one pooled handle per client guarantees the pragma
|
|
62
|
+
// covers every handle the client will ever use. Writers are fully
|
|
63
|
+
// serialised across processes by the per-scope cross-process write
|
|
64
|
+
// lock (see cross-process-write-lock.ts); the default rollback
|
|
65
|
+
// journal is kept because file-level migration copies rely on the
|
|
66
|
+
// main database file being current after every commit.
|
|
67
|
+
await db.execute("PRAGMA busy_timeout = 5000");
|
|
68
|
+
await db.execute("PRAGMA foreign_keys = ON");
|
|
69
|
+
this.connections.set(dbPath, db);
|
|
70
|
+
return db;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
try {
|
|
74
|
+
client.close();
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// ignore close errors during cleanup
|
|
78
|
+
}
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
})();
|
|
82
|
+
this.pending.set(dbPath, openPromise);
|
|
83
|
+
try {
|
|
84
|
+
return await openPromise;
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
this.connections.delete(dbPath);
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
this.pending.delete(dbPath);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async closeConnection(dbPath) {
|
|
95
|
+
if (this.closingPromise) {
|
|
96
|
+
await this.closingPromise;
|
|
97
|
+
}
|
|
98
|
+
const existingClose = this.closingConnections.get(dbPath);
|
|
99
|
+
if (existingClose) {
|
|
100
|
+
return existingClose;
|
|
101
|
+
}
|
|
102
|
+
const closePromise = Promise.resolve()
|
|
103
|
+
.then(async () => {
|
|
104
|
+
const pending = this.pending.get(dbPath);
|
|
105
|
+
if (pending) {
|
|
106
|
+
await Promise.allSettled([pending]);
|
|
107
|
+
}
|
|
108
|
+
const db = this.connections.get(dbPath);
|
|
109
|
+
if (db) {
|
|
110
|
+
try {
|
|
111
|
+
await db.close();
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
log("Error closing Turso database", { path: dbPath, error: String(error) });
|
|
115
|
+
}
|
|
116
|
+
this.connections.delete(dbPath);
|
|
117
|
+
}
|
|
118
|
+
await collectReleasedSqliteHandles();
|
|
119
|
+
})
|
|
120
|
+
.finally(() => {
|
|
121
|
+
this.closingConnections.delete(dbPath);
|
|
122
|
+
});
|
|
123
|
+
this.closingConnections.set(dbPath, closePromise);
|
|
124
|
+
return closePromise;
|
|
125
|
+
}
|
|
126
|
+
async closeAll() {
|
|
127
|
+
if (this.closingPromise)
|
|
128
|
+
return this.closingPromise;
|
|
129
|
+
this.closingPromise = (async () => {
|
|
130
|
+
await Promise.allSettled([...this.pending.values(), ...this.closingConnections.values()]);
|
|
131
|
+
for (const [path, db] of this.connections) {
|
|
132
|
+
try {
|
|
133
|
+
await db.close();
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
log("Error closing Turso database", { path, error: String(error) });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
this.connections.clear();
|
|
140
|
+
this.pending.clear();
|
|
141
|
+
await collectReleasedSqliteHandles();
|
|
142
|
+
})();
|
|
143
|
+
try {
|
|
144
|
+
await this.closingPromise;
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
this.closingPromise = null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
closeAllSync() {
|
|
151
|
+
for (const [path, db] of this.connections) {
|
|
152
|
+
try {
|
|
153
|
+
db.getClient().close();
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
log("Error closing Turso database (sync)", { path, error: String(error) });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
this.connections.clear();
|
|
160
|
+
this.pending.clear();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
export const tursoConnectionManager = new TursoConnectionManager();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function withCrossProcessWriteLock<T>(scope: "user" | "project", hash: string, fn: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { CONFIG } from "../../config.js";
|
|
4
|
+
/**
|
|
5
|
+
* Cross-process advisory write lock, one per (scope, scopeHash).
|
|
6
|
+
*
|
|
7
|
+
* The in-process write queue in shard-manager only serialises writers inside
|
|
8
|
+
* one process. When two hosts (OpenCode and Pi, or two test workers) write the
|
|
9
|
+
* same project shard set concurrently, their read-modify-write sequences can
|
|
10
|
+
* interleave: COUNT()-based vector-count syncs straddle another process's
|
|
11
|
+
* insert-then-increment and double-count, and rollover decisions race. This
|
|
12
|
+
* lock serialises the whole write critical section across processes while
|
|
13
|
+
* leaving readers lock-free (WAL mode keeps them concurrent).
|
|
14
|
+
*
|
|
15
|
+
* Semantics match turso/operation-lock: PID-liveness staleness detection, so a
|
|
16
|
+
* crashed holder is cleaned up by the next acquirer. Acquisition blocks with
|
|
17
|
+
* polling and a timeout that fails loudly instead of hanging.
|
|
18
|
+
*/
|
|
19
|
+
const WRITE_LOCK_DIR = ".write-locks";
|
|
20
|
+
const LOCK_TIMEOUT_MS = 15000;
|
|
21
|
+
const LOCK_POLL_MS = 15;
|
|
22
|
+
function isProcessAlive(pid) {
|
|
23
|
+
try {
|
|
24
|
+
process.kill(pid, 0);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function readLiveLock(path) {
|
|
32
|
+
if (!existsSync(path))
|
|
33
|
+
return null;
|
|
34
|
+
try {
|
|
35
|
+
const state = JSON.parse(readFileSync(path, "utf8"));
|
|
36
|
+
if (Number.isInteger(state.pid) && state.pid > 0 && isProcessAlive(state.pid)) {
|
|
37
|
+
return state;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Corrupt lock files are stale; remove below.
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
unlinkSync(path);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Already removed by a racing acquirer — retry the acquisition.
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function lockFilePath(scope, hash) {
|
|
52
|
+
return join(CONFIG.storagePath, WRITE_LOCK_DIR, `${scope}_${hash}.lock`);
|
|
53
|
+
}
|
|
54
|
+
export async function withCrossProcessWriteLock(scope, hash, fn) {
|
|
55
|
+
const dir = join(CONFIG.storagePath, WRITE_LOCK_DIR);
|
|
56
|
+
if (!existsSync(dir)) {
|
|
57
|
+
mkdirSync(dir, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
const path = lockFilePath(scope, hash);
|
|
60
|
+
const state = { pid: process.pid, timestamp: new Date().toISOString() };
|
|
61
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
62
|
+
for (;;) {
|
|
63
|
+
try {
|
|
64
|
+
writeFileSync(path, JSON.stringify(state), { flag: "wx" });
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
const holder = readLiveLock(path);
|
|
69
|
+
if (!holder)
|
|
70
|
+
continue;
|
|
71
|
+
if (Date.now() > deadline) {
|
|
72
|
+
throw new Error(`Timed out acquiring the cross-process write lock for ${scope}/${hash}: ` +
|
|
73
|
+
`held by pid ${holder.pid} since ${holder.timestamp}`);
|
|
74
|
+
}
|
|
75
|
+
await new Promise((resolve) => setTimeout(resolve, LOCK_POLL_MS + Math.random() * 10));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
return await fn();
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
try {
|
|
83
|
+
unlinkSync(path);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Best effort: a stale file is cleaned up by the next acquirer.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runLegacyTursoMigration(): Promise<void>;
|