@ephemushroom/opencode-claude-mem 0.5.0 → 0.6.1
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 +149 -17
- package/dist/build-tui-bridge.d.ts +1 -0
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +64 -13
- package/dist/file-context.d.ts +17 -0
- package/dist/index.js +602 -58
- package/dist/memory-options.d.ts +12 -0
- package/dist/memory-session.d.ts +22 -0
- package/dist/memory-turn.d.ts +10 -0
- package/dist/memory-worker.d.ts +21 -0
- package/dist/package-tui.d.ts +1 -1
- package/dist/package-tui.js +2 -2
- package/dist/tui.js +53 -4
- package/dist/v2.js +4327 -186
- package/dist/worker-client.d.ts +3 -0
- package/package.json +8 -5
package/dist/index.js
CHANGED
|
@@ -16,7 +16,6 @@ var __export = (target, all) => {
|
|
|
16
16
|
|
|
17
17
|
// src/shared.ts
|
|
18
18
|
var MAX_OBSERVATION_BYTES = 24 * 1024;
|
|
19
|
-
var MAX_TAG_REPLACEMENTS = 100;
|
|
20
19
|
var META_TOOLS = new Set([
|
|
21
20
|
"askuserquestion",
|
|
22
21
|
"getmcpresource",
|
|
@@ -29,26 +28,27 @@ var META_TOOLS = new Set([
|
|
|
29
28
|
"slashcommand",
|
|
30
29
|
"todowrite"
|
|
31
30
|
]);
|
|
32
|
-
var PRIVATE_TAG_REGEX = /<private>[\s\S]*?<\/private>/g;
|
|
33
|
-
var CONTEXT_TAG_REGEX = /<claude-mem-context>[\s\S]*?<\/claude-mem-context>/g;
|
|
34
31
|
function stripTaggedContent(text) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
let result = "";
|
|
33
|
+
let offset = 0;
|
|
34
|
+
const stack = [];
|
|
35
|
+
for (const match of text.matchAll(/<(\/?)(private|claude-mem-context)\b[^>]*>/gi)) {
|
|
36
|
+
if (stack.length === 0) {
|
|
37
|
+
result += text.slice(offset, match.index);
|
|
38
|
+
}
|
|
39
|
+
const name = match[2].toLowerCase();
|
|
40
|
+
if (match[1] === "/") {
|
|
41
|
+
if (stack.at(-1) === name) {
|
|
42
|
+
stack.pop();
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
stack.push(name);
|
|
46
|
+
}
|
|
47
|
+
offset = match.index + match[0].length;
|
|
44
48
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
CONTEXT_TAG_REGEX.lastIndex = 0;
|
|
48
|
-
result = result.replace(CONTEXT_TAG_REGEX, "");
|
|
49
|
-
replacements++;
|
|
49
|
+
if (stack.length === 0) {
|
|
50
|
+
result += text.slice(offset);
|
|
50
51
|
}
|
|
51
|
-
CONTEXT_TAG_REGEX.lastIndex = 0;
|
|
52
52
|
return result.trim();
|
|
53
53
|
}
|
|
54
54
|
function sanitizeObservationValue(value) {
|
|
@@ -90,7 +90,7 @@ function shouldSkipObservationTool(toolName) {
|
|
|
90
90
|
return true;
|
|
91
91
|
}
|
|
92
92
|
const normalizedName = toolName.toLowerCase().replaceAll("_", "-");
|
|
93
|
-
if (normalizedName.includes("mcp-search") || /(?:^|[-.])mem-(?:search|timeline|get-observations)$/.test(normalizedName)) {
|
|
93
|
+
if (normalizedName.includes("mcp-search") || /(?:^|[-.])mem-(?:save|search|timeline|get-observations)$/.test(normalizedName)) {
|
|
94
94
|
return true;
|
|
95
95
|
}
|
|
96
96
|
return META_TOOLS.has(normalizedName);
|
|
@@ -12436,6 +12436,44 @@ function tool(input) {
|
|
|
12436
12436
|
return input;
|
|
12437
12437
|
}
|
|
12438
12438
|
tool.schema = exports_external;
|
|
12439
|
+
// src/memory-turn.ts
|
|
12440
|
+
function memoryTurn(id, text, media = false) {
|
|
12441
|
+
const cleaned = stripTaggedContent(text.replace(/<(system-reminder|system_instruction|system-instruction|persisted-output)\b[^>]*>[\s\S]*?<\/\1>/gi, ""));
|
|
12442
|
+
const internal = /^\s*<task-notification\b[^>]*>[\s\S]*?<\/task-notification>\s*$/i.test(text);
|
|
12443
|
+
return {
|
|
12444
|
+
id,
|
|
12445
|
+
text: truncateUtf8Bytes(cleaned || (media ? "[media prompt]" : ""), 256 * 1024),
|
|
12446
|
+
capture: !internal && (Boolean(cleaned) || media && !text.trim()),
|
|
12447
|
+
semantic: !internal && cleaned.length >= 20
|
|
12448
|
+
};
|
|
12449
|
+
}
|
|
12450
|
+
var deliveredRecord = object({
|
|
12451
|
+
id: string2(),
|
|
12452
|
+
type: string2(),
|
|
12453
|
+
text: string2().optional(),
|
|
12454
|
+
files: array(unknown()).optional(),
|
|
12455
|
+
metadata: object({ synthetic: boolean2().optional(), agentId: string2().optional() }).optional()
|
|
12456
|
+
});
|
|
12457
|
+
var v1Record = object({
|
|
12458
|
+
info: object({ id: string2(), role: string2() }),
|
|
12459
|
+
parts: array(unknown())
|
|
12460
|
+
});
|
|
12461
|
+
function resumedV1Turn(messages) {
|
|
12462
|
+
if (!Array.isArray(messages)) {
|
|
12463
|
+
return;
|
|
12464
|
+
}
|
|
12465
|
+
for (const raw of messages.toReversed()) {
|
|
12466
|
+
const parsed = v1Record.safeParse(raw);
|
|
12467
|
+
if (parsed.success && parsed.data.info.role === "user") {
|
|
12468
|
+
return memoryTurn(parsed.data.info.id, extractTextFromParts(parsed.data.parts), parsed.data.parts.some((part) => typeof part === "object" && part !== null && ("type" in part) && part.type === "file"));
|
|
12469
|
+
}
|
|
12470
|
+
}
|
|
12471
|
+
return;
|
|
12472
|
+
}
|
|
12473
|
+
|
|
12474
|
+
// src/file-context.ts
|
|
12475
|
+
import { relative, resolve } from "path";
|
|
12476
|
+
|
|
12439
12477
|
// src/worker-client.ts
|
|
12440
12478
|
import { existsSync, readFileSync } from "fs";
|
|
12441
12479
|
import { homedir } from "os";
|
|
@@ -12636,7 +12674,16 @@ class WorkerClient {
|
|
|
12636
12674
|
if (!response.ok) {
|
|
12637
12675
|
return null;
|
|
12638
12676
|
}
|
|
12639
|
-
|
|
12677
|
+
const value = await response.json();
|
|
12678
|
+
if (!isRecord(value) || typeof value.sessionDbId !== "number" || typeof value.promptNumber !== "number" || !Number.isFinite(value.sessionDbId) || !Number.isFinite(value.promptNumber)) {
|
|
12679
|
+
return null;
|
|
12680
|
+
}
|
|
12681
|
+
return {
|
|
12682
|
+
sessionDbId: value.sessionDbId,
|
|
12683
|
+
promptNumber: value.promptNumber,
|
|
12684
|
+
skipped: value.skipped === true,
|
|
12685
|
+
reason: typeof value.reason === "string" ? value.reason : undefined
|
|
12686
|
+
};
|
|
12640
12687
|
} catch {
|
|
12641
12688
|
return null;
|
|
12642
12689
|
}
|
|
@@ -12670,8 +12717,8 @@ class WorkerClient {
|
|
|
12670
12717
|
headers: { "Content-Type": "application/json" },
|
|
12671
12718
|
body: JSON.stringify({
|
|
12672
12719
|
contentSessionId,
|
|
12673
|
-
last_user_message: lastUserMessage,
|
|
12674
|
-
last_assistant_message: lastAssistantMessage,
|
|
12720
|
+
last_user_message: stripTaggedContent(lastUserMessage),
|
|
12721
|
+
last_assistant_message: stripTaggedContent(lastAssistantMessage),
|
|
12675
12722
|
platformSource: PLATFORM_SOURCE,
|
|
12676
12723
|
observedModel: metadata.observedModel,
|
|
12677
12724
|
observedBilling: metadata.observedBilling
|
|
@@ -12684,7 +12731,7 @@ class WorkerClient {
|
|
|
12684
12731
|
}
|
|
12685
12732
|
static async getContext(project) {
|
|
12686
12733
|
try {
|
|
12687
|
-
const response = await fetch(`${this.BASE_URL}/api/context/inject?project=${encodeURIComponent(project)}
|
|
12734
|
+
const response = await fetch(`${this.BASE_URL}/api/context/inject?project=${encodeURIComponent(project)}`, { signal: AbortSignal.timeout(2000) });
|
|
12688
12735
|
if (!response.ok) {
|
|
12689
12736
|
return null;
|
|
12690
12737
|
}
|
|
@@ -12835,6 +12882,376 @@ class WorkerClient {
|
|
|
12835
12882
|
}
|
|
12836
12883
|
}
|
|
12837
12884
|
|
|
12885
|
+
// src/memory-worker.ts
|
|
12886
|
+
var saveInput = object({
|
|
12887
|
+
text: string2().min(1).max(MAX_OBSERVATION_BYTES),
|
|
12888
|
+
title: string2().max(MAX_OBSERVATION_BYTES).optional(),
|
|
12889
|
+
project: string2().trim().min(1).max(1000).optional()
|
|
12890
|
+
});
|
|
12891
|
+
var saveResponse = object({
|
|
12892
|
+
success: literal(true),
|
|
12893
|
+
id: number2().int().positive(),
|
|
12894
|
+
title: string2(),
|
|
12895
|
+
project: string2(),
|
|
12896
|
+
message: string2()
|
|
12897
|
+
});
|
|
12898
|
+
var semanticResponse = object({ context: string2(), count: number2().int().nonnegative() });
|
|
12899
|
+
var fileResponse = object({
|
|
12900
|
+
observations: array(object({
|
|
12901
|
+
id: number2().int().positive(),
|
|
12902
|
+
title: string2().nullable(),
|
|
12903
|
+
type: string2(),
|
|
12904
|
+
created_at_epoch: number2().finite(),
|
|
12905
|
+
memory_session_id: string2().nullable().optional()
|
|
12906
|
+
})),
|
|
12907
|
+
count: number2().int().nonnegative()
|
|
12908
|
+
});
|
|
12909
|
+
|
|
12910
|
+
class MemoryWorker {
|
|
12911
|
+
static baseUrl = getWorkerBaseUrl();
|
|
12912
|
+
static async json(path, init) {
|
|
12913
|
+
try {
|
|
12914
|
+
const response = await fetch(`${this.baseUrl}${path}`, init);
|
|
12915
|
+
if (!response.ok) {
|
|
12916
|
+
return null;
|
|
12917
|
+
}
|
|
12918
|
+
const reader = response.body?.getReader();
|
|
12919
|
+
if (!reader) {
|
|
12920
|
+
return null;
|
|
12921
|
+
}
|
|
12922
|
+
const chunks = [];
|
|
12923
|
+
let size = 0;
|
|
12924
|
+
try {
|
|
12925
|
+
while (true) {
|
|
12926
|
+
const { done, value } = await reader.read();
|
|
12927
|
+
if (done) {
|
|
12928
|
+
break;
|
|
12929
|
+
}
|
|
12930
|
+
size += value.byteLength;
|
|
12931
|
+
if (size > 512 * 1024) {
|
|
12932
|
+
return null;
|
|
12933
|
+
}
|
|
12934
|
+
chunks.push(value);
|
|
12935
|
+
}
|
|
12936
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
12937
|
+
} finally {
|
|
12938
|
+
await reader.cancel();
|
|
12939
|
+
}
|
|
12940
|
+
} catch {
|
|
12941
|
+
return null;
|
|
12942
|
+
}
|
|
12943
|
+
}
|
|
12944
|
+
static async save(input, project) {
|
|
12945
|
+
const parsed = saveInput.safeParse(input);
|
|
12946
|
+
if (!parsed.success) {
|
|
12947
|
+
return "Memory not saved: invalid or oversized input.";
|
|
12948
|
+
}
|
|
12949
|
+
const text = stripTaggedContent(parsed.data.text);
|
|
12950
|
+
const title = parsed.data.title === undefined ? undefined : stripTaggedContent(parsed.data.title);
|
|
12951
|
+
if (!text || title !== undefined && !title || Buffer.byteLength(text) > MAX_OBSERVATION_BYTES || title !== undefined && Buffer.byteLength(title) > MAX_OBSERVATION_BYTES) {
|
|
12952
|
+
return "Memory not saved: content is empty, private, or exceeds 24 KiB.";
|
|
12953
|
+
}
|
|
12954
|
+
const result = saveResponse.safeParse(await this.json("/api/memory/save", {
|
|
12955
|
+
method: "POST",
|
|
12956
|
+
signal: AbortSignal.timeout(1e4),
|
|
12957
|
+
headers: { "Content-Type": "application/json" },
|
|
12958
|
+
body: JSON.stringify({
|
|
12959
|
+
text,
|
|
12960
|
+
title,
|
|
12961
|
+
project: parsed.data.project ?? project,
|
|
12962
|
+
metadata: { platformSource: "opencode" }
|
|
12963
|
+
})
|
|
12964
|
+
}));
|
|
12965
|
+
return result.success ? JSON.stringify(result.data) : "Memory save not confirmed. The Worker may be unavailable or unsupported; check before retrying to avoid duplicates.";
|
|
12966
|
+
}
|
|
12967
|
+
static async semantic(prompt, project, options) {
|
|
12968
|
+
const q = stripTaggedContent(prompt);
|
|
12969
|
+
if (!options.enabled || q.length < 20) {
|
|
12970
|
+
return "";
|
|
12971
|
+
}
|
|
12972
|
+
const result = semanticResponse.safeParse(await this.json("/api/context/semantic", {
|
|
12973
|
+
method: "POST",
|
|
12974
|
+
signal: AbortSignal.timeout(options.timeoutMs),
|
|
12975
|
+
headers: { "Content-Type": "application/json" },
|
|
12976
|
+
body: JSON.stringify({ q, project, limit: options.limit })
|
|
12977
|
+
}));
|
|
12978
|
+
return result.success ? stripTaggedContent(result.data.context).slice(0, options.maxChars) : "";
|
|
12979
|
+
}
|
|
12980
|
+
static async byFile(paths, project, options) {
|
|
12981
|
+
const lookahead = Math.max(40, options.limit);
|
|
12982
|
+
const params = new URLSearchParams({ projects: project, limit: String(lookahead) });
|
|
12983
|
+
for (const path of new Set(paths.map((item) => item.replaceAll("\\", "/")))) {
|
|
12984
|
+
params.append("path", path);
|
|
12985
|
+
}
|
|
12986
|
+
const result = fileResponse.safeParse(await this.json(`/api/observations/by-file?${params}`, {
|
|
12987
|
+
signal: AbortSignal.timeout(options.timeoutMs)
|
|
12988
|
+
}));
|
|
12989
|
+
return result.success ? result.data.observations.slice(0, lookahead) : [];
|
|
12990
|
+
}
|
|
12991
|
+
}
|
|
12992
|
+
|
|
12993
|
+
// src/file-context.ts
|
|
12994
|
+
import { stat } from "fs/promises";
|
|
12995
|
+
var readInput = object({
|
|
12996
|
+
filePath: string2().optional(),
|
|
12997
|
+
file_path: string2().optional(),
|
|
12998
|
+
filePaths: array(string2()).optional(),
|
|
12999
|
+
file_paths: array(string2()).optional(),
|
|
13000
|
+
path: string2().optional()
|
|
13001
|
+
}).passthrough();
|
|
13002
|
+
|
|
13003
|
+
class FileContext {
|
|
13004
|
+
sessions = new Map;
|
|
13005
|
+
options;
|
|
13006
|
+
worker;
|
|
13007
|
+
constructor(options, worker = MemoryWorker) {
|
|
13008
|
+
this.options = options;
|
|
13009
|
+
this.worker = worker;
|
|
13010
|
+
}
|
|
13011
|
+
async read(session, input, location) {
|
|
13012
|
+
if (!this.options.enabled || !/(?:^|[._-])read(?:_file|_files)?$/i.test(input.tool)) {
|
|
13013
|
+
return "";
|
|
13014
|
+
}
|
|
13015
|
+
const parsed = readInput.safeParse(input.args);
|
|
13016
|
+
if (!parsed.success) {
|
|
13017
|
+
return "";
|
|
13018
|
+
}
|
|
13019
|
+
const args = parsed.data;
|
|
13020
|
+
const paths = [
|
|
13021
|
+
...new Set([
|
|
13022
|
+
args.filePath,
|
|
13023
|
+
args.file_path,
|
|
13024
|
+
args.path,
|
|
13025
|
+
...args.filePaths ?? [],
|
|
13026
|
+
...args.file_paths ?? []
|
|
13027
|
+
].filter((path) => path !== undefined))
|
|
13028
|
+
].slice(0, 10);
|
|
13029
|
+
let seen = this.sessions.get(session);
|
|
13030
|
+
if (!seen) {
|
|
13031
|
+
seen = new Map;
|
|
13032
|
+
this.sessions.set(session, seen);
|
|
13033
|
+
}
|
|
13034
|
+
const revisions = seen;
|
|
13035
|
+
const attempted = new Map;
|
|
13036
|
+
const deadline = Date.now() + this.options.timeoutMs;
|
|
13037
|
+
let active = true;
|
|
13038
|
+
const timeout = Promise.withResolvers();
|
|
13039
|
+
const timer = setTimeout(() => timeout.resolve(""), this.options.timeoutMs);
|
|
13040
|
+
const pending = (async () => {
|
|
13041
|
+
const results = await Promise.all(paths.map(async (path) => {
|
|
13042
|
+
const absolute = resolve(location.directory, path);
|
|
13043
|
+
const file = await stat(absolute).catch(() => null);
|
|
13044
|
+
if (!active || this.sessions.get(session) !== revisions || !file?.isFile() || file.size < 1500) {
|
|
13045
|
+
return [];
|
|
13046
|
+
}
|
|
13047
|
+
const revision = `${file.mtimeMs}:${file.size}`;
|
|
13048
|
+
const inFlight = `pending:${revision}`;
|
|
13049
|
+
if (revisions.get(absolute) === revision || revisions.get(absolute) === inFlight) {
|
|
13050
|
+
return [];
|
|
13051
|
+
}
|
|
13052
|
+
attempted.set(absolute, inFlight);
|
|
13053
|
+
revisions.set(absolute, inFlight);
|
|
13054
|
+
const rows = await this.worker.byFile([absolute, relative(location.directory, absolute)], location.project, { ...this.options, timeoutMs: Math.max(1, deadline - Date.now()) });
|
|
13055
|
+
if (!active || this.sessions.get(session) !== revisions || !rows.length || file.mtimeMs >= Math.max(...rows.map((row) => row.created_at_epoch))) {
|
|
13056
|
+
return [];
|
|
13057
|
+
}
|
|
13058
|
+
return rows.map((row) => ({ row, path: absolute, revision }));
|
|
13059
|
+
}));
|
|
13060
|
+
const sessions = new Set;
|
|
13061
|
+
const selected = results.flat().toSorted((a, b) => b.row.created_at_epoch - a.row.created_at_epoch).filter(({ row }) => {
|
|
13062
|
+
const key = row.memory_session_id ?? `observation:${row.id}`;
|
|
13063
|
+
if (sessions.has(key)) {
|
|
13064
|
+
return false;
|
|
13065
|
+
}
|
|
13066
|
+
sessions.add(key);
|
|
13067
|
+
return true;
|
|
13068
|
+
}).slice(0, this.options.limit);
|
|
13069
|
+
const delivered = new Map;
|
|
13070
|
+
let text = "";
|
|
13071
|
+
for (const { row, path, revision } of selected) {
|
|
13072
|
+
const separator = text ? `
|
|
13073
|
+
` : "";
|
|
13074
|
+
const available = this.options.maxChars - text.length - separator.length;
|
|
13075
|
+
if (available <= 0) {
|
|
13076
|
+
break;
|
|
13077
|
+
}
|
|
13078
|
+
const line = `#${row.id} ${stripTaggedContent(row.type)}: ${stripTaggedContent(row.title ?? "")}`;
|
|
13079
|
+
text += separator + line.slice(0, available);
|
|
13080
|
+
delivered.set(path, revision);
|
|
13081
|
+
}
|
|
13082
|
+
if (text && active && this.sessions.get(session) === revisions) {
|
|
13083
|
+
for (const [path, revision] of delivered) {
|
|
13084
|
+
revisions.set(path, revision);
|
|
13085
|
+
}
|
|
13086
|
+
}
|
|
13087
|
+
return text ? `
|
|
13088
|
+
|
|
13089
|
+
<claude-mem-context>
|
|
13090
|
+
Supplementary file history (past observations, not file contents):
|
|
13091
|
+
${text}
|
|
13092
|
+
</claude-mem-context>` : "";
|
|
13093
|
+
})();
|
|
13094
|
+
try {
|
|
13095
|
+
return await Promise.race([pending, timeout.promise]);
|
|
13096
|
+
} catch {
|
|
13097
|
+
return "";
|
|
13098
|
+
} finally {
|
|
13099
|
+
active = false;
|
|
13100
|
+
clearTimeout(timer);
|
|
13101
|
+
for (const [path, revision] of attempted) {
|
|
13102
|
+
if (revisions.get(path) === revision) {
|
|
13103
|
+
revisions.delete(path);
|
|
13104
|
+
}
|
|
13105
|
+
}
|
|
13106
|
+
}
|
|
13107
|
+
}
|
|
13108
|
+
delete(session) {
|
|
13109
|
+
this.sessions.delete(session);
|
|
13110
|
+
}
|
|
13111
|
+
clear() {
|
|
13112
|
+
this.sessions.clear();
|
|
13113
|
+
}
|
|
13114
|
+
}
|
|
13115
|
+
|
|
13116
|
+
// src/memory-session.ts
|
|
13117
|
+
class MemorySessions {
|
|
13118
|
+
sessions = new Map;
|
|
13119
|
+
deleted = new Set;
|
|
13120
|
+
options;
|
|
13121
|
+
worker;
|
|
13122
|
+
constructor(options, worker = MemoryWorker) {
|
|
13123
|
+
this.options = options;
|
|
13124
|
+
this.worker = worker;
|
|
13125
|
+
}
|
|
13126
|
+
state(id) {
|
|
13127
|
+
let state = this.sessions.get(id);
|
|
13128
|
+
if (!state) {
|
|
13129
|
+
state = {
|
|
13130
|
+
turns: new Map,
|
|
13131
|
+
registered: new Set,
|
|
13132
|
+
blocked: new Set,
|
|
13133
|
+
pending: [],
|
|
13134
|
+
semantic: new Map
|
|
13135
|
+
};
|
|
13136
|
+
this.sessions.set(id, state);
|
|
13137
|
+
}
|
|
13138
|
+
return state;
|
|
13139
|
+
}
|
|
13140
|
+
select(session, turn) {
|
|
13141
|
+
if (this.isDeleted(session)) {
|
|
13142
|
+
return false;
|
|
13143
|
+
}
|
|
13144
|
+
const state = this.state(session);
|
|
13145
|
+
const known = state.turns.get(turn.id);
|
|
13146
|
+
if (known && state.current !== known) {
|
|
13147
|
+
return false;
|
|
13148
|
+
}
|
|
13149
|
+
state.current = known ?? turn;
|
|
13150
|
+
if (!known) {
|
|
13151
|
+
state.turns.set(turn.id, turn);
|
|
13152
|
+
if (turn.capture) {
|
|
13153
|
+
state.pending.push(turn);
|
|
13154
|
+
}
|
|
13155
|
+
}
|
|
13156
|
+
return !known;
|
|
13157
|
+
}
|
|
13158
|
+
hasTurn(session) {
|
|
13159
|
+
return Boolean(this.sessions.get(session)?.current);
|
|
13160
|
+
}
|
|
13161
|
+
hasMessage(session, id) {
|
|
13162
|
+
return this.sessions.get(session)?.turns.has(id) ?? false;
|
|
13163
|
+
}
|
|
13164
|
+
isDeleted(session) {
|
|
13165
|
+
return this.deleted.has(session);
|
|
13166
|
+
}
|
|
13167
|
+
canCapture(session) {
|
|
13168
|
+
const state = this.sessions.get(session);
|
|
13169
|
+
const turn = state?.current;
|
|
13170
|
+
return Boolean(turn?.capture && state?.registered.has(turn.id) && !state.blocked.has(turn.id));
|
|
13171
|
+
}
|
|
13172
|
+
async register(session, project) {
|
|
13173
|
+
const state = this.sessions.get(session);
|
|
13174
|
+
if (!state?.current?.capture) {
|
|
13175
|
+
return false;
|
|
13176
|
+
}
|
|
13177
|
+
if (state.registration) {
|
|
13178
|
+
return state.registration;
|
|
13179
|
+
}
|
|
13180
|
+
const pending = (async () => {
|
|
13181
|
+
while (state.pending.length) {
|
|
13182
|
+
const [turn] = state.pending;
|
|
13183
|
+
if (!turn || this.sessions.get(session) !== state) {
|
|
13184
|
+
return false;
|
|
13185
|
+
}
|
|
13186
|
+
const result = await WorkerClient.sessionInit(session, project, turn.text);
|
|
13187
|
+
if (!result || this.sessions.get(session) !== state) {
|
|
13188
|
+
return false;
|
|
13189
|
+
}
|
|
13190
|
+
if (result.skipped && result.reason !== "duplicate") {
|
|
13191
|
+
state.blocked.add(turn.id);
|
|
13192
|
+
}
|
|
13193
|
+
state.registered.add(turn.id);
|
|
13194
|
+
state.pending.shift();
|
|
13195
|
+
}
|
|
13196
|
+
return this.canCapture(session);
|
|
13197
|
+
})();
|
|
13198
|
+
state.registration = pending;
|
|
13199
|
+
try {
|
|
13200
|
+
return await pending;
|
|
13201
|
+
} finally {
|
|
13202
|
+
state.registration = undefined;
|
|
13203
|
+
}
|
|
13204
|
+
}
|
|
13205
|
+
async semantic(session, project) {
|
|
13206
|
+
const state = this.sessions.get(session);
|
|
13207
|
+
const turn = state?.current;
|
|
13208
|
+
if (!state || !turn?.semantic || !this.canCapture(session)) {
|
|
13209
|
+
return "";
|
|
13210
|
+
}
|
|
13211
|
+
let pending = state.semantic.get(turn.id);
|
|
13212
|
+
if (!pending) {
|
|
13213
|
+
pending = this.worker.semantic(turn.text, project, this.options.semanticInjection);
|
|
13214
|
+
state.semantic.set(turn.id, pending);
|
|
13215
|
+
}
|
|
13216
|
+
const result = await pending;
|
|
13217
|
+
return this.sessions.get(session) === state && state.current === turn ? result : "";
|
|
13218
|
+
}
|
|
13219
|
+
async context(session, project) {
|
|
13220
|
+
if (this.isDeleted(session)) {
|
|
13221
|
+
return null;
|
|
13222
|
+
}
|
|
13223
|
+
const state = this.state(session);
|
|
13224
|
+
if (!state.context) {
|
|
13225
|
+
state.context = WorkerClient.getContext(project);
|
|
13226
|
+
}
|
|
13227
|
+
const pending = state.context;
|
|
13228
|
+
const value = await pending;
|
|
13229
|
+
if (this.sessions.get(session) !== state || state.context !== pending) {
|
|
13230
|
+
return null;
|
|
13231
|
+
}
|
|
13232
|
+
if (value === null) {
|
|
13233
|
+
state.context = undefined;
|
|
13234
|
+
}
|
|
13235
|
+
return value === null ? null : stripTaggedContent(value).slice(0, 24000);
|
|
13236
|
+
}
|
|
13237
|
+
invalidate(session) {
|
|
13238
|
+
const state = this.sessions.get(session);
|
|
13239
|
+
if (state) {
|
|
13240
|
+
state.context = undefined;
|
|
13241
|
+
}
|
|
13242
|
+
}
|
|
13243
|
+
delete(session) {
|
|
13244
|
+
this.deleted.add(session);
|
|
13245
|
+
this.sessions.delete(session);
|
|
13246
|
+
}
|
|
13247
|
+
clear() {
|
|
13248
|
+
for (const session of this.sessions.keys()) {
|
|
13249
|
+
this.deleted.add(session);
|
|
13250
|
+
}
|
|
13251
|
+
this.sessions.clear();
|
|
13252
|
+
}
|
|
13253
|
+
}
|
|
13254
|
+
|
|
12838
13255
|
// src/tui-registration.ts
|
|
12839
13256
|
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
12840
13257
|
import { homedir as homedir2 } from "os";
|
|
@@ -12904,10 +13321,73 @@ function ensureTuiPluginEntry() {
|
|
|
12904
13321
|
}
|
|
12905
13322
|
}
|
|
12906
13323
|
|
|
13324
|
+
// src/memory-options.ts
|
|
13325
|
+
import { homedir as homedir3 } from "os";
|
|
13326
|
+
import { join as join3 } from "path";
|
|
13327
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
13328
|
+
var group = object({
|
|
13329
|
+
enabled: boolean2().optional().catch(undefined),
|
|
13330
|
+
limit: number2().int().min(1).max(50).optional().catch(undefined),
|
|
13331
|
+
maxChars: number2().int().min(256).max(24000).optional().catch(undefined),
|
|
13332
|
+
timeoutMs: number2().int().min(100).max(1e4).optional().catch(undefined)
|
|
13333
|
+
}).catch({});
|
|
13334
|
+
var optionsSchema = object({
|
|
13335
|
+
semanticInjection: group.optional(),
|
|
13336
|
+
fileContext: group.optional()
|
|
13337
|
+
}).catch({});
|
|
13338
|
+
var settingsSchema = object({
|
|
13339
|
+
CLAUDE_MEM_SEMANTIC_INJECT: unknown().optional(),
|
|
13340
|
+
CLAUDE_MEM_SEMANTIC_INJECT_LIMIT: unknown().optional()
|
|
13341
|
+
}).catch({});
|
|
13342
|
+
function booleanSetting(value) {
|
|
13343
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase() : value;
|
|
13344
|
+
if (normalized === true || normalized === "true" || normalized === "1") {
|
|
13345
|
+
return true;
|
|
13346
|
+
}
|
|
13347
|
+
if (normalized === false || normalized === "false" || normalized === "0") {
|
|
13348
|
+
return false;
|
|
13349
|
+
}
|
|
13350
|
+
return;
|
|
13351
|
+
}
|
|
13352
|
+
function limitSetting(value) {
|
|
13353
|
+
const parsed = exports_coerce.number().int().min(1).max(50).safeParse(value);
|
|
13354
|
+
return parsed.success ? parsed.data : undefined;
|
|
13355
|
+
}
|
|
13356
|
+
function parseMemoryOptions(options, env, settings) {
|
|
13357
|
+
const explicit = optionsSchema.parse(options);
|
|
13358
|
+
const stored = settingsSchema.parse(settings);
|
|
13359
|
+
return {
|
|
13360
|
+
semanticInjection: {
|
|
13361
|
+
enabled: explicit.semanticInjection?.enabled ?? booleanSetting(env.CLAUDE_MEM_SEMANTIC_INJECT) ?? booleanSetting(stored.CLAUDE_MEM_SEMANTIC_INJECT) ?? false,
|
|
13362
|
+
limit: explicit.semanticInjection?.limit ?? limitSetting(env.CLAUDE_MEM_SEMANTIC_INJECT_LIMIT) ?? limitSetting(stored.CLAUDE_MEM_SEMANTIC_INJECT_LIMIT) ?? 5,
|
|
13363
|
+
maxChars: explicit.semanticInjection?.maxChars ?? 6000,
|
|
13364
|
+
timeoutMs: explicit.semanticInjection?.timeoutMs ?? 2000
|
|
13365
|
+
},
|
|
13366
|
+
fileContext: {
|
|
13367
|
+
enabled: explicit.fileContext?.enabled ?? true,
|
|
13368
|
+
limit: explicit.fileContext?.limit ?? 15,
|
|
13369
|
+
maxChars: explicit.fileContext?.maxChars ?? 6000,
|
|
13370
|
+
timeoutMs: explicit.fileContext?.timeoutMs ?? 1500
|
|
13371
|
+
}
|
|
13372
|
+
};
|
|
13373
|
+
}
|
|
13374
|
+
function resolveMemoryOptions(options) {
|
|
13375
|
+
let settings = {};
|
|
13376
|
+
try {
|
|
13377
|
+
settings = JSON.parse(readFileSync3(join3(homedir3(), ".claude-mem", "settings.json"), "utf8"));
|
|
13378
|
+
} catch {}
|
|
13379
|
+
return parseMemoryOptions(options, process.env, settings);
|
|
13380
|
+
}
|
|
13381
|
+
|
|
12907
13382
|
// src/index.ts
|
|
12908
13383
|
var ASSISTANT_FLUSH_DEBOUNCE_MS = 250;
|
|
12909
|
-
var ClaudeMemPlugin = async (ctx) => {
|
|
13384
|
+
var ClaudeMemPlugin = async (ctx, options) => {
|
|
12910
13385
|
const { project, directory, client } = ctx;
|
|
13386
|
+
const config = resolveMemoryOptions(options);
|
|
13387
|
+
const memory = new MemorySessions(config);
|
|
13388
|
+
const files = new FileContext(config.fileContext);
|
|
13389
|
+
const resumed = new Map;
|
|
13390
|
+
const subagentSessions = new Set;
|
|
12911
13391
|
ensureTuiPluginEntry();
|
|
12912
13392
|
const projectRoot = directory || process.cwd();
|
|
12913
13393
|
const projectName = project?.worktree ? project.worktree.split(/[\\/]/).findLast(Boolean) || "unknown-project" : "unknown-project";
|
|
@@ -12918,17 +13398,6 @@ var ClaudeMemPlugin = async (ctx) => {
|
|
|
12918
13398
|
});
|
|
12919
13399
|
} catch {}
|
|
12920
13400
|
}
|
|
12921
|
-
let contextCache = undefined;
|
|
12922
|
-
async function getCachedContext() {
|
|
12923
|
-
if (contextCache !== undefined) {
|
|
12924
|
-
return contextCache;
|
|
12925
|
-
}
|
|
12926
|
-
const context = await WorkerClient.getContext(projectName);
|
|
12927
|
-
if (context !== null) {
|
|
12928
|
-
contextCache = context;
|
|
12929
|
-
}
|
|
12930
|
-
return context;
|
|
12931
|
-
}
|
|
12932
13401
|
let workerHealthy = null;
|
|
12933
13402
|
let initToastShown = false;
|
|
12934
13403
|
async function checkWorkerAndToast() {
|
|
@@ -12941,9 +13410,17 @@ var ClaudeMemPlugin = async (ctx) => {
|
|
|
12941
13410
|
return workerHealthy;
|
|
12942
13411
|
}
|
|
12943
13412
|
let currentSessionId = null;
|
|
12944
|
-
const initializedSessions = new Set;
|
|
12945
13413
|
const assistantBuffers = new Map;
|
|
13414
|
+
const assistantFlushes = new Map;
|
|
12946
13415
|
async function flushAssistantBuffer(sessionId) {
|
|
13416
|
+
const existing = assistantFlushes.get(sessionId);
|
|
13417
|
+
if (existing) {
|
|
13418
|
+
await existing;
|
|
13419
|
+
}
|
|
13420
|
+
if (!memory.canCapture(sessionId)) {
|
|
13421
|
+
discardAssistantBuffer(sessionId);
|
|
13422
|
+
return;
|
|
13423
|
+
}
|
|
12947
13424
|
const buf = assistantBuffers.get(sessionId);
|
|
12948
13425
|
if (!buf || !buf.messageId) {
|
|
12949
13426
|
return;
|
|
@@ -12954,13 +13431,23 @@ var ClaudeMemPlugin = async (ctx) => {
|
|
|
12954
13431
|
}
|
|
12955
13432
|
const { messageId } = buf;
|
|
12956
13433
|
buf.messageId = null;
|
|
13434
|
+
const pending = (async () => {
|
|
13435
|
+
try {
|
|
13436
|
+
const text = await fetchAssistantMessageText(sessionId, messageId);
|
|
13437
|
+
const sanitized = truncateUtf8Bytes(stripTaggedContent(text), MAX_OBSERVATION_BYTES);
|
|
13438
|
+
if (sanitized) {
|
|
13439
|
+
await WorkerClient.sendObservation(sessionId, "assistant_message", { messageId }, sanitized, projectRoot);
|
|
13440
|
+
}
|
|
13441
|
+
} catch {}
|
|
13442
|
+
})();
|
|
13443
|
+
assistantFlushes.set(sessionId, pending);
|
|
12957
13444
|
try {
|
|
12958
|
-
|
|
12959
|
-
|
|
12960
|
-
if (
|
|
12961
|
-
|
|
13445
|
+
await pending;
|
|
13446
|
+
} finally {
|
|
13447
|
+
if (assistantFlushes.get(sessionId) === pending) {
|
|
13448
|
+
assistantFlushes.delete(sessionId);
|
|
12962
13449
|
}
|
|
12963
|
-
}
|
|
13450
|
+
}
|
|
12964
13451
|
}
|
|
12965
13452
|
function scheduleAssistantFlush(sessionId) {
|
|
12966
13453
|
const buf = assistantBuffers.get(sessionId);
|
|
@@ -12982,23 +13469,34 @@ var ClaudeMemPlugin = async (ctx) => {
|
|
|
12982
13469
|
}
|
|
12983
13470
|
assistantBuffers.delete(sessionId);
|
|
12984
13471
|
}
|
|
12985
|
-
async function ensureSessionInit(sessionId
|
|
12986
|
-
if (
|
|
12987
|
-
return true;
|
|
12988
|
-
}
|
|
12989
|
-
const isHealthy = await checkWorkerAndToast();
|
|
12990
|
-
if (!isHealthy) {
|
|
13472
|
+
async function ensureSessionInit(sessionId) {
|
|
13473
|
+
if (memory.isDeleted(sessionId)) {
|
|
12991
13474
|
return false;
|
|
12992
13475
|
}
|
|
12993
13476
|
try {
|
|
12994
|
-
|
|
12995
|
-
|
|
13477
|
+
if (!memory.hasTurn(sessionId)) {
|
|
13478
|
+
let pending = resumed.get(sessionId);
|
|
13479
|
+
if (!pending) {
|
|
13480
|
+
pending = (async () => {
|
|
13481
|
+
const result = await client.session.messages({ path: { id: sessionId } });
|
|
13482
|
+
const turn = resumedV1Turn(result.data);
|
|
13483
|
+
if (turn && !memory.hasTurn(sessionId) && resumed.has(sessionId)) {
|
|
13484
|
+
memory.select(sessionId, turn);
|
|
13485
|
+
}
|
|
13486
|
+
})();
|
|
13487
|
+
resumed.set(sessionId, pending);
|
|
13488
|
+
}
|
|
13489
|
+
await pending;
|
|
13490
|
+
if (!memory.hasTurn(sessionId)) {
|
|
13491
|
+
resumed.delete(sessionId);
|
|
13492
|
+
}
|
|
13493
|
+
}
|
|
13494
|
+
if (!memory.hasTurn(sessionId) || !await checkWorkerAndToast()) {
|
|
12996
13495
|
return false;
|
|
12997
13496
|
}
|
|
12998
|
-
|
|
12999
|
-
currentSessionId = sessionId;
|
|
13000
|
-
return true;
|
|
13497
|
+
return await memory.register(sessionId, projectName);
|
|
13001
13498
|
} catch {
|
|
13499
|
+
resumed.delete(sessionId);
|
|
13002
13500
|
return false;
|
|
13003
13501
|
}
|
|
13004
13502
|
}
|
|
@@ -13042,6 +13540,15 @@ var ClaudeMemPlugin = async (ctx) => {
|
|
|
13042
13540
|
}
|
|
13043
13541
|
return {
|
|
13044
13542
|
tool: {
|
|
13543
|
+
"mem-save": tool({
|
|
13544
|
+
description: "Save an explicit memory to Claude-Mem. Private and injected context are removed. Check an uncertain result before retrying.",
|
|
13545
|
+
args: {
|
|
13546
|
+
text: tool.schema.string().min(1).max(MAX_OBSERVATION_BYTES),
|
|
13547
|
+
title: tool.schema.string().max(MAX_OBSERVATION_BYTES).optional(),
|
|
13548
|
+
project: tool.schema.string().min(1).optional()
|
|
13549
|
+
},
|
|
13550
|
+
execute: async (input) => MemoryWorker.save(input, projectName)
|
|
13551
|
+
}),
|
|
13045
13552
|
"mem-search": tool({
|
|
13046
13553
|
description: "Search Claude-Mem persistent memory. Supports query, project, platformSource, type, obs_type, dateStart, dateEnd, offset, and orderBy filters.",
|
|
13047
13554
|
args: {
|
|
@@ -13136,7 +13643,10 @@ var ClaudeMemPlugin = async (ctx) => {
|
|
|
13136
13643
|
return;
|
|
13137
13644
|
}
|
|
13138
13645
|
currentSessionId = sessionId;
|
|
13139
|
-
|
|
13646
|
+
if (event.properties?.info?.parentID) {
|
|
13647
|
+
subagentSessions.add(sessionId);
|
|
13648
|
+
}
|
|
13649
|
+
memory.invalidate(sessionId);
|
|
13140
13650
|
await checkWorkerAndToast();
|
|
13141
13651
|
return;
|
|
13142
13652
|
}
|
|
@@ -13186,7 +13696,11 @@ var ClaudeMemPlugin = async (ctx) => {
|
|
|
13186
13696
|
if (!sessionId) {
|
|
13187
13697
|
return;
|
|
13188
13698
|
}
|
|
13699
|
+
memory.invalidate(sessionId);
|
|
13189
13700
|
await flushAssistantBuffer(sessionId);
|
|
13701
|
+
if (!await ensureSessionInit(sessionId)) {
|
|
13702
|
+
return;
|
|
13703
|
+
}
|
|
13190
13704
|
const isHealthy = await checkWorkerAndToast();
|
|
13191
13705
|
if (!isHealthy) {
|
|
13192
13706
|
return;
|
|
@@ -13203,6 +13717,9 @@ var ClaudeMemPlugin = async (ctx) => {
|
|
|
13203
13717
|
return;
|
|
13204
13718
|
}
|
|
13205
13719
|
await flushAssistantBuffer(sessionId);
|
|
13720
|
+
if (!await ensureSessionInit(sessionId)) {
|
|
13721
|
+
return;
|
|
13722
|
+
}
|
|
13206
13723
|
try {
|
|
13207
13724
|
const { user, assistant, observedModel } = await fetchLastMessages(sessionId);
|
|
13208
13725
|
if (await WorkerClient.summarize(sessionId, user, assistant, { observedModel })) {
|
|
@@ -13218,7 +13735,10 @@ var ClaudeMemPlugin = async (ctx) => {
|
|
|
13218
13735
|
}
|
|
13219
13736
|
await flushAssistantBuffer(sessionId);
|
|
13220
13737
|
discardAssistantBuffer(sessionId);
|
|
13221
|
-
|
|
13738
|
+
memory.delete(sessionId);
|
|
13739
|
+
files.delete(sessionId);
|
|
13740
|
+
resumed.delete(sessionId);
|
|
13741
|
+
subagentSessions.delete(sessionId);
|
|
13222
13742
|
if (currentSessionId === sessionId) {
|
|
13223
13743
|
currentSessionId = null;
|
|
13224
13744
|
}
|
|
@@ -13233,11 +13753,23 @@ var ClaudeMemPlugin = async (ctx) => {
|
|
|
13233
13753
|
const sessionId = input.sessionID;
|
|
13234
13754
|
if (sessionId) {
|
|
13235
13755
|
const userPrompt = extractTextFromParts(output.parts);
|
|
13236
|
-
|
|
13756
|
+
const id = input.messageID ?? output.message.id;
|
|
13757
|
+
if (!id) {
|
|
13758
|
+
return;
|
|
13759
|
+
}
|
|
13760
|
+
if (!memory.hasMessage(sessionId, id)) {
|
|
13761
|
+
await flushAssistantBuffer(sessionId);
|
|
13762
|
+
}
|
|
13763
|
+
currentSessionId = sessionId;
|
|
13764
|
+
memory.select(sessionId, memoryTurn(id, userPrompt, output.parts.some((part) => part.type === "file")));
|
|
13765
|
+
await ensureSessionInit(sessionId);
|
|
13237
13766
|
}
|
|
13238
13767
|
},
|
|
13239
13768
|
"experimental.chat.system.transform": async (input, output) => {
|
|
13240
|
-
const sessionId = input.sessionID;
|
|
13769
|
+
const sessionId = input.sessionID ?? currentSessionId;
|
|
13770
|
+
if (!sessionId) {
|
|
13771
|
+
return;
|
|
13772
|
+
}
|
|
13241
13773
|
if (sessionId) {
|
|
13242
13774
|
await ensureSessionInit(sessionId);
|
|
13243
13775
|
}
|
|
@@ -13246,7 +13778,16 @@ var ClaudeMemPlugin = async (ctx) => {
|
|
|
13246
13778
|
return;
|
|
13247
13779
|
}
|
|
13248
13780
|
try {
|
|
13249
|
-
const
|
|
13781
|
+
const base = await memory.context(sessionId, projectName);
|
|
13782
|
+
const semantic = await memory.semantic(sessionId, projectName);
|
|
13783
|
+
const context = [base, semantic].filter(Boolean).join(`
|
|
13784
|
+
|
|
13785
|
+
`);
|
|
13786
|
+
for (let i = output.system.length - 1;i >= 0; i--) {
|
|
13787
|
+
if (output.system[i].startsWith("<claude-mem-context>")) {
|
|
13788
|
+
output.system.splice(i, 1);
|
|
13789
|
+
}
|
|
13790
|
+
}
|
|
13250
13791
|
if (context) {
|
|
13251
13792
|
output.system.push(`<claude-mem-context>
|
|
13252
13793
|
[Claude-Mem] Memory Active. Previous Context:
|
|
@@ -13257,6 +13798,7 @@ ${context}
|
|
|
13257
13798
|
},
|
|
13258
13799
|
"experimental.session.compacting": async (input, output) => {
|
|
13259
13800
|
const sessionId = input.sessionID;
|
|
13801
|
+
memory.invalidate(sessionId);
|
|
13260
13802
|
if (sessionId) {
|
|
13261
13803
|
await ensureSessionInit(sessionId);
|
|
13262
13804
|
}
|
|
@@ -13265,7 +13807,7 @@ ${context}
|
|
|
13265
13807
|
return;
|
|
13266
13808
|
}
|
|
13267
13809
|
try {
|
|
13268
|
-
const context = await
|
|
13810
|
+
const context = await memory.context(sessionId, projectName);
|
|
13269
13811
|
if (context) {
|
|
13270
13812
|
output.context.push(`<claude-mem-context>
|
|
13271
13813
|
[Claude-Mem] Memory Active. Previous Context:
|
|
@@ -13289,6 +13831,8 @@ ${context}
|
|
|
13289
13831
|
const sanitizedToolInput = sanitizeObservationValue(input.args || {});
|
|
13290
13832
|
const sanitizedToolOutput = truncateUtf8Bytes(stripTaggedContent(normalizeToolOutput(output.output)), MAX_OBSERVATION_BYTES);
|
|
13291
13833
|
await WorkerClient.sendObservation(sessionId, input.tool, sanitizedToolInput, sanitizedToolOutput, projectRoot, input.callID);
|
|
13834
|
+
const history = subagentSessions.has(sessionId) ? "" : await files.read(sessionId, { tool: input.tool, args: input.args }, { directory: projectRoot, project: projectName });
|
|
13835
|
+
output.output += history;
|
|
13292
13836
|
} catch {}
|
|
13293
13837
|
}
|
|
13294
13838
|
};
|