opencode-mem-agents 0.3.1 → 0.3.2
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 +97 -0
- package/dist/contracts.d.ts +175 -0
- package/dist/contracts.js +170 -0
- package/dist/dashboard-html.d.ts +1 -1
- package/dist/dashboard-html.js +12 -12
- package/dist/index.js +273 -73
- package/dist/worker/config.d.ts +14 -0
- package/dist/worker/config.js +44 -0
- package/dist/worker/http.d.ts +23 -0
- package/dist/worker/http.js +130 -0
- package/dist/worker/repository.d.ts +61 -0
- package/dist/worker/repository.js +535 -0
- package/dist/worker/routes.d.ts +31 -0
- package/dist/worker/routes.js +251 -0
- package/dist/worker/server.d.ts +1 -0
- package/dist/worker/server.js +177 -0
- package/dist/worker/utils.d.ts +8 -0
- package/dist/worker/utils.js +98 -0
- package/dist/worker.d.ts +0 -16
- package/dist/worker.js +2 -647
- package/package.json +4 -2
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { activityParamsSchema, contextSessionParamsSchema, memorySaveBodySchema, observationsBatchBodySchema, searchParamsSchema, timelineParamsSchema, tracesParamsSchema, toolResultBodySchema, } from "../contracts.js";
|
|
2
|
+
import { DASHBOARD_HTML } from "../dashboard-html.js";
|
|
3
|
+
import { parseSchema } from "./http.js";
|
|
4
|
+
const ROLE_PRIORITY = {
|
|
5
|
+
lead: ["decision", "contract", "blocker", "review-finding"],
|
|
6
|
+
backend: ["contract", "feature", "bugfix", "refactor"],
|
|
7
|
+
frontend: ["feature", "bugfix", "refactor", "contract"],
|
|
8
|
+
database: ["contract", "decision", "bugfix", "refactor"],
|
|
9
|
+
test: ["test-result", "bugfix", "review-finding"],
|
|
10
|
+
security: ["review-finding", "blocker", "contract", "decision"],
|
|
11
|
+
reviewer: ["review-finding", "contract", "decision", "bugfix"],
|
|
12
|
+
default: ["decision", "contract", "blocker", "discovery"],
|
|
13
|
+
};
|
|
14
|
+
export function handleDashboard() {
|
|
15
|
+
return {
|
|
16
|
+
kind: "html",
|
|
17
|
+
status: 200,
|
|
18
|
+
body: DASHBOARD_HTML,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function handleHealth(ctx) {
|
|
22
|
+
return {
|
|
23
|
+
kind: "json",
|
|
24
|
+
status: 200,
|
|
25
|
+
body: {
|
|
26
|
+
status: "ok",
|
|
27
|
+
version: ctx.version,
|
|
28
|
+
port: ctx.config.port,
|
|
29
|
+
dataDir: ctx.config.dataDir,
|
|
30
|
+
uptime: Date.now() - ctx.startedAt,
|
|
31
|
+
pid: process.pid,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function handleSearch(ctx, paramsRaw) {
|
|
36
|
+
const params = parseSchema(searchParamsSchema, paramsRaw);
|
|
37
|
+
const result = ctx.repo.search(params);
|
|
38
|
+
return {
|
|
39
|
+
kind: "json",
|
|
40
|
+
status: 200,
|
|
41
|
+
body: {
|
|
42
|
+
observations: result.observations,
|
|
43
|
+
totalResults: result.totalResults,
|
|
44
|
+
query: params.query ?? "",
|
|
45
|
+
offset: params.offset,
|
|
46
|
+
limit: params.limit,
|
|
47
|
+
orderBy: params.orderBy,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function formatTimelineText(observations, anchorId, depthBefore, depthAfter) {
|
|
52
|
+
const lines = [
|
|
53
|
+
"# Timeline",
|
|
54
|
+
`Anchor: #${anchorId} | Window: ${depthBefore} before, ${depthAfter} after | Items: ${observations.length}`,
|
|
55
|
+
"",
|
|
56
|
+
];
|
|
57
|
+
let currentDay = "";
|
|
58
|
+
for (const obs of observations) {
|
|
59
|
+
const d = new Date(obs.created_at_epoch);
|
|
60
|
+
const day = d.toISOString().slice(0, 10);
|
|
61
|
+
if (day !== currentDay) {
|
|
62
|
+
currentDay = day;
|
|
63
|
+
lines.push(`## ${day}`, "");
|
|
64
|
+
}
|
|
65
|
+
const time = d.toISOString().slice(11, 16);
|
|
66
|
+
const marker = obs.id === anchorId ? " <-- anchor" : "";
|
|
67
|
+
const content = obs.title || obs.text?.slice(0, 80) || "(no title)";
|
|
68
|
+
lines.push(`- **${time}** [${obs.type}] #${obs.id}: ${content}${marker}`);
|
|
69
|
+
}
|
|
70
|
+
return lines.join("\n");
|
|
71
|
+
}
|
|
72
|
+
export function handleTimeline(ctx, paramsRaw) {
|
|
73
|
+
const params = parseSchema(timelineParamsSchema, paramsRaw);
|
|
74
|
+
const timeline = ctx.repo.timeline(params);
|
|
75
|
+
if (timeline.observations.length === 0) {
|
|
76
|
+
if (params.format === "json") {
|
|
77
|
+
return {
|
|
78
|
+
kind: "json",
|
|
79
|
+
status: 200,
|
|
80
|
+
body: {
|
|
81
|
+
anchorId: null,
|
|
82
|
+
observations: [],
|
|
83
|
+
depth_before: timeline.depthBefore,
|
|
84
|
+
depth_after: timeline.depthAfter,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
kind: "text",
|
|
90
|
+
status: 200,
|
|
91
|
+
body: "No observations found.",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (params.format === "json") {
|
|
95
|
+
return {
|
|
96
|
+
kind: "json",
|
|
97
|
+
status: 200,
|
|
98
|
+
body: {
|
|
99
|
+
anchorId: timeline.anchorId,
|
|
100
|
+
observations: timeline.observations,
|
|
101
|
+
depth_before: timeline.depthBefore,
|
|
102
|
+
depth_after: timeline.depthAfter,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
kind: "text",
|
|
108
|
+
status: 200,
|
|
109
|
+
body: formatTimelineText(timeline.observations, timeline.anchorId, timeline.depthBefore, timeline.depthAfter),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export function handleObservationsBatch(ctx, bodyRaw) {
|
|
113
|
+
const body = parseSchema(observationsBatchBodySchema, bodyRaw);
|
|
114
|
+
const observations = ctx.repo.getByIds(body.ids, body.project);
|
|
115
|
+
return {
|
|
116
|
+
kind: "json",
|
|
117
|
+
status: 200,
|
|
118
|
+
body: { observations },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export function handleMemorySave(ctx, bodyRaw) {
|
|
122
|
+
const body = parseSchema(memorySaveBodySchema, bodyRaw);
|
|
123
|
+
const result = ctx.repo.saveMemory(body);
|
|
124
|
+
return {
|
|
125
|
+
kind: "json",
|
|
126
|
+
status: 200,
|
|
127
|
+
body: { id: result.id, status: result.status },
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export function handleToolResult(ctx, bodyRaw) {
|
|
131
|
+
const body = parseSchema(toolResultBodySchema, bodyRaw);
|
|
132
|
+
const result = ctx.repo.saveToolResult(body);
|
|
133
|
+
return {
|
|
134
|
+
kind: "json",
|
|
135
|
+
status: 200,
|
|
136
|
+
body: { id: result.id, status: result.status },
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
export function handleContextSession(ctx, paramsRaw) {
|
|
140
|
+
const params = parseSchema(contextSessionParamsSchema, paramsRaw);
|
|
141
|
+
const rows = ctx.repo.getContextRows(params);
|
|
142
|
+
if (rows.length === 0) {
|
|
143
|
+
return {
|
|
144
|
+
kind: "text",
|
|
145
|
+
status: 200,
|
|
146
|
+
body: "",
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const lines = [
|
|
150
|
+
`# [opencode-mem] Recent context (${rows.length} observations)`,
|
|
151
|
+
"",
|
|
152
|
+
];
|
|
153
|
+
const grouped = {};
|
|
154
|
+
for (const row of rows) {
|
|
155
|
+
(grouped[row.type] ??= []).push(row);
|
|
156
|
+
}
|
|
157
|
+
for (const [type, items] of Object.entries(grouped)) {
|
|
158
|
+
lines.push(`## ${type} (${items.length})`);
|
|
159
|
+
for (const item of items) {
|
|
160
|
+
const agentTag = item.agent ? ` [${item.agent}]` : "";
|
|
161
|
+
const sourceTag = item.session_id ? ` [session:${item.session_id}]` : "";
|
|
162
|
+
const content = item.title || item.text?.slice(0, 300) || "(empty)";
|
|
163
|
+
lines.push(`- #${item.id}${agentTag}${sourceTag}: ${content}`);
|
|
164
|
+
}
|
|
165
|
+
lines.push("");
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
kind: "text",
|
|
169
|
+
status: 200,
|
|
170
|
+
body: lines.join("\n"),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
export function handleActivity(ctx, paramsRaw) {
|
|
174
|
+
const params = parseSchema(activityParamsSchema, paramsRaw);
|
|
175
|
+
const rows = ctx.repo.getActivityRows(params);
|
|
176
|
+
const role = (params.role ?? "default").toLowerCase();
|
|
177
|
+
const preferred = ROLE_PRIORITY[role] ?? ROLE_PRIORITY.default;
|
|
178
|
+
const preferredSet = new Set(preferred);
|
|
179
|
+
const agents = {};
|
|
180
|
+
const fileRollups = {};
|
|
181
|
+
for (const row of rows) {
|
|
182
|
+
const agentName = row.agent || "unknown";
|
|
183
|
+
if (!agents[agentName]) {
|
|
184
|
+
agents[agentName] = {
|
|
185
|
+
observations: [],
|
|
186
|
+
files_modified: [],
|
|
187
|
+
last_active: 0,
|
|
188
|
+
};
|
|
189
|
+
fileRollups[agentName] = new Set();
|
|
190
|
+
}
|
|
191
|
+
agents[agentName].observations.push({
|
|
192
|
+
id: row.id,
|
|
193
|
+
type: row.type,
|
|
194
|
+
title: row.title,
|
|
195
|
+
signal: row.signal,
|
|
196
|
+
phase: row.phase,
|
|
197
|
+
task_id: row.task_id,
|
|
198
|
+
created_at_epoch: row.created_at_epoch,
|
|
199
|
+
role_priority: preferredSet.has(row.type) ? 0 : 1,
|
|
200
|
+
});
|
|
201
|
+
if (row.created_at_epoch > agents[agentName].last_active) {
|
|
202
|
+
agents[agentName].last_active = row.created_at_epoch;
|
|
203
|
+
}
|
|
204
|
+
const modifiedFiles = safeParseArray(row.files_modified);
|
|
205
|
+
for (const filePath of modifiedFiles) {
|
|
206
|
+
if (filePath)
|
|
207
|
+
fileRollups[agentName].add(filePath);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
for (const [agentName, data] of Object.entries(agents)) {
|
|
211
|
+
data.observations.sort((a, b) => {
|
|
212
|
+
if (a.role_priority !== b.role_priority)
|
|
213
|
+
return a.role_priority - b.role_priority;
|
|
214
|
+
return b.created_at_epoch - a.created_at_epoch;
|
|
215
|
+
});
|
|
216
|
+
for (const obs of data.observations) {
|
|
217
|
+
delete obs.role_priority;
|
|
218
|
+
}
|
|
219
|
+
data.files_modified = Array.from(fileRollups[agentName]);
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
kind: "json",
|
|
223
|
+
status: 200,
|
|
224
|
+
body: {
|
|
225
|
+
project: params.project ?? "",
|
|
226
|
+
since_id: params.since_id,
|
|
227
|
+
total_observations: rows.length,
|
|
228
|
+
role,
|
|
229
|
+
preferred_types: preferred,
|
|
230
|
+
agents,
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
export function handleTraces(ctx, paramsRaw) {
|
|
235
|
+
const params = parseSchema(tracesParamsSchema, paramsRaw);
|
|
236
|
+
const events = ctx.repo.getTraces(params);
|
|
237
|
+
return {
|
|
238
|
+
kind: "json",
|
|
239
|
+
status: 200,
|
|
240
|
+
body: { events },
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function safeParseArray(raw) {
|
|
244
|
+
try {
|
|
245
|
+
const parsed = JSON.parse(raw);
|
|
246
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function startWorkerServer(): void;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
3
|
+
import http from "http";
|
|
4
|
+
import { applyCors, InputValidationError, isAuthorized, readBody, requiresAuth, sendError, sendHtml, sendJson, sendText, } from "./http.js";
|
|
5
|
+
import { WorkerRepository } from "./repository.js";
|
|
6
|
+
import { readWorkerConfig, WORKER_VERSION } from "./config.js";
|
|
7
|
+
import { parseQuery } from "./utils.js";
|
|
8
|
+
import { handleActivity, handleContextSession, handleDashboard, handleHealth, handleMemorySave, handleObservationsBatch, handleSearch, handleTimeline, handleToolResult, handleTraces, } from "./routes.js";
|
|
9
|
+
export function startWorkerServer() {
|
|
10
|
+
const config = readWorkerConfig();
|
|
11
|
+
lockPidFile(config.pidFile);
|
|
12
|
+
const startedAt = Date.now();
|
|
13
|
+
const repo = new WorkerRepository(config.dbPath, config.traceRetentionDays, config.traceMaxPayloadChars);
|
|
14
|
+
const routeCtx = {
|
|
15
|
+
config,
|
|
16
|
+
startedAt,
|
|
17
|
+
version: WORKER_VERSION,
|
|
18
|
+
repo,
|
|
19
|
+
};
|
|
20
|
+
const traceLog = (ctx, direction, status, payload, durationMs = 0) => {
|
|
21
|
+
repo.logTrace(ctx.traceId, direction, ctx.method, ctx.path, status, payload, durationMs);
|
|
22
|
+
};
|
|
23
|
+
const server = http.createServer(async (req, res) => {
|
|
24
|
+
const method = req.method ?? "GET";
|
|
25
|
+
const url = new URL(req.url ?? "/", `http://${config.host}:${config.port}`);
|
|
26
|
+
const path = url.pathname;
|
|
27
|
+
const requestCtx = {
|
|
28
|
+
traceId: randomUUID(),
|
|
29
|
+
method,
|
|
30
|
+
path,
|
|
31
|
+
startedAt: Date.now(),
|
|
32
|
+
};
|
|
33
|
+
const corsAllowed = applyCors(req, res, config.corsOrigin);
|
|
34
|
+
if (!corsAllowed) {
|
|
35
|
+
traceLog(requestCtx, "in", 0, { rejected: "cors", origin: req.headers.origin ?? "" });
|
|
36
|
+
sendError(requestCtx, res, 403, "FORBIDDEN_ORIGIN", "Origin is not allowed by CORS policy", traceLog);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (method === "OPTIONS") {
|
|
40
|
+
res.writeHead(204);
|
|
41
|
+
res.end();
|
|
42
|
+
traceLog(requestCtx, "out", 204, { preflight: true }, Date.now() - requestCtx.startedAt);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (requiresAuth(path, config.apiToken) && !isAuthorized(req, config.apiToken)) {
|
|
46
|
+
traceLog(requestCtx, "in", 0, { rejected: "auth", path });
|
|
47
|
+
sendError(requestCtx, res, 401, "UNAUTHORIZED", "Missing or invalid API token", traceLog);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const queryParams = parseQuery(url);
|
|
51
|
+
traceLog(requestCtx, "in", 0, { query: queryParams });
|
|
52
|
+
try {
|
|
53
|
+
if (method === "GET") {
|
|
54
|
+
if (path === "/" || path === "/dashboard") {
|
|
55
|
+
return sendRouteResult(requestCtx, res, traceLog, handleDashboard());
|
|
56
|
+
}
|
|
57
|
+
if (path === "/api/health" || path === "/api/readiness") {
|
|
58
|
+
return sendRouteResult(requestCtx, res, traceLog, handleHealth(routeCtx));
|
|
59
|
+
}
|
|
60
|
+
if (path === "/api/search") {
|
|
61
|
+
return sendRouteResult(requestCtx, res, traceLog, handleSearch(routeCtx, queryParams));
|
|
62
|
+
}
|
|
63
|
+
if (path === "/api/timeline") {
|
|
64
|
+
return sendRouteResult(requestCtx, res, traceLog, handleTimeline(routeCtx, queryParams));
|
|
65
|
+
}
|
|
66
|
+
if (path === "/api/context/session") {
|
|
67
|
+
return sendRouteResult(requestCtx, res, traceLog, handleContextSession(routeCtx, queryParams));
|
|
68
|
+
}
|
|
69
|
+
if (path === "/api/activity") {
|
|
70
|
+
return sendRouteResult(requestCtx, res, traceLog, handleActivity(routeCtx, queryParams));
|
|
71
|
+
}
|
|
72
|
+
if (path === "/api/traces") {
|
|
73
|
+
return sendRouteResult(requestCtx, res, traceLog, handleTraces(routeCtx, queryParams));
|
|
74
|
+
}
|
|
75
|
+
sendError(requestCtx, res, 404, "NOT_FOUND", "Route not found", traceLog);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (method === "POST") {
|
|
79
|
+
let rawBody = "";
|
|
80
|
+
try {
|
|
81
|
+
rawBody = await readBody(req, config.maxBodyBytes);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error instanceof InputValidationError) {
|
|
85
|
+
sendError(requestCtx, res, 413, "PAYLOAD_TOO_LARGE", error.message, traceLog);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
let body = {};
|
|
91
|
+
if (rawBody.trim()) {
|
|
92
|
+
try {
|
|
93
|
+
body = JSON.parse(rawBody);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
sendError(requestCtx, res, 400, "INVALID_JSON", "Request body must be valid JSON", traceLog);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
traceLog(requestCtx, "in", 0, { body });
|
|
101
|
+
if (path === "/api/observations/batch") {
|
|
102
|
+
return sendRouteResult(requestCtx, res, traceLog, handleObservationsBatch(routeCtx, body));
|
|
103
|
+
}
|
|
104
|
+
if (path === "/api/memory/save") {
|
|
105
|
+
return sendRouteResult(requestCtx, res, traceLog, handleMemorySave(routeCtx, body));
|
|
106
|
+
}
|
|
107
|
+
if (path === "/api/session/tool-result") {
|
|
108
|
+
return sendRouteResult(requestCtx, res, traceLog, handleToolResult(routeCtx, body));
|
|
109
|
+
}
|
|
110
|
+
sendError(requestCtx, res, 404, "NOT_FOUND", "Route not found", traceLog);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
sendError(requestCtx, res, 405, "METHOD_NOT_ALLOWED", `Method ${method} is not supported`, traceLog);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (error instanceof InputValidationError) {
|
|
117
|
+
sendError(requestCtx, res, 400, "INVALID_INPUT", error.message, traceLog);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const message = error instanceof Error ? error.message : "Internal error";
|
|
121
|
+
console.error(`[opencode-mem] Error on ${requestCtx.path}:`, message);
|
|
122
|
+
traceLog(requestCtx, "error", 500, { message }, Date.now() - requestCtx.startedAt);
|
|
123
|
+
sendError(requestCtx, res, 500, "INTERNAL_ERROR", "Internal server error", traceLog);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
server.listen(config.port, config.host, () => {
|
|
127
|
+
console.log(`[opencode-mem] Worker listening on http://${config.host}:${config.port}`);
|
|
128
|
+
console.log(`[opencode-mem] Database: ${config.dbPath}`);
|
|
129
|
+
});
|
|
130
|
+
const cleanup = () => {
|
|
131
|
+
try {
|
|
132
|
+
unlinkSync(config.pidFile);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// ignore
|
|
136
|
+
}
|
|
137
|
+
repo.close();
|
|
138
|
+
};
|
|
139
|
+
process.on("SIGTERM", () => {
|
|
140
|
+
console.log("[opencode-mem] Shutting down...");
|
|
141
|
+
server.close(() => {
|
|
142
|
+
cleanup();
|
|
143
|
+
process.exit(0);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
process.on("SIGINT", () => {
|
|
147
|
+
server.close(() => {
|
|
148
|
+
cleanup();
|
|
149
|
+
process.exit(0);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function sendRouteResult(ctx, res, traceLog, result) {
|
|
154
|
+
if (result.kind === "json") {
|
|
155
|
+
sendJson(ctx, res, result.status, result.body, traceLog);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (result.kind === "text") {
|
|
159
|
+
sendText(ctx, res, result.status, result.body, traceLog);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
sendHtml(ctx, res, result.status, result.body, traceLog);
|
|
163
|
+
}
|
|
164
|
+
function lockPidFile(pidFile) {
|
|
165
|
+
if (existsSync(pidFile)) {
|
|
166
|
+
const existingPid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
|
|
167
|
+
try {
|
|
168
|
+
process.kill(existingPid, 0);
|
|
169
|
+
console.log(`[opencode-mem] Worker already running (pid ${existingPid}), exiting`);
|
|
170
|
+
process.exit(0);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// stale pid file
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
writeFileSync(pidFile, String(process.pid));
|
|
177
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function parseQuery(url: URL): Record<string, string>;
|
|
2
|
+
export declare function sanitizeText(value: string, max: number): string;
|
|
3
|
+
export declare function sanitizeFtsQuery(query: string): string;
|
|
4
|
+
export declare function safeSerialize(value: unknown, maxChars?: number): string;
|
|
5
|
+
export declare function safeParseJson<T>(raw: string, fallback: T): T;
|
|
6
|
+
export declare function toEpochMs(value: string | undefined): number | undefined;
|
|
7
|
+
export declare function redactSensitive(value: unknown, depth?: number): unknown;
|
|
8
|
+
export declare function prepareTracePayload(value: unknown, maxChars: number): string;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export function parseQuery(url) {
|
|
2
|
+
const query = {};
|
|
3
|
+
url.searchParams.forEach((value, key) => {
|
|
4
|
+
query[key] = value;
|
|
5
|
+
});
|
|
6
|
+
return query;
|
|
7
|
+
}
|
|
8
|
+
export function sanitizeText(value, max) {
|
|
9
|
+
return value.substring(0, max).replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
|
|
10
|
+
}
|
|
11
|
+
export function sanitizeFtsQuery(query) {
|
|
12
|
+
return query
|
|
13
|
+
.replace(/[*"():^]/g, " ")
|
|
14
|
+
.trim()
|
|
15
|
+
.split(/\s+/)
|
|
16
|
+
.filter(Boolean)
|
|
17
|
+
.map((term) => `"${term}"`)
|
|
18
|
+
.join(" ");
|
|
19
|
+
}
|
|
20
|
+
export function safeSerialize(value, maxChars = 4000) {
|
|
21
|
+
let text;
|
|
22
|
+
try {
|
|
23
|
+
text = JSON.stringify(value);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
text = String(value);
|
|
27
|
+
}
|
|
28
|
+
if (text.length <= maxChars)
|
|
29
|
+
return text;
|
|
30
|
+
return `${text.slice(0, maxChars)}...[truncated ${text.length - maxChars} chars]`;
|
|
31
|
+
}
|
|
32
|
+
export function safeParseJson(raw, fallback) {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(raw);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return fallback;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function toEpochMs(value) {
|
|
41
|
+
if (!value)
|
|
42
|
+
return undefined;
|
|
43
|
+
const epoch = Date.parse(value);
|
|
44
|
+
if (!Number.isFinite(epoch))
|
|
45
|
+
return undefined;
|
|
46
|
+
return epoch;
|
|
47
|
+
}
|
|
48
|
+
const SENSITIVE_KEY_PATTERN = /(^|[_-])(auth|authorization|token|secret|password|api[_-]?key|cookie|set-cookie)($|[_-])/i;
|
|
49
|
+
function isSensitiveKey(key) {
|
|
50
|
+
return SENSITIVE_KEY_PATTERN.test(key);
|
|
51
|
+
}
|
|
52
|
+
function clampDepth(depth) {
|
|
53
|
+
return depth >= 10;
|
|
54
|
+
}
|
|
55
|
+
const SECRET_VALUE_PATTERNS = [
|
|
56
|
+
/\bsk-[a-z0-9_-]{8,}\b/gi,
|
|
57
|
+
/\bghp_[a-z0-9]{20,}\b/gi,
|
|
58
|
+
/\bgithub_pat_[a-z0-9_]{20,}\b/gi,
|
|
59
|
+
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
60
|
+
/\bBearer\s+[a-z0-9._~+/=-]{8,}\b/gi,
|
|
61
|
+
];
|
|
62
|
+
function sanitizeTraceString(value) {
|
|
63
|
+
let sanitized = value;
|
|
64
|
+
if (/^bearer\s+/i.test(sanitized))
|
|
65
|
+
return "[redacted]";
|
|
66
|
+
for (const pattern of SECRET_VALUE_PATTERNS) {
|
|
67
|
+
sanitized = sanitized.replace(pattern, "[redacted]");
|
|
68
|
+
}
|
|
69
|
+
sanitized = sanitized.replace(/\b(api[_-]?key|token|secret|password)\s*[:=]\s*([^\s,;'"`]+)/gi, "$1=[redacted]");
|
|
70
|
+
return sanitized;
|
|
71
|
+
}
|
|
72
|
+
export function redactSensitive(value, depth = 0) {
|
|
73
|
+
if (value === null || value === undefined)
|
|
74
|
+
return value;
|
|
75
|
+
if (clampDepth(depth))
|
|
76
|
+
return "[depth-limited]";
|
|
77
|
+
if (typeof value === "string") {
|
|
78
|
+
return sanitizeTraceString(value);
|
|
79
|
+
}
|
|
80
|
+
if (typeof value !== "object")
|
|
81
|
+
return value;
|
|
82
|
+
if (Array.isArray(value)) {
|
|
83
|
+
return value.map((entry) => redactSensitive(entry, depth + 1));
|
|
84
|
+
}
|
|
85
|
+
const source = value;
|
|
86
|
+
const output = {};
|
|
87
|
+
for (const [key, entry] of Object.entries(source)) {
|
|
88
|
+
if (isSensitiveKey(key)) {
|
|
89
|
+
output[key] = "[redacted]";
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
output[key] = redactSensitive(entry, depth + 1);
|
|
93
|
+
}
|
|
94
|
+
return output;
|
|
95
|
+
}
|
|
96
|
+
export function prepareTracePayload(value, maxChars) {
|
|
97
|
+
return safeSerialize(redactSensitive(value), maxChars);
|
|
98
|
+
}
|
package/dist/worker.d.ts
CHANGED
|
@@ -1,17 +1 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* opencode-mem worker — Standalone HTTP server with SQLite + FTS5
|
|
3
|
-
*
|
|
4
|
-
* Runs as a daemon on port 37778 (configurable via OPENCODE_MEM_PORT).
|
|
5
|
-
* Stores observations in ~/.opencode-mem/opencode-mem.db.
|
|
6
|
-
*
|
|
7
|
-
* Endpoints:
|
|
8
|
-
* GET /api/health — Health check
|
|
9
|
-
* GET /api/search — FTS5 + metadata search
|
|
10
|
-
* GET /api/timeline — Chronological window around anchor
|
|
11
|
-
* POST /api/observations/batch — Get observations by IDs
|
|
12
|
-
* POST /api/memory/save — Save an observation
|
|
13
|
-
* POST /api/session/tool-result — Capture tool result as observation
|
|
14
|
-
* GET /api/context/session — Formatted context for system prompt
|
|
15
|
-
* GET /api/activity — Team activity grouped by agent
|
|
16
|
-
*/
|
|
17
1
|
export {};
|