om-memory-system 3.1.0-next.4 → 3.2.0-next.10
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 +4 -1
- package/dist/adapters/opencode/auto-capture-summary.js +50 -8
- package/dist/adapters/pi/extension.js +3 -0
- package/dist/adapters/pi/provider.js +41 -14
- package/dist/config.d.ts +7 -0
- package/dist/config.js +21 -0
- package/dist/core/capture-context.js +1 -1
- package/dist/core/capture.d.ts +4 -0
- package/dist/core/capture.js +29 -0
- package/dist/core/extraction.d.ts +23 -1
- package/dist/core/extraction.js +41 -0
- package/dist/core/host.d.ts +23 -0
- package/dist/core/host.js +9 -1
- package/dist/importer/opencode-reader.js +28 -9
- package/dist/index.js +3 -0
- package/dist/services/ai/opencode-provider.d.ts +9 -0
- package/dist/services/ai/opencode-provider.js +31 -4
- package/dist/services/ai/providers/anthropic-messages.js +14 -6
- package/dist/services/ai/providers/base-provider.d.ts +8 -0
- package/dist/services/ai/providers/base-provider.js +12 -0
- package/dist/services/ai/providers/google-gemini.js +21 -7
- package/dist/services/ai/providers/openai-chat-completion.js +20 -7
- package/dist/services/ai/providers/openai-responses.js +11 -0
- package/dist/services/capture-diagnostics.d.ts +66 -0
- package/dist/services/capture-diagnostics.js +172 -0
- package/dist/services/log-path.d.ts +2 -0
- package/dist/services/log-path.js +15 -0
- package/dist/services/logger.js +1 -12
- package/dist/v2/legacy-client.js +10 -1
- package/package.json +1 -1
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { appendFileSync, chmodSync, existsSync, mkdirSync, readdirSync, unlinkSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { getLogDirPath } from "./log-path.js";
|
|
4
|
+
import { log } from "./logger.js";
|
|
5
|
+
import { stripPrivateContent } from "./privacy.js";
|
|
6
|
+
export const DEFAULT_CAPTURE_TRACE_RETENTION_DAYS = 7;
|
|
7
|
+
const TRACE_SCHEMA_VERSION = 1;
|
|
8
|
+
const TRACE_FILE = /^capture-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
|
|
9
|
+
const REDACTED = "[REDACTED]";
|
|
10
|
+
export function buildCaptureAttemptRecord(context, diagnostics, outcome, durationMs) {
|
|
11
|
+
const promptChars = diagnostics.systemPrompt === undefined && diagnostics.userPrompt === undefined
|
|
12
|
+
? null
|
|
13
|
+
: (diagnostics.systemPrompt?.length ?? 0) + (diagnostics.userPrompt?.length ?? 0);
|
|
14
|
+
return {
|
|
15
|
+
host: context.host,
|
|
16
|
+
sourceType: context.sourceType,
|
|
17
|
+
sessionId: context.sessionId,
|
|
18
|
+
path: diagnostics.path ?? null,
|
|
19
|
+
provider: diagnostics.provider ?? null,
|
|
20
|
+
model: diagnostics.model ?? null,
|
|
21
|
+
stopReason: diagnostics.stopReason ?? null,
|
|
22
|
+
blockTypes: diagnostics.blockTypes ?? null,
|
|
23
|
+
promptChars,
|
|
24
|
+
replyChars: diagnostics.rawReply?.length ?? null,
|
|
25
|
+
durationMs,
|
|
26
|
+
outcome,
|
|
27
|
+
reason: outcome === "failed" ? (diagnostics.failureReason ?? "call-error") : null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// Common key and token formats. Each match is replaced whole, except the
|
|
31
|
+
// Bearer rule, which keeps the scheme so the trace still reads naturally.
|
|
32
|
+
const SECRET_PATTERNS = [
|
|
33
|
+
[/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g, REDACTED],
|
|
34
|
+
[/\bsk-ant-[A-Za-z0-9_-]{10,}/g, REDACTED],
|
|
35
|
+
[/\bsk-[A-Za-z0-9_-]{16,}/g, REDACTED],
|
|
36
|
+
[/\bgithub_pat_[A-Za-z0-9_]{20,}/g, REDACTED],
|
|
37
|
+
[/\bgh[pousr]_[A-Za-z0-9]{20,}/g, REDACTED],
|
|
38
|
+
[/\bAKIA[0-9A-Z]{16}\b/g, REDACTED],
|
|
39
|
+
[/\bxox[abprs]-[A-Za-z0-9-]{10,}/g, REDACTED],
|
|
40
|
+
[/\bAIza[0-9A-Za-z_-]{35}/g, REDACTED],
|
|
41
|
+
[/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, REDACTED],
|
|
42
|
+
[/(\bBearer\s+)[A-Za-z0-9._~+/=-]{16,}/gi, `$1${REDACTED}`],
|
|
43
|
+
];
|
|
44
|
+
function configuredSecrets(config) {
|
|
45
|
+
return [
|
|
46
|
+
config.memoryApiKey,
|
|
47
|
+
config.embeddingApiKey,
|
|
48
|
+
config.webServerApiToken,
|
|
49
|
+
config.webServerAuthPassword,
|
|
50
|
+
].filter((value) => typeof value === "string" &&
|
|
51
|
+
// Short values would redact ordinary words; unresolved references are not secrets.
|
|
52
|
+
value.length >= 8 &&
|
|
53
|
+
!value.startsWith("env://") &&
|
|
54
|
+
!value.startsWith("file://"));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Prepare prompt or reply text for the trace file: drop `<private>` regions the
|
|
58
|
+
* same way memory storage does, then configured secrets, then common key and
|
|
59
|
+
* token formats.
|
|
60
|
+
*/
|
|
61
|
+
export function redactTraceText(text, config) {
|
|
62
|
+
let result = stripPrivateContent(text);
|
|
63
|
+
for (const secret of configuredSecrets(config)) {
|
|
64
|
+
result = result.replaceAll(secret, REDACTED);
|
|
65
|
+
}
|
|
66
|
+
for (const [pattern, replacement] of SECRET_PATTERNS) {
|
|
67
|
+
result = result.replace(pattern, replacement);
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
function formatDate(d) {
|
|
72
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
73
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
74
|
+
}
|
|
75
|
+
export function getTraceDirectory() {
|
|
76
|
+
return join(getLogDirPath(), "traces");
|
|
77
|
+
}
|
|
78
|
+
function retentionDays(config) {
|
|
79
|
+
const days = config.captureTraceRetentionDays;
|
|
80
|
+
if (typeof days !== "number" || !Number.isFinite(days)) {
|
|
81
|
+
return DEFAULT_CAPTURE_TRACE_RETENTION_DAYS;
|
|
82
|
+
}
|
|
83
|
+
return Math.max(1, Math.floor(days));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Delete trace files dated more than `captureTraceRetentionDays` before `now`.
|
|
87
|
+
* The date comes from the file name, not the modification time, and files
|
|
88
|
+
* that do not match the trace name pattern are never touched.
|
|
89
|
+
*/
|
|
90
|
+
export function pruneTraces(config, now = new Date()) {
|
|
91
|
+
const dir = getTraceDirectory();
|
|
92
|
+
let removed = 0;
|
|
93
|
+
try {
|
|
94
|
+
if (!existsSync(dir))
|
|
95
|
+
return 0;
|
|
96
|
+
const cutoff = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
97
|
+
cutoff.setDate(cutoff.getDate() - retentionDays(config));
|
|
98
|
+
for (const file of readdirSync(dir)) {
|
|
99
|
+
const match = TRACE_FILE.exec(file);
|
|
100
|
+
if (!match)
|
|
101
|
+
continue;
|
|
102
|
+
const fileDate = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
|
103
|
+
if (fileDate < cutoff) {
|
|
104
|
+
unlinkSync(join(dir, file));
|
|
105
|
+
removed++;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
log("Capture trace cleanup failed", { code: errorCode(error) });
|
|
111
|
+
}
|
|
112
|
+
return removed;
|
|
113
|
+
}
|
|
114
|
+
let lastTraceDate = null;
|
|
115
|
+
/**
|
|
116
|
+
* Append one trace entry to the day's JSON Lines file. A single append per
|
|
117
|
+
* entry keeps lines whole when Pi and OpenCode write at the same time.
|
|
118
|
+
*/
|
|
119
|
+
export function writeTraceEntry(record, diagnostics, config, now = new Date()) {
|
|
120
|
+
try {
|
|
121
|
+
const today = formatDate(now);
|
|
122
|
+
if (lastTraceDate !== today) {
|
|
123
|
+
lastTraceDate = today;
|
|
124
|
+
pruneTraces(config, now);
|
|
125
|
+
}
|
|
126
|
+
const dir = getTraceDirectory();
|
|
127
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
128
|
+
chmodSync(dir, 0o700);
|
|
129
|
+
const redact = (value) => value === undefined ? null : redactTraceText(value, config);
|
|
130
|
+
const entry = {
|
|
131
|
+
schemaVersion: TRACE_SCHEMA_VERSION,
|
|
132
|
+
timestamp: now.toISOString(),
|
|
133
|
+
...record,
|
|
134
|
+
systemPrompt: redact(diagnostics.systemPrompt),
|
|
135
|
+
userPrompt: redact(diagnostics.userPrompt),
|
|
136
|
+
reply: redact(diagnostics.rawReply),
|
|
137
|
+
};
|
|
138
|
+
const file = join(dir, `capture-${today}.jsonl`);
|
|
139
|
+
appendFileSync(file, `${JSON.stringify(entry)}\n`, { mode: 0o600 });
|
|
140
|
+
// The mode above applies only when the append creates the file; a file
|
|
141
|
+
// that already existed with wider permissions is narrowed here.
|
|
142
|
+
chmodSync(file, 0o600);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
log("Capture trace write failed", { code: errorCode(error) });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Write the attempt's metadata record to the log and, when tracing is on, its
|
|
150
|
+
* trace entry. Never throws: diagnostics must not change a capture outcome.
|
|
151
|
+
*/
|
|
152
|
+
export function emitCaptureAttempt(record, diagnostics, config, now = new Date()) {
|
|
153
|
+
try {
|
|
154
|
+
log("Capture attempt", record);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// The log is best effort here, as everywhere else.
|
|
158
|
+
}
|
|
159
|
+
if (config.captureTrace === true) {
|
|
160
|
+
writeTraceEntry(record, diagnostics, config, now);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function errorCode(error) {
|
|
164
|
+
if (error && typeof error === "object" && "code" in error && typeof error.code === "string") {
|
|
165
|
+
return error.code;
|
|
166
|
+
}
|
|
167
|
+
return error instanceof Error ? error.name : "unknown";
|
|
168
|
+
}
|
|
169
|
+
/** Test hook: forget which day the last trace was written. */
|
|
170
|
+
export function resetTraceStateForTests() {
|
|
171
|
+
lastTraceDate = null;
|
|
172
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { homedir } from "os";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
// Kept apart from logger.ts: many tests replace logger.js with a stub that
|
|
4
|
+
// only has `log`, so modules that need the paths import them from here.
|
|
5
|
+
export function getLogFilePath() {
|
|
6
|
+
return (process.env.OMMS_LOG_FILE ||
|
|
7
|
+
// Legacy override, still honoured from the opencode-mem days.
|
|
8
|
+
process.env.OPENCODE_MEM_LOG_FILE ||
|
|
9
|
+
join(homedir(), ".omms", "omms.log"));
|
|
10
|
+
}
|
|
11
|
+
export function getLogDirPath() {
|
|
12
|
+
const logFile = getLogFilePath();
|
|
13
|
+
const lastSlash = Math.max(logFile.lastIndexOf("/"), logFile.lastIndexOf("\\"));
|
|
14
|
+
return lastSlash === -1 ? "." : logFile.slice(0, lastSlash);
|
|
15
|
+
}
|
package/dist/services/logger.js
CHANGED
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
import { appendFileSync, writeFileSync, existsSync, mkdirSync, statSync, renameSync, unlinkSync, readdirSync, } from "fs";
|
|
2
|
-
import { homedir } from "os";
|
|
3
2
|
import { join } from "path";
|
|
4
|
-
|
|
5
|
-
return (process.env.OMMS_LOG_FILE ||
|
|
6
|
-
// Legacy override, still honoured from the opencode-mem days.
|
|
7
|
-
process.env.OPENCODE_MEM_LOG_FILE ||
|
|
8
|
-
join(homedir(), ".omms", "omms.log"));
|
|
9
|
-
}
|
|
10
|
-
function getLogDirPath() {
|
|
11
|
-
const logFile = getLogFilePath();
|
|
12
|
-
const lastSlash = Math.max(logFile.lastIndexOf("/"), logFile.lastIndexOf("\\"));
|
|
13
|
-
return lastSlash === -1 ? "." : logFile.slice(0, lastSlash);
|
|
14
|
-
}
|
|
3
|
+
import { getLogDirPath, getLogFilePath } from "./log-path.js";
|
|
15
4
|
const MAX_LOG_SIZE = 5 * 1024 * 1024;
|
|
16
5
|
const MAX_LOG_DAYS = 30;
|
|
17
6
|
const GLOBAL_LOGGER_KEY = Symbol.for("omms.logger.initialized");
|
package/dist/v2/legacy-client.js
CHANGED
|
@@ -222,8 +222,17 @@ export function toLegacyEvent(raw) {
|
|
|
222
222
|
const envelope = raw?.payload ?? raw;
|
|
223
223
|
const source = envelope?.type === "sync" && envelope.syncEvent ? envelope.syncEvent : envelope;
|
|
224
224
|
const rawType = typeof source?.type === "string" ? source.type.replace(/\.1$/, "") : source?.type;
|
|
225
|
-
const type = rawType === "session.compaction.ended" ? "session.compacted" : rawType;
|
|
226
225
|
const data = source?.data ?? {};
|
|
226
|
+
// OpenCode 2.0.14 ends a turn with `session.execution.succeeded` and emits no
|
|
227
|
+
// `session.idle`; some builds report idleness through `session.status`. The
|
|
228
|
+
// V1 capture path listens for `session.idle`, so both map onto it.
|
|
229
|
+
// Subscribed events carry their payload in `data`; bus-style ones in `properties`.
|
|
230
|
+
const payload = source?.data ?? source?.properties ?? {};
|
|
231
|
+
if (rawType === "session.execution.succeeded" ||
|
|
232
|
+
(rawType === "session.status" && payload?.status?.type === "idle")) {
|
|
233
|
+
return { type: "session.idle", properties: { sessionID: payload.sessionID } };
|
|
234
|
+
}
|
|
235
|
+
const type = rawType === "session.compaction.ended" ? "session.compacted" : rawType;
|
|
227
236
|
if (source && typeof source === "object" && "properties" in source) {
|
|
228
237
|
return { type, properties: source.properties };
|
|
229
238
|
}
|