opencode-claude-memory 1.6.3 → 1.6.5
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/bin/opencode-memory +119 -17
- package/dist/index.js +69 -5
- package/package.json +1 -1
package/bin/opencode-memory
CHANGED
|
@@ -473,6 +473,41 @@ get_latest_session_id() {
|
|
|
473
473
|
fi
|
|
474
474
|
}
|
|
475
475
|
|
|
476
|
+
get_opencode_db_path() {
|
|
477
|
+
printf '%s\n' "$HOME/.local/share/opencode/opencode.db"
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
get_session_title_from_db() {
|
|
481
|
+
local session_id="$1"
|
|
482
|
+
local db_path
|
|
483
|
+
db_path=$(get_opencode_db_path)
|
|
484
|
+
|
|
485
|
+
if [ -z "$session_id" ] || [ ! -f "$db_path" ] || ! command -v python3 >/dev/null 2>&1; then
|
|
486
|
+
return 1
|
|
487
|
+
fi
|
|
488
|
+
|
|
489
|
+
python3 - "$db_path" "$session_id" <<'PY'
|
|
490
|
+
import sqlite3
|
|
491
|
+
import sys
|
|
492
|
+
|
|
493
|
+
db_path, session_id = sys.argv[1:3]
|
|
494
|
+
|
|
495
|
+
try:
|
|
496
|
+
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
|
|
497
|
+
row = conn.execute("SELECT title FROM session WHERE id = ? LIMIT 1", (session_id,)).fetchone()
|
|
498
|
+
except Exception:
|
|
499
|
+
raise SystemExit(1)
|
|
500
|
+
finally:
|
|
501
|
+
try:
|
|
502
|
+
conn.close()
|
|
503
|
+
except Exception:
|
|
504
|
+
pass
|
|
505
|
+
|
|
506
|
+
if row and row[0]:
|
|
507
|
+
print(row[0])
|
|
508
|
+
PY
|
|
509
|
+
}
|
|
510
|
+
|
|
476
511
|
get_session_target_id() {
|
|
477
512
|
local before_json="$1"
|
|
478
513
|
local started_at_ms="$2"
|
|
@@ -834,6 +869,78 @@ wait_for_session_target_id() {
|
|
|
834
869
|
wait_for_scoped_session_id_since "$before_json" "$started_at_ms" "$TIMESTAMP_FILE" "$wait_seconds"
|
|
835
870
|
}
|
|
836
871
|
|
|
872
|
+
get_fork_cleanup_candidate_id() {
|
|
873
|
+
local started_at_ms="$1"
|
|
874
|
+
local parent_title="$2"
|
|
875
|
+
local workdir="$3"
|
|
876
|
+
local project_dir="$4"
|
|
877
|
+
local db_path
|
|
878
|
+
db_path=$(get_opencode_db_path)
|
|
879
|
+
|
|
880
|
+
if [ -z "$started_at_ms" ] || [ -z "$parent_title" ] || [ ! -f "$db_path" ] || ! command -v python3 >/dev/null 2>&1; then
|
|
881
|
+
return 1
|
|
882
|
+
fi
|
|
883
|
+
|
|
884
|
+
python3 - "$db_path" "$started_at_ms" "$parent_title" "$workdir" "$project_dir" <<'PY'
|
|
885
|
+
import os
|
|
886
|
+
import re
|
|
887
|
+
import sqlite3
|
|
888
|
+
import sys
|
|
889
|
+
|
|
890
|
+
db_path, started_at_ms_raw, parent_title, workdir, project_dir = sys.argv[1:6]
|
|
891
|
+
started_at_ms = int(started_at_ms_raw or "0")
|
|
892
|
+
scope = {os.path.realpath(path) for path in (workdir, project_dir) if path}
|
|
893
|
+
title_pattern = re.compile(rf"^{re.escape(parent_title)} \(fork #\d+\)$")
|
|
894
|
+
|
|
895
|
+
try:
|
|
896
|
+
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
|
|
897
|
+
rows = conn.execute(
|
|
898
|
+
"SELECT id, title, directory, time_created FROM session WHERE time_created >= ? ORDER BY time_created DESC",
|
|
899
|
+
(started_at_ms,),
|
|
900
|
+
).fetchall()
|
|
901
|
+
except Exception:
|
|
902
|
+
raise SystemExit(1)
|
|
903
|
+
finally:
|
|
904
|
+
try:
|
|
905
|
+
conn.close()
|
|
906
|
+
except Exception:
|
|
907
|
+
pass
|
|
908
|
+
|
|
909
|
+
matches = []
|
|
910
|
+
for session_id, title, directory, time_created in rows:
|
|
911
|
+
if not session_id or not title or not directory or not time_created:
|
|
912
|
+
continue
|
|
913
|
+
if os.path.realpath(directory) not in scope:
|
|
914
|
+
continue
|
|
915
|
+
if not title_pattern.match(title):
|
|
916
|
+
continue
|
|
917
|
+
matches.append(session_id)
|
|
918
|
+
|
|
919
|
+
if len(matches) == 1:
|
|
920
|
+
print(matches[0])
|
|
921
|
+
PY
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
wait_for_fork_cleanup_candidate_id() {
|
|
925
|
+
local started_at_ms="$1"
|
|
926
|
+
local parent_title="$2"
|
|
927
|
+
local wait_seconds="${3:-5}"
|
|
928
|
+
local attempt=0
|
|
929
|
+
local session_id=""
|
|
930
|
+
|
|
931
|
+
while [ "$attempt" -lt "$wait_seconds" ]; do
|
|
932
|
+
session_id=$(get_fork_cleanup_candidate_id "$started_at_ms" "$parent_title" "$WORKING_DIR" "$PROJECT_SCOPE_DIR" || true)
|
|
933
|
+
if [ -n "$session_id" ]; then
|
|
934
|
+
printf '%s\n' "$session_id"
|
|
935
|
+
return 0
|
|
936
|
+
fi
|
|
937
|
+
sleep 1
|
|
938
|
+
attempt=$((attempt + 1))
|
|
939
|
+
done
|
|
940
|
+
|
|
941
|
+
return 1
|
|
942
|
+
}
|
|
943
|
+
|
|
837
944
|
file_mtime_secs() {
|
|
838
945
|
local file="$1"
|
|
839
946
|
if [ ! -f "$file" ]; then
|
|
@@ -973,11 +1080,10 @@ rollback_consolidation_lock() {
|
|
|
973
1080
|
}
|
|
974
1081
|
|
|
975
1082
|
cleanup_forked_sessions() {
|
|
976
|
-
local
|
|
977
|
-
local
|
|
978
|
-
local timestamp_file="$3"
|
|
1083
|
+
local started_at_ms="$1"
|
|
1084
|
+
local parent_title="$2"
|
|
979
1085
|
|
|
980
|
-
if [ -z "$
|
|
1086
|
+
if [ -z "$started_at_ms" ] || [ -z "$parent_title" ]; then
|
|
981
1087
|
return 0
|
|
982
1088
|
fi
|
|
983
1089
|
|
|
@@ -993,7 +1099,7 @@ PY
|
|
|
993
1099
|
fi
|
|
994
1100
|
|
|
995
1101
|
local fork_id
|
|
996
|
-
fork_id=$(
|
|
1102
|
+
fork_id=$(wait_for_fork_cleanup_candidate_id "$started_at_ms" "$parent_title" "$SESSION_WAIT_SECONDS" || true)
|
|
997
1103
|
|
|
998
1104
|
[ -n "$fork_id" ] || return 0
|
|
999
1105
|
|
|
@@ -1040,6 +1146,9 @@ run_extraction_if_needed() {
|
|
|
1040
1146
|
log "Extracting memories from session $session_id..."
|
|
1041
1147
|
log "Extraction log: $EXTRACT_LOG_FILE"
|
|
1042
1148
|
|
|
1149
|
+
local parent_session_title
|
|
1150
|
+
parent_session_title=$(get_session_title_from_db "$session_id" || true)
|
|
1151
|
+
|
|
1043
1152
|
local cmd=("$REAL_OPENCODE" run -s "$session_id" --fork --dir "$WORKING_DIR")
|
|
1044
1153
|
if [ -n "$EXTRACT_MODEL" ]; then
|
|
1045
1154
|
cmd+=(-m "$EXTRACT_MODEL")
|
|
@@ -1049,10 +1158,6 @@ run_extraction_if_needed() {
|
|
|
1049
1158
|
fi
|
|
1050
1159
|
cmd+=("$EXTRACT_PROMPT")
|
|
1051
1160
|
|
|
1052
|
-
local pre_fork_json
|
|
1053
|
-
pre_fork_json=$(get_session_list_json "$AUTODREAM_SCAN_LIMIT" 2>/dev/null || true)
|
|
1054
|
-
local fork_timestamp_file
|
|
1055
|
-
fork_timestamp_file=$(mktemp)
|
|
1056
1161
|
local fork_started_at_ms
|
|
1057
1162
|
fork_started_at_ms=$(( $(date +%s) * 1000 ))
|
|
1058
1163
|
|
|
@@ -1063,8 +1168,7 @@ run_extraction_if_needed() {
|
|
|
1063
1168
|
log "Memory extraction failed (exit code $code). Check $EXTRACT_LOG_FILE for details"
|
|
1064
1169
|
fi
|
|
1065
1170
|
|
|
1066
|
-
cleanup_forked_sessions "$
|
|
1067
|
-
rm -f "$fork_timestamp_file"
|
|
1171
|
+
cleanup_forked_sessions "$fork_started_at_ms" "$parent_session_title"
|
|
1068
1172
|
release_simple_lock "$EXTRACT_LOCK_FILE"
|
|
1069
1173
|
}
|
|
1070
1174
|
|
|
@@ -1107,6 +1211,9 @@ run_autodream_if_needed() {
|
|
|
1107
1211
|
log "Auto-dream firing (${hours_since}h since last consolidation, ${touched_count} sessions touched)"
|
|
1108
1212
|
log "Auto-dream log: $AUTODREAM_LOG_FILE"
|
|
1109
1213
|
|
|
1214
|
+
local parent_session_title
|
|
1215
|
+
parent_session_title=$(get_session_title_from_db "$session_id" || true)
|
|
1216
|
+
|
|
1110
1217
|
local cmd=("$REAL_OPENCODE" run -s "$session_id" --fork --dir "$WORKING_DIR")
|
|
1111
1218
|
if [ -n "$AUTODREAM_MODEL" ]; then
|
|
1112
1219
|
cmd+=(-m "$AUTODREAM_MODEL")
|
|
@@ -1116,10 +1223,6 @@ run_autodream_if_needed() {
|
|
|
1116
1223
|
fi
|
|
1117
1224
|
cmd+=("$AUTODREAM_PROMPT")
|
|
1118
1225
|
|
|
1119
|
-
local pre_fork_json
|
|
1120
|
-
pre_fork_json=$(get_session_list_json "$AUTODREAM_SCAN_LIMIT" 2>/dev/null || true)
|
|
1121
|
-
local fork_timestamp_file
|
|
1122
|
-
fork_timestamp_file=$(mktemp)
|
|
1123
1226
|
local fork_started_at_ms
|
|
1124
1227
|
fork_started_at_ms=$(( $(date +%s) * 1000 ))
|
|
1125
1228
|
|
|
@@ -1132,8 +1235,7 @@ run_autodream_if_needed() {
|
|
|
1132
1235
|
rollback_consolidation_lock "$CONSOLIDATION_PRIOR_MTIME"
|
|
1133
1236
|
fi
|
|
1134
1237
|
|
|
1135
|
-
cleanup_forked_sessions "$
|
|
1136
|
-
rm -f "$fork_timestamp_file"
|
|
1238
|
+
cleanup_forked_sessions "$fork_started_at_ms" "$parent_session_title"
|
|
1137
1239
|
}
|
|
1138
1240
|
|
|
1139
1241
|
run_post_session_tasks() {
|
package/dist/index.js
CHANGED
|
@@ -98,9 +98,67 @@ function extractRecentTools(messages) {
|
|
|
98
98
|
}
|
|
99
99
|
return tools;
|
|
100
100
|
}
|
|
101
|
+
// Tracks how many memory entries a memory_list call saw so tool.execute.after
|
|
102
|
+
// can render a meaningful title without re-reading the filesystem. Keyed by
|
|
103
|
+
// callID, which uniquely identifies a single tool invocation.
|
|
104
|
+
const memoryListCountByCallID = new Map();
|
|
105
|
+
const memorySearchCountByCallID = new Map();
|
|
106
|
+
function buildMemoryToolTitle(toolID, args, callID) {
|
|
107
|
+
switch (toolID) {
|
|
108
|
+
case "memory_save": {
|
|
109
|
+
const type = typeof args?.type === "string" ? args.type : "";
|
|
110
|
+
const name = typeof args?.name === "string" ? args.name : "";
|
|
111
|
+
if (type && name)
|
|
112
|
+
return `${type}: ${name}`;
|
|
113
|
+
if (name)
|
|
114
|
+
return name;
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
case "memory_delete":
|
|
118
|
+
case "memory_read": {
|
|
119
|
+
const fileName = typeof args?.file_name === "string" ? args.file_name : "";
|
|
120
|
+
return fileName || undefined;
|
|
121
|
+
}
|
|
122
|
+
case "memory_list": {
|
|
123
|
+
const count = callID ? memoryListCountByCallID.get(callID) : undefined;
|
|
124
|
+
if (callID)
|
|
125
|
+
memoryListCountByCallID.delete(callID);
|
|
126
|
+
if (count === undefined)
|
|
127
|
+
return "list memories";
|
|
128
|
+
return `${count} ${count === 1 ? "memory" : "memories"}`;
|
|
129
|
+
}
|
|
130
|
+
case "memory_search": {
|
|
131
|
+
const query = typeof args?.query === "string" ? args.query : "";
|
|
132
|
+
const count = callID ? memorySearchCountByCallID.get(callID) : undefined;
|
|
133
|
+
if (callID)
|
|
134
|
+
memorySearchCountByCallID.delete(callID);
|
|
135
|
+
if (query && count !== undefined) {
|
|
136
|
+
return `"${query}" · ${count} ${count === 1 ? "match" : "matches"}`;
|
|
137
|
+
}
|
|
138
|
+
if (query)
|
|
139
|
+
return `"${query}"`;
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
default:
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function getCallID(ctx) {
|
|
147
|
+
if (!ctx || typeof ctx !== "object")
|
|
148
|
+
return undefined;
|
|
149
|
+
const v = ctx.callID;
|
|
150
|
+
return typeof v === "string" ? v : undefined;
|
|
151
|
+
}
|
|
101
152
|
export const MemoryPlugin = async ({ worktree }) => {
|
|
102
153
|
getMemoryDir(worktree);
|
|
103
154
|
return {
|
|
155
|
+
"tool.execute.after": async (input, output) => {
|
|
156
|
+
if (!input.tool.startsWith("memory_"))
|
|
157
|
+
return;
|
|
158
|
+
const title = buildMemoryToolTitle(input.tool, input.args, input.callID);
|
|
159
|
+
if (title)
|
|
160
|
+
output.title = title;
|
|
161
|
+
},
|
|
104
162
|
"experimental.chat.messages.transform": async (_input, output) => {
|
|
105
163
|
const { query, sessionID } = getLastUserQuery(output.messages);
|
|
106
164
|
if (sessionID) {
|
|
@@ -177,7 +235,7 @@ export const MemoryPlugin = async ({ worktree }) => {
|
|
|
177
235
|
.string()
|
|
178
236
|
.describe("Memory content. For feedback/project types, structure as: rule/fact, then **Why:** and **How to apply:** lines"),
|
|
179
237
|
},
|
|
180
|
-
async execute(args) {
|
|
238
|
+
async execute(args, _ctx) {
|
|
181
239
|
const filePath = saveMemory(worktree, args.file_name, args.name, args.description, args.type, args.content);
|
|
182
240
|
return `Memory saved to ${filePath}`;
|
|
183
241
|
},
|
|
@@ -187,7 +245,7 @@ export const MemoryPlugin = async ({ worktree }) => {
|
|
|
187
245
|
args: {
|
|
188
246
|
file_name: tool.schema.string().describe("File name of the memory to delete (with or without .md extension)"),
|
|
189
247
|
},
|
|
190
|
-
async execute(args) {
|
|
248
|
+
async execute(args, _ctx) {
|
|
191
249
|
const deleted = deleteMemory(worktree, args.file_name);
|
|
192
250
|
return deleted ? `Memory "${args.file_name}" deleted.` : `Memory "${args.file_name}" not found.`;
|
|
193
251
|
},
|
|
@@ -197,8 +255,11 @@ export const MemoryPlugin = async ({ worktree }) => {
|
|
|
197
255
|
"Use this to check what memories exist before saving a new one (to avoid duplicates) " +
|
|
198
256
|
"or when you need to recall what's been stored.",
|
|
199
257
|
args: {},
|
|
200
|
-
async execute() {
|
|
258
|
+
async execute(_args, ctx) {
|
|
201
259
|
const entries = listMemories(worktree);
|
|
260
|
+
const callID = getCallID(ctx);
|
|
261
|
+
if (callID)
|
|
262
|
+
memoryListCountByCallID.set(callID, entries.length);
|
|
202
263
|
if (entries.length === 0) {
|
|
203
264
|
return "No memories saved yet.";
|
|
204
265
|
}
|
|
@@ -212,8 +273,11 @@ export const MemoryPlugin = async ({ worktree }) => {
|
|
|
212
273
|
args: {
|
|
213
274
|
query: tool.schema.string().describe("Search query — searches across name, description, and content"),
|
|
214
275
|
},
|
|
215
|
-
async execute(args) {
|
|
276
|
+
async execute(args, ctx) {
|
|
216
277
|
const results = searchMemories(worktree, args.query);
|
|
278
|
+
const callID = getCallID(ctx);
|
|
279
|
+
if (callID)
|
|
280
|
+
memorySearchCountByCallID.set(callID, results.length);
|
|
217
281
|
if (results.length === 0) {
|
|
218
282
|
return `No memories matching "${args.query}".`;
|
|
219
283
|
}
|
|
@@ -226,7 +290,7 @@ export const MemoryPlugin = async ({ worktree }) => {
|
|
|
226
290
|
args: {
|
|
227
291
|
file_name: tool.schema.string().describe("File name of the memory to read (with or without .md extension)"),
|
|
228
292
|
},
|
|
229
|
-
async execute(args) {
|
|
293
|
+
async execute(args, _ctx) {
|
|
230
294
|
const entry = readMemory(worktree, args.file_name);
|
|
231
295
|
if (!entry) {
|
|
232
296
|
return `Memory "${args.file_name}" not found.`;
|
package/package.json
CHANGED