opencode-mem 1.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/README.md +588 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +258 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +618 -0
- package/dist/plugin.d.ts +5 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +15 -0
- package/dist/services/api-handlers.d.ts +102 -0
- package/dist/services/api-handlers.d.ts.map +1 -0
- package/dist/services/api-handlers.js +494 -0
- package/dist/services/auto-capture.d.ts +32 -0
- package/dist/services/auto-capture.d.ts.map +1 -0
- package/dist/services/auto-capture.js +451 -0
- package/dist/services/cleanup-service.d.ts +20 -0
- package/dist/services/cleanup-service.d.ts.map +1 -0
- package/dist/services/cleanup-service.js +88 -0
- package/dist/services/client.d.ts +104 -0
- package/dist/services/client.d.ts.map +1 -0
- package/dist/services/client.js +251 -0
- package/dist/services/compaction.d.ts +92 -0
- package/dist/services/compaction.d.ts.map +1 -0
- package/dist/services/compaction.js +421 -0
- package/dist/services/context.d.ts +17 -0
- package/dist/services/context.d.ts.map +1 -0
- package/dist/services/context.js +41 -0
- package/dist/services/deduplication-service.d.ts +30 -0
- package/dist/services/deduplication-service.d.ts.map +1 -0
- package/dist/services/deduplication-service.js +131 -0
- package/dist/services/embedding.d.ts +10 -0
- package/dist/services/embedding.d.ts.map +1 -0
- package/dist/services/embedding.js +77 -0
- package/dist/services/jsonc.d.ts +7 -0
- package/dist/services/jsonc.d.ts.map +1 -0
- package/dist/services/jsonc.js +76 -0
- package/dist/services/logger.d.ts +2 -0
- package/dist/services/logger.d.ts.map +1 -0
- package/dist/services/logger.js +16 -0
- package/dist/services/migration-service.d.ts +42 -0
- package/dist/services/migration-service.d.ts.map +1 -0
- package/dist/services/migration-service.js +258 -0
- package/dist/services/privacy.d.ts +4 -0
- package/dist/services/privacy.d.ts.map +1 -0
- package/dist/services/privacy.js +10 -0
- package/dist/services/sqlite/connection-manager.d.ts +10 -0
- package/dist/services/sqlite/connection-manager.d.ts.map +1 -0
- package/dist/services/sqlite/connection-manager.js +45 -0
- package/dist/services/sqlite/shard-manager.d.ts +20 -0
- package/dist/services/sqlite/shard-manager.d.ts.map +1 -0
- package/dist/services/sqlite/shard-manager.js +221 -0
- package/dist/services/sqlite/types.d.ts +39 -0
- package/dist/services/sqlite/types.d.ts.map +1 -0
- package/dist/services/sqlite/types.js +1 -0
- package/dist/services/sqlite/vector-search.d.ts +18 -0
- package/dist/services/sqlite/vector-search.d.ts.map +1 -0
- package/dist/services/sqlite/vector-search.js +129 -0
- package/dist/services/sqlite-client.d.ts +116 -0
- package/dist/services/sqlite-client.d.ts.map +1 -0
- package/dist/services/sqlite-client.js +284 -0
- package/dist/services/tags.d.ts +20 -0
- package/dist/services/tags.d.ts.map +1 -0
- package/dist/services/tags.js +76 -0
- package/dist/services/web-server-lock.d.ts +12 -0
- package/dist/services/web-server-lock.d.ts.map +1 -0
- package/dist/services/web-server-lock.js +157 -0
- package/dist/services/web-server-worker.d.ts +2 -0
- package/dist/services/web-server-worker.d.ts.map +1 -0
- package/dist/services/web-server-worker.js +221 -0
- package/dist/services/web-server.d.ts +22 -0
- package/dist/services/web-server.d.ts.map +1 -0
- package/dist/services/web-server.js +134 -0
- package/dist/types/index.d.ts +48 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +1 -0
- package/dist/web/app.d.ts +2 -0
- package/dist/web/app.d.ts.map +1 -0
- package/dist/web/app.js +691 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/favicon.svg +14 -0
- package/dist/web/index.html +202 -0
- package/dist/web/styles.css +851 -0
- package/package.json +52 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import * as sqliteVec from "sqlite-vec";
|
|
3
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { CONFIG } from "../config.js";
|
|
6
|
+
import { log } from "./logger.js";
|
|
7
|
+
export class SQLiteMemoryClient {
|
|
8
|
+
db = null;
|
|
9
|
+
embedder;
|
|
10
|
+
isInitialized = false;
|
|
11
|
+
constructor(embedder) {
|
|
12
|
+
this.embedder = embedder;
|
|
13
|
+
}
|
|
14
|
+
initializeDatabase() {
|
|
15
|
+
if (!existsSync(CONFIG.storagePath)) {
|
|
16
|
+
mkdirSync(CONFIG.storagePath, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
const dbPath = join(CONFIG.storagePath, "memories.db");
|
|
19
|
+
this.db = new Database(dbPath);
|
|
20
|
+
this.db.pragma("journal_mode = WAL");
|
|
21
|
+
this.db.pragma("synchronous = NORMAL");
|
|
22
|
+
this.db.pragma("cache_size = -64000");
|
|
23
|
+
this.db.pragma("temp_store = MEMORY");
|
|
24
|
+
sqliteVec.load(this.db);
|
|
25
|
+
this.db.exec(`
|
|
26
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
27
|
+
id TEXT PRIMARY KEY,
|
|
28
|
+
content TEXT NOT NULL,
|
|
29
|
+
vector BLOB NOT NULL,
|
|
30
|
+
containerTag TEXT NOT NULL,
|
|
31
|
+
type TEXT,
|
|
32
|
+
createdAt INTEGER NOT NULL,
|
|
33
|
+
updatedAt INTEGER NOT NULL,
|
|
34
|
+
metadata TEXT,
|
|
35
|
+
displayName TEXT,
|
|
36
|
+
userName TEXT,
|
|
37
|
+
userEmail TEXT,
|
|
38
|
+
projectPath TEXT,
|
|
39
|
+
projectName TEXT,
|
|
40
|
+
gitRepoUrl TEXT
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_containerTag ON memories(containerTag);
|
|
44
|
+
CREATE INDEX IF NOT EXISTS idx_createdAt ON memories(createdAt DESC);
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_type ON memories(type);
|
|
46
|
+
|
|
47
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_memories USING vec0(
|
|
48
|
+
id TEXT PRIMARY KEY,
|
|
49
|
+
embedding FLOAT[384]
|
|
50
|
+
);
|
|
51
|
+
`);
|
|
52
|
+
log("SQLite database initialized", { path: dbPath });
|
|
53
|
+
}
|
|
54
|
+
async warmup(progressCallback) {
|
|
55
|
+
if (this.isInitialized)
|
|
56
|
+
return;
|
|
57
|
+
await this.embedder.warmup(progressCallback);
|
|
58
|
+
this.initializeDatabase();
|
|
59
|
+
this.isInitialized = true;
|
|
60
|
+
log("SQLite memory client ready");
|
|
61
|
+
}
|
|
62
|
+
async isReady() {
|
|
63
|
+
return this.isInitialized && this.embedder.isWarmedUp;
|
|
64
|
+
}
|
|
65
|
+
getStatus() {
|
|
66
|
+
return {
|
|
67
|
+
dbConnected: this.db !== null,
|
|
68
|
+
modelLoaded: this.embedder.isWarmedUp,
|
|
69
|
+
ready: this.isInitialized && this.embedder.isWarmedUp,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
async addMemory(content, containerTag, metadata) {
|
|
73
|
+
log("addMemory: start", { containerTag, contentLength: content.length });
|
|
74
|
+
try {
|
|
75
|
+
if (!this.db)
|
|
76
|
+
throw new Error("Database not initialized");
|
|
77
|
+
const vector = await this.embedder.embed(content);
|
|
78
|
+
const vectorBuffer = Buffer.from(new Float32Array(vector).buffer);
|
|
79
|
+
const id = `mem_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
const record = {
|
|
82
|
+
id,
|
|
83
|
+
content,
|
|
84
|
+
containerTag,
|
|
85
|
+
type: metadata?.type,
|
|
86
|
+
createdAt: now,
|
|
87
|
+
updatedAt: now,
|
|
88
|
+
metadata: metadata ? JSON.stringify(metadata) : undefined,
|
|
89
|
+
displayName: metadata?.displayName,
|
|
90
|
+
userName: metadata?.userName,
|
|
91
|
+
userEmail: metadata?.userEmail,
|
|
92
|
+
projectPath: metadata?.projectPath,
|
|
93
|
+
projectName: metadata?.projectName,
|
|
94
|
+
gitRepoUrl: metadata?.gitRepoUrl,
|
|
95
|
+
};
|
|
96
|
+
const insertMemory = this.db.prepare(`
|
|
97
|
+
INSERT INTO memories (
|
|
98
|
+
id, content, vector, containerTag, type, createdAt, updatedAt,
|
|
99
|
+
metadata, displayName, userName, userEmail, projectPath, projectName, gitRepoUrl
|
|
100
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
101
|
+
`);
|
|
102
|
+
const insertVec = this.db.prepare(`
|
|
103
|
+
INSERT INTO vec_memories (id, embedding) VALUES (?, ?)
|
|
104
|
+
`);
|
|
105
|
+
this.db.transaction(() => {
|
|
106
|
+
insertMemory.run(record.id, record.content, vectorBuffer, record.containerTag, record.type, record.createdAt, record.updatedAt, record.metadata, record.displayName, record.userName, record.userEmail, record.projectPath, record.projectName, record.gitRepoUrl);
|
|
107
|
+
insertVec.run(record.id, vectorBuffer);
|
|
108
|
+
})();
|
|
109
|
+
log("addMemory: success", { id });
|
|
110
|
+
return { success: true, id };
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
114
|
+
log("addMemory: error", { error: errorMessage });
|
|
115
|
+
return { success: false, error: errorMessage };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async searchMemories(query, containerTag) {
|
|
119
|
+
log("searchMemories: start", { containerTag });
|
|
120
|
+
try {
|
|
121
|
+
if (!this.db)
|
|
122
|
+
throw new Error("Database not initialized");
|
|
123
|
+
const queryVector = await this.embedder.embed(query);
|
|
124
|
+
const queryBuffer = Buffer.from(new Float32Array(queryVector).buffer);
|
|
125
|
+
const stmt = this.db.prepare(`
|
|
126
|
+
SELECT
|
|
127
|
+
m.id, m.content, m.metadata, m.displayName, m.userName, m.userEmail,
|
|
128
|
+
m.projectPath, m.projectName, m.gitRepoUrl,
|
|
129
|
+
vec_distance_L2(v.embedding, ?) as distance
|
|
130
|
+
FROM memories m
|
|
131
|
+
JOIN vec_memories v ON m.id = v.id
|
|
132
|
+
WHERE m.containerTag = ?
|
|
133
|
+
ORDER BY distance ASC
|
|
134
|
+
LIMIT ?
|
|
135
|
+
`);
|
|
136
|
+
const rows = stmt.all(queryBuffer, containerTag, CONFIG.maxMemories * 2);
|
|
137
|
+
const results = rows
|
|
138
|
+
.map((r) => ({
|
|
139
|
+
id: r.id,
|
|
140
|
+
memory: r.content,
|
|
141
|
+
similarity: 1 / (1 + r.distance),
|
|
142
|
+
metadata: r.metadata ? JSON.parse(r.metadata) : undefined,
|
|
143
|
+
displayName: r.displayName,
|
|
144
|
+
userName: r.userName,
|
|
145
|
+
userEmail: r.userEmail,
|
|
146
|
+
projectPath: r.projectPath,
|
|
147
|
+
projectName: r.projectName,
|
|
148
|
+
gitRepoUrl: r.gitRepoUrl,
|
|
149
|
+
}))
|
|
150
|
+
.filter((r) => r.similarity >= CONFIG.similarityThreshold);
|
|
151
|
+
log("searchMemories: success", { count: results.length });
|
|
152
|
+
return {
|
|
153
|
+
success: true,
|
|
154
|
+
results,
|
|
155
|
+
total: results.length,
|
|
156
|
+
timing: 0,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
161
|
+
log("searchMemories: error", { error: errorMessage });
|
|
162
|
+
return {
|
|
163
|
+
success: false,
|
|
164
|
+
error: errorMessage,
|
|
165
|
+
results: [],
|
|
166
|
+
total: 0,
|
|
167
|
+
timing: 0,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async getProfile(containerTag, query) {
|
|
172
|
+
log("getProfile: start", { containerTag });
|
|
173
|
+
try {
|
|
174
|
+
if (!this.db)
|
|
175
|
+
throw new Error("Database not initialized");
|
|
176
|
+
const stmt = this.db.prepare(`
|
|
177
|
+
SELECT content, type
|
|
178
|
+
FROM memories
|
|
179
|
+
WHERE containerTag = ?
|
|
180
|
+
ORDER BY createdAt DESC
|
|
181
|
+
LIMIT ?
|
|
182
|
+
`);
|
|
183
|
+
const rows = stmt.all(containerTag, CONFIG.maxProfileItems * 2);
|
|
184
|
+
const staticFacts = [];
|
|
185
|
+
const dynamicFacts = [];
|
|
186
|
+
for (const r of rows) {
|
|
187
|
+
if (r.type === "preference") {
|
|
188
|
+
staticFacts.push(r.content);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
dynamicFacts.push(r.content);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const profile = {
|
|
195
|
+
static: staticFacts.slice(0, CONFIG.maxProfileItems),
|
|
196
|
+
dynamic: dynamicFacts.slice(0, CONFIG.maxProfileItems),
|
|
197
|
+
};
|
|
198
|
+
log("getProfile: success");
|
|
199
|
+
return { success: true, profile };
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
203
|
+
log("getProfile: error", { error: errorMessage });
|
|
204
|
+
return { success: false, error: errorMessage, profile: null };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async listMemories(containerTag, limit = 20) {
|
|
208
|
+
log("listMemories: start", { containerTag, limit });
|
|
209
|
+
try {
|
|
210
|
+
if (!this.db)
|
|
211
|
+
throw new Error("Database not initialized");
|
|
212
|
+
const stmt = this.db.prepare(`
|
|
213
|
+
SELECT * FROM memories
|
|
214
|
+
WHERE containerTag = ?
|
|
215
|
+
ORDER BY createdAt DESC
|
|
216
|
+
LIMIT ?
|
|
217
|
+
`);
|
|
218
|
+
const rows = stmt.all(containerTag, limit);
|
|
219
|
+
const memories = rows.map((r) => ({
|
|
220
|
+
id: r.id,
|
|
221
|
+
summary: r.content,
|
|
222
|
+
createdAt: new Date(r.createdAt).toISOString(),
|
|
223
|
+
metadata: r.metadata ? JSON.parse(r.metadata) : undefined,
|
|
224
|
+
displayName: r.displayName,
|
|
225
|
+
userName: r.userName,
|
|
226
|
+
userEmail: r.userEmail,
|
|
227
|
+
projectPath: r.projectPath,
|
|
228
|
+
projectName: r.projectName,
|
|
229
|
+
gitRepoUrl: r.gitRepoUrl,
|
|
230
|
+
}));
|
|
231
|
+
log("listMemories: success", { count: memories.length });
|
|
232
|
+
return {
|
|
233
|
+
success: true,
|
|
234
|
+
memories,
|
|
235
|
+
pagination: { currentPage: 1, totalItems: memories.length, totalPages: 1 },
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
240
|
+
log("listMemories: error", { error: errorMessage });
|
|
241
|
+
return {
|
|
242
|
+
success: false,
|
|
243
|
+
error: errorMessage,
|
|
244
|
+
memories: [],
|
|
245
|
+
pagination: { currentPage: 1, totalItems: 0, totalPages: 0 },
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async deleteMemory(memoryId) {
|
|
250
|
+
log("deleteMemory: start", { memoryId });
|
|
251
|
+
try {
|
|
252
|
+
if (!this.db)
|
|
253
|
+
throw new Error("Database not initialized");
|
|
254
|
+
const deleteMemory = this.db.prepare("DELETE FROM memories WHERE id = ?");
|
|
255
|
+
const deleteVec = this.db.prepare("DELETE FROM vec_memories WHERE id = ?");
|
|
256
|
+
const result = this.db.transaction(() => {
|
|
257
|
+
const info = deleteMemory.run(memoryId);
|
|
258
|
+
deleteVec.run(memoryId);
|
|
259
|
+
return info.changes > 0;
|
|
260
|
+
})();
|
|
261
|
+
if (result) {
|
|
262
|
+
log("deleteMemory: success", { memoryId });
|
|
263
|
+
return { success: true };
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
log("deleteMemory: not found", { memoryId });
|
|
267
|
+
return { success: false, error: "Memory not found" };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
272
|
+
log("deleteMemory: error", { memoryId, error: errorMessage });
|
|
273
|
+
return { success: false, error: errorMessage };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
shutdown() {
|
|
277
|
+
if (this.db) {
|
|
278
|
+
this.db.close();
|
|
279
|
+
this.db = null;
|
|
280
|
+
}
|
|
281
|
+
this.isInitialized = false;
|
|
282
|
+
log("SQLite memory client shutdown");
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface TagInfo {
|
|
2
|
+
tag: string;
|
|
3
|
+
displayName: string;
|
|
4
|
+
userName?: string;
|
|
5
|
+
userEmail?: string;
|
|
6
|
+
projectPath?: string;
|
|
7
|
+
projectName?: string;
|
|
8
|
+
gitRepoUrl?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function getGitEmail(): string | null;
|
|
11
|
+
export declare function getGitName(): string | null;
|
|
12
|
+
export declare function getGitRepoUrl(directory: string): string | null;
|
|
13
|
+
export declare function getProjectName(directory: string): string;
|
|
14
|
+
export declare function getUserTagInfo(): TagInfo;
|
|
15
|
+
export declare function getProjectTagInfo(directory: string): TagInfo;
|
|
16
|
+
export declare function getTags(directory: string): {
|
|
17
|
+
user: TagInfo;
|
|
18
|
+
project: TagInfo;
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=tags.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tags.d.ts","sourceRoot":"","sources":["../../src/services/tags.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,OAAO;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,WAAW,IAAI,MAAM,GAAG,IAAI,CAO3C;AAED,wBAAgB,UAAU,IAAI,MAAM,GAAG,IAAI,CAO1C;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAU9D;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAGxD;AAED,wBAAgB,cAAc,IAAI,OAAO,CAoBxC;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAW5D;AAED,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG;IAC1C,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB,CAKA"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import { CONFIG } from "../config.js";
|
|
4
|
+
function sha256(input) {
|
|
5
|
+
return createHash("sha256").update(input).digest("hex").slice(0, 16);
|
|
6
|
+
}
|
|
7
|
+
export function getGitEmail() {
|
|
8
|
+
try {
|
|
9
|
+
const email = execSync("git config user.email", { encoding: "utf-8" }).trim();
|
|
10
|
+
return email || null;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function getGitName() {
|
|
17
|
+
try {
|
|
18
|
+
const name = execSync("git config user.name", { encoding: "utf-8" }).trim();
|
|
19
|
+
return name || null;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function getGitRepoUrl(directory) {
|
|
26
|
+
try {
|
|
27
|
+
const url = execSync("git config --get remote.origin.url", {
|
|
28
|
+
encoding: "utf-8",
|
|
29
|
+
cwd: directory,
|
|
30
|
+
}).trim();
|
|
31
|
+
return url || null;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function getProjectName(directory) {
|
|
38
|
+
const parts = directory.split("/").filter((p) => p);
|
|
39
|
+
return parts[parts.length - 1] || directory;
|
|
40
|
+
}
|
|
41
|
+
export function getUserTagInfo() {
|
|
42
|
+
const email = getGitEmail();
|
|
43
|
+
const name = getGitName();
|
|
44
|
+
if (email) {
|
|
45
|
+
return {
|
|
46
|
+
tag: `${CONFIG.containerTagPrefix}_user_${sha256(email)}`,
|
|
47
|
+
displayName: name || email,
|
|
48
|
+
userName: name || undefined,
|
|
49
|
+
userEmail: email,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const fallback = process.env.USER || process.env.USERNAME || "anonymous";
|
|
53
|
+
return {
|
|
54
|
+
tag: `${CONFIG.containerTagPrefix}_user_${sha256(fallback)}`,
|
|
55
|
+
displayName: fallback,
|
|
56
|
+
userName: fallback,
|
|
57
|
+
userEmail: undefined,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function getProjectTagInfo(directory) {
|
|
61
|
+
const projectName = getProjectName(directory);
|
|
62
|
+
const gitRepoUrl = getGitRepoUrl(directory);
|
|
63
|
+
return {
|
|
64
|
+
tag: `${CONFIG.containerTagPrefix}_project_${sha256(directory)}`,
|
|
65
|
+
displayName: directory,
|
|
66
|
+
projectPath: directory,
|
|
67
|
+
projectName,
|
|
68
|
+
gitRepoUrl: gitRepoUrl || undefined,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function getTags(directory) {
|
|
72
|
+
return {
|
|
73
|
+
user: getUserTagInfo(),
|
|
74
|
+
project: getProjectTagInfo(directory),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare class WebServerLock {
|
|
2
|
+
private pid;
|
|
3
|
+
private cleanupRegistered;
|
|
4
|
+
constructor();
|
|
5
|
+
private registerCleanupHandlers;
|
|
6
|
+
acquire(port: number, host: string): Promise<boolean>;
|
|
7
|
+
release(): Promise<boolean>;
|
|
8
|
+
private isProcessAlive;
|
|
9
|
+
private writeLock;
|
|
10
|
+
static cleanup(): void;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=web-server-lock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-server-lock.d.ts","sourceRoot":"","sources":["../../src/services/web-server-lock.ts"],"names":[],"mappings":"AAeA,qBAAa,aAAa;IACxB,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,iBAAiB,CAAkB;;IAO3C,OAAO,CAAC,uBAAuB;IAwBzB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAmDrD,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC;IAqCjC,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,SAAS;IAIjB,MAAM,CAAC,OAAO,IAAI,IAAI;CAiCvB"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { log } from "./logger.js";
|
|
5
|
+
const LOCK_DIR = join(homedir(), ".opencode-mem");
|
|
6
|
+
const LOCK_FILE = join(LOCK_DIR, "webserver.lock");
|
|
7
|
+
export class WebServerLock {
|
|
8
|
+
pid;
|
|
9
|
+
cleanupRegistered = false;
|
|
10
|
+
constructor() {
|
|
11
|
+
this.pid = process.pid;
|
|
12
|
+
this.registerCleanupHandlers();
|
|
13
|
+
}
|
|
14
|
+
registerCleanupHandlers() {
|
|
15
|
+
if (this.cleanupRegistered)
|
|
16
|
+
return;
|
|
17
|
+
this.cleanupRegistered = true;
|
|
18
|
+
const cleanup = () => {
|
|
19
|
+
this.release().catch(() => { });
|
|
20
|
+
};
|
|
21
|
+
process.on('exit', cleanup);
|
|
22
|
+
process.on('SIGTERM', () => {
|
|
23
|
+
cleanup();
|
|
24
|
+
process.exit(0);
|
|
25
|
+
});
|
|
26
|
+
process.on('SIGINT', () => {
|
|
27
|
+
cleanup();
|
|
28
|
+
process.exit(0);
|
|
29
|
+
});
|
|
30
|
+
process.on('uncaughtException', (error) => {
|
|
31
|
+
log("WebServerLock: uncaught exception", { error: String(error) });
|
|
32
|
+
cleanup();
|
|
33
|
+
process.exit(1);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async acquire(port, host) {
|
|
37
|
+
try {
|
|
38
|
+
if (existsSync(LOCK_FILE)) {
|
|
39
|
+
const content = readFileSync(LOCK_FILE, "utf-8");
|
|
40
|
+
const lockData = JSON.parse(content);
|
|
41
|
+
const alivePids = lockData.pids.filter(pid => this.isProcessAlive(pid));
|
|
42
|
+
if (alivePids.length > 0) {
|
|
43
|
+
if (lockData.port === port && lockData.host === host) {
|
|
44
|
+
alivePids.push(this.pid);
|
|
45
|
+
this.writeLock({
|
|
46
|
+
pids: alivePids,
|
|
47
|
+
port: lockData.port,
|
|
48
|
+
host: lockData.host,
|
|
49
|
+
startedAt: lockData.startedAt,
|
|
50
|
+
});
|
|
51
|
+
log("WebServerLock: joined existing server", {
|
|
52
|
+
pid: this.pid,
|
|
53
|
+
totalInstances: alivePids.length
|
|
54
|
+
});
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
log("WebServerLock: port conflict", {
|
|
59
|
+
requestedPort: port,
|
|
60
|
+
existingPort: lockData.port
|
|
61
|
+
});
|
|
62
|
+
throw new Error(`Web server already running on ${lockData.host}:${lockData.port}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
this.writeLock({
|
|
67
|
+
pids: [this.pid],
|
|
68
|
+
port,
|
|
69
|
+
host,
|
|
70
|
+
startedAt: Date.now(),
|
|
71
|
+
});
|
|
72
|
+
log("WebServerLock: acquired", { pid: this.pid, port, host });
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error instanceof Error && error.message.includes("already running")) {
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
log("WebServerLock: acquire error", { error: String(error) });
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async release() {
|
|
84
|
+
try {
|
|
85
|
+
if (!existsSync(LOCK_FILE)) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
const content = readFileSync(LOCK_FILE, "utf-8");
|
|
89
|
+
const lockData = JSON.parse(content);
|
|
90
|
+
const remainingPids = lockData.pids.filter(pid => pid !== this.pid && this.isProcessAlive(pid));
|
|
91
|
+
if (remainingPids.length === 0) {
|
|
92
|
+
unlinkSync(LOCK_FILE);
|
|
93
|
+
log("WebServerLock: released (last instance)", { pid: this.pid });
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
this.writeLock({
|
|
98
|
+
pids: remainingPids,
|
|
99
|
+
port: lockData.port,
|
|
100
|
+
host: lockData.host,
|
|
101
|
+
startedAt: lockData.startedAt,
|
|
102
|
+
});
|
|
103
|
+
log("WebServerLock: released (instances remaining)", {
|
|
104
|
+
pid: this.pid,
|
|
105
|
+
remaining: remainingPids.length
|
|
106
|
+
});
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
log("WebServerLock: release error", { error: String(error) });
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
isProcessAlive(pid) {
|
|
116
|
+
try {
|
|
117
|
+
process.kill(pid, 0);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
writeLock(data) {
|
|
125
|
+
writeFileSync(LOCK_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
126
|
+
}
|
|
127
|
+
static cleanup() {
|
|
128
|
+
try {
|
|
129
|
+
if (existsSync(LOCK_FILE)) {
|
|
130
|
+
const content = readFileSync(LOCK_FILE, "utf-8");
|
|
131
|
+
const lockData = JSON.parse(content);
|
|
132
|
+
const alivePids = lockData.pids.filter(pid => {
|
|
133
|
+
try {
|
|
134
|
+
process.kill(pid, 0);
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
if (alivePids.length === 0) {
|
|
142
|
+
unlinkSync(LOCK_FILE);
|
|
143
|
+
log("WebServerLock: cleanup completed (no alive processes)");
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
writeFileSync(LOCK_FILE, JSON.stringify({ ...lockData, pids: alivePids }, null, 2), "utf-8");
|
|
147
|
+
log("WebServerLock: cleanup completed (alive processes remain)", {
|
|
148
|
+
count: alivePids.length
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
log("WebServerLock: cleanup error", { error: String(error) });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-server-worker.d.ts","sourceRoot":"","sources":["../../src/services/web-server-worker.ts"],"names":[],"mappings":""}
|