opencode-claude-memory 1.6.2 → 1.6.4
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 +38 -0
- package/bin/opencode-memory +160 -58
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,6 +29,30 @@ Claude Code writes memory → OpenCode reads it. OpenCode writes memory → Clau
|
|
|
29
29
|
|
|
30
30
|
## 🚀 Quick Start
|
|
31
31
|
|
|
32
|
+
### Prerequisites
|
|
33
|
+
|
|
34
|
+
- `opencode`
|
|
35
|
+
- `python3` available in `PATH`
|
|
36
|
+
|
|
37
|
+
`python3` is a runtime dependency for the wrapper's scoped session detection and fork cleanup logic.
|
|
38
|
+
If it is missing or not executable, post-session maintenance becomes less reliable: session targeting can fall back to less precise heuristics, and fork cleanup is skipped for safety.
|
|
39
|
+
|
|
40
|
+
Common install commands:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# macOS (Homebrew)
|
|
44
|
+
brew install python
|
|
45
|
+
|
|
46
|
+
# Ubuntu / Debian
|
|
47
|
+
sudo apt-get update && sudo apt-get install -y python3
|
|
48
|
+
|
|
49
|
+
# Fedora
|
|
50
|
+
sudo dnf install -y python3
|
|
51
|
+
|
|
52
|
+
# Arch Linux
|
|
53
|
+
sudo pacman -S python
|
|
54
|
+
```
|
|
55
|
+
|
|
32
56
|
### 1. Install
|
|
33
57
|
|
|
34
58
|
```bash
|
|
@@ -41,6 +65,8 @@ This installs:
|
|
|
41
65
|
- The `opencode-memory` **CLI** — wraps opencode with automatic memory extraction + auto-dream consolidation
|
|
42
66
|
- A **shell hook** — defines an `opencode()` function in your `.zshrc`/`.bashrc` that delegates to `opencode-memory`
|
|
43
67
|
|
|
68
|
+
If `python3` is not installed yet, install it first using the commands above before enabling the shell hook.
|
|
69
|
+
|
|
44
70
|
### 2. Configure
|
|
45
71
|
|
|
46
72
|
```jsonc
|
|
@@ -118,6 +144,18 @@ The shell hook defines an `opencode()` function that delegates to `opencode-memo
|
|
|
118
144
|
8. Maintenance runs **in the background** unless `OPENCODE_MEMORY_FOREGROUND=1`
|
|
119
145
|
9. Terminal maintenance logs are shown in foreground mode by default, or can be forced on/off with `OPENCODE_MEMORY_TERMINAL_LOG=1|0`
|
|
120
146
|
|
|
147
|
+
### Runtime dependencies
|
|
148
|
+
|
|
149
|
+
The wrapper expects `python3` to be available at runtime.
|
|
150
|
+
|
|
151
|
+
It is used for:
|
|
152
|
+
|
|
153
|
+
- scoped session selection from `opencode session list`
|
|
154
|
+
- parsing `opencode export` output to resolve session directories
|
|
155
|
+
- safely identifying and cleaning up forked extraction / auto-dream sessions
|
|
156
|
+
|
|
157
|
+
Without `python3`, the plugin tools still load, but wrapper maintenance is degraded and fork cleanup is intentionally skipped to avoid deleting the wrong session.
|
|
158
|
+
|
|
121
159
|
### Compatibility details
|
|
122
160
|
|
|
123
161
|
The implementation ports core logic from Claude Code for path hashing, git-root/worktree handling, memory format, and memory prompting behavior, so both tools can operate on the same files safely.
|
package/bin/opencode-memory
CHANGED
|
@@ -473,22 +473,58 @@ 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"
|
|
479
514
|
local workdir="$3"
|
|
480
515
|
local project_dir="$4"
|
|
516
|
+
local allow_existing_fallback="${5:-1}"
|
|
481
517
|
local after_json
|
|
482
518
|
|
|
483
519
|
after_json=$(get_session_list_json "$AUTODREAM_SCAN_LIMIT") || return 1
|
|
484
520
|
|
|
485
521
|
if command -v python3 >/dev/null 2>&1; then
|
|
486
|
-
python3 - "$before_json" "$after_json" "$started_at_ms" "$workdir" "$project_dir" <<'PY'
|
|
522
|
+
python3 - "$before_json" "$after_json" "$started_at_ms" "$workdir" "$project_dir" "$allow_existing_fallback" <<'PY'
|
|
487
523
|
import json
|
|
488
524
|
import os
|
|
489
525
|
import sys
|
|
490
526
|
|
|
491
|
-
before_raw, after_raw, started_at_ms_raw, workdir, project_dir = sys.argv[1:
|
|
527
|
+
before_raw, after_raw, started_at_ms_raw, workdir, project_dir, allow_existing_fallback_raw = sys.argv[1:7]
|
|
492
528
|
|
|
493
529
|
def parse(raw):
|
|
494
530
|
try:
|
|
@@ -522,6 +558,7 @@ def normalize(path):
|
|
|
522
558
|
before = parse(before_raw)
|
|
523
559
|
after = parse(after_raw)
|
|
524
560
|
started_at_ms = int(started_at_ms_raw or "0")
|
|
561
|
+
allow_existing_fallback = allow_existing_fallback_raw == "1"
|
|
525
562
|
before_ids = {item.get("id") for item in before if item.get("id")}
|
|
526
563
|
workdir = normalize(workdir)
|
|
527
564
|
project_dir = normalize(project_dir)
|
|
@@ -544,11 +581,15 @@ def choose(candidates):
|
|
|
544
581
|
new_sessions = [item for item in after if item.get("id") not in before_ids]
|
|
545
582
|
updated_sessions = [item for item in after if timestamp(item) > started_at_ms]
|
|
546
583
|
|
|
547
|
-
|
|
584
|
+
candidate_pools = [
|
|
548
585
|
[item for item in new_sessions if in_scope(item)],
|
|
549
586
|
[item for item in updated_sessions if in_scope(item)],
|
|
550
|
-
|
|
551
|
-
|
|
587
|
+
]
|
|
588
|
+
|
|
589
|
+
if allow_existing_fallback:
|
|
590
|
+
candidate_pools.append([item for item in after if in_scope(item)])
|
|
591
|
+
|
|
592
|
+
for pool in candidate_pools:
|
|
552
593
|
if choose(pool):
|
|
553
594
|
break
|
|
554
595
|
PY
|
|
@@ -795,18 +836,100 @@ main_prompt_requests_ignore_memory() {
|
|
|
795
836
|
printf '%s\n' "$joined" | grep -Eq "(ignore|don't use|do not use|without|skip)[[:space:]]+(the[[:space:]]+)?memory|memory[[:space:]]+((should|must)[[:space:]]+be[[:space:]]+)?ignored"
|
|
796
837
|
}
|
|
797
838
|
|
|
798
|
-
|
|
839
|
+
wait_for_scoped_session_id_since() {
|
|
799
840
|
local before_json="$1"
|
|
800
841
|
local started_at_ms="$2"
|
|
801
|
-
local
|
|
842
|
+
local timestamp_file="$3"
|
|
843
|
+
local wait_seconds="${4:-5}"
|
|
844
|
+
local allow_existing_fallback="${5:-1}"
|
|
802
845
|
local attempt=0
|
|
803
846
|
local session_id=""
|
|
804
847
|
|
|
805
848
|
while [ "$attempt" -lt "$wait_seconds" ]; do
|
|
806
|
-
session_id=$(get_session_target_id "$before_json" "$started_at_ms" "$WORKING_DIR" "$PROJECT_SCOPE_DIR" || true)
|
|
849
|
+
session_id=$(get_session_target_id "$before_json" "$started_at_ms" "$WORKING_DIR" "$PROJECT_SCOPE_DIR" "$allow_existing_fallback" || true)
|
|
807
850
|
if [ -z "$session_id" ]; then
|
|
808
|
-
session_id=$(get_scoped_artifact_session_id_since "$
|
|
851
|
+
session_id=$(get_scoped_artifact_session_id_since "$timestamp_file" "$WORKING_DIR" "$PROJECT_SCOPE_DIR" || true)
|
|
852
|
+
fi
|
|
853
|
+
if [ -n "$session_id" ]; then
|
|
854
|
+
printf '%s\n' "$session_id"
|
|
855
|
+
return 0
|
|
809
856
|
fi
|
|
857
|
+
sleep 1
|
|
858
|
+
attempt=$((attempt + 1))
|
|
859
|
+
done
|
|
860
|
+
|
|
861
|
+
return 1
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
wait_for_session_target_id() {
|
|
865
|
+
local before_json="$1"
|
|
866
|
+
local started_at_ms="$2"
|
|
867
|
+
local wait_seconds="${3:-5}"
|
|
868
|
+
|
|
869
|
+
wait_for_scoped_session_id_since "$before_json" "$started_at_ms" "$TIMESTAMP_FILE" "$wait_seconds"
|
|
870
|
+
}
|
|
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)
|
|
810
933
|
if [ -n "$session_id" ]; then
|
|
811
934
|
printf '%s\n' "$session_id"
|
|
812
935
|
return 0
|
|
@@ -957,59 +1080,32 @@ rollback_consolidation_lock() {
|
|
|
957
1080
|
}
|
|
958
1081
|
|
|
959
1082
|
cleanup_forked_sessions() {
|
|
960
|
-
local
|
|
1083
|
+
local started_at_ms="$1"
|
|
1084
|
+
local parent_title="$2"
|
|
961
1085
|
|
|
962
|
-
if
|
|
1086
|
+
if [ -z "$started_at_ms" ] || [ -z "$parent_title" ]; then
|
|
963
1087
|
return 0
|
|
964
1088
|
fi
|
|
965
1089
|
|
|
966
|
-
|
|
967
|
-
after_json=$(get_session_list_json 10 2>/dev/null || true)
|
|
968
|
-
|
|
969
|
-
if [ -z "$before_json" ] || [ -z "$after_json" ]; then
|
|
1090
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
970
1091
|
return 0
|
|
971
1092
|
fi
|
|
972
1093
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
def parse(raw):
|
|
980
|
-
try:
|
|
981
|
-
data = json.loads(raw)
|
|
982
|
-
return data if isinstance(data, list) else []
|
|
983
|
-
except Exception:
|
|
984
|
-
return []
|
|
985
|
-
|
|
986
|
-
before_raw, after_raw, workdir, project_dir = sys.argv[1:5]
|
|
987
|
-
before = parse(before_raw)
|
|
988
|
-
after = parse(after_raw)
|
|
1094
|
+
if ! python3 - <<'PY' >/dev/null 2>&1
|
|
1095
|
+
pass
|
|
1096
|
+
PY
|
|
1097
|
+
then
|
|
1098
|
+
return 0
|
|
1099
|
+
fi
|
|
989
1100
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
project_dir = os.path.realpath(project_dir)
|
|
1101
|
+
local fork_id
|
|
1102
|
+
fork_id=$(wait_for_fork_cleanup_candidate_id "$started_at_ms" "$parent_title" "$SESSION_WAIT_SECONDS" || true)
|
|
993
1103
|
|
|
994
|
-
|
|
995
|
-
sid = item.get("id", "")
|
|
996
|
-
if not sid or sid in before_ids:
|
|
997
|
-
continue
|
|
998
|
-
directory = item.get("directory", "")
|
|
999
|
-
if not directory:
|
|
1000
|
-
continue
|
|
1001
|
-
d = os.path.realpath(directory)
|
|
1002
|
-
if d in (workdir, project_dir):
|
|
1003
|
-
print(sid)
|
|
1004
|
-
PY
|
|
1005
|
-
) || return 0
|
|
1104
|
+
[ -n "$fork_id" ] || return 0
|
|
1006
1105
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
log "Cleaned up forked session $fork_id"
|
|
1011
|
-
fi
|
|
1012
|
-
done <<< "$fork_ids"
|
|
1106
|
+
if "$REAL_OPENCODE" session delete "$fork_id" >/dev/null 2>&1; then
|
|
1107
|
+
log "Cleaned up forked session $fork_id"
|
|
1108
|
+
fi
|
|
1013
1109
|
}
|
|
1014
1110
|
|
|
1015
1111
|
session_has_conversation() {
|
|
@@ -1050,6 +1146,9 @@ run_extraction_if_needed() {
|
|
|
1050
1146
|
log "Extracting memories from session $session_id..."
|
|
1051
1147
|
log "Extraction log: $EXTRACT_LOG_FILE"
|
|
1052
1148
|
|
|
1149
|
+
local parent_session_title
|
|
1150
|
+
parent_session_title=$(get_session_title_from_db "$session_id" || true)
|
|
1151
|
+
|
|
1053
1152
|
local cmd=("$REAL_OPENCODE" run -s "$session_id" --fork --dir "$WORKING_DIR")
|
|
1054
1153
|
if [ -n "$EXTRACT_MODEL" ]; then
|
|
1055
1154
|
cmd+=(-m "$EXTRACT_MODEL")
|
|
@@ -1059,8 +1158,8 @@ run_extraction_if_needed() {
|
|
|
1059
1158
|
fi
|
|
1060
1159
|
cmd+=("$EXTRACT_PROMPT")
|
|
1061
1160
|
|
|
1062
|
-
local
|
|
1063
|
-
|
|
1161
|
+
local fork_started_at_ms
|
|
1162
|
+
fork_started_at_ms=$(( $(date +%s) * 1000 ))
|
|
1064
1163
|
|
|
1065
1164
|
if "${cmd[@]}" >> "$EXTRACT_LOG_FILE" 2>&1; then
|
|
1066
1165
|
log "Memory extraction completed successfully"
|
|
@@ -1069,7 +1168,7 @@ run_extraction_if_needed() {
|
|
|
1069
1168
|
log "Memory extraction failed (exit code $code). Check $EXTRACT_LOG_FILE for details"
|
|
1070
1169
|
fi
|
|
1071
1170
|
|
|
1072
|
-
cleanup_forked_sessions "$
|
|
1171
|
+
cleanup_forked_sessions "$fork_started_at_ms" "$parent_session_title"
|
|
1073
1172
|
release_simple_lock "$EXTRACT_LOCK_FILE"
|
|
1074
1173
|
}
|
|
1075
1174
|
|
|
@@ -1112,6 +1211,9 @@ run_autodream_if_needed() {
|
|
|
1112
1211
|
log "Auto-dream firing (${hours_since}h since last consolidation, ${touched_count} sessions touched)"
|
|
1113
1212
|
log "Auto-dream log: $AUTODREAM_LOG_FILE"
|
|
1114
1213
|
|
|
1214
|
+
local parent_session_title
|
|
1215
|
+
parent_session_title=$(get_session_title_from_db "$session_id" || true)
|
|
1216
|
+
|
|
1115
1217
|
local cmd=("$REAL_OPENCODE" run -s "$session_id" --fork --dir "$WORKING_DIR")
|
|
1116
1218
|
if [ -n "$AUTODREAM_MODEL" ]; then
|
|
1117
1219
|
cmd+=(-m "$AUTODREAM_MODEL")
|
|
@@ -1121,8 +1223,8 @@ run_autodream_if_needed() {
|
|
|
1121
1223
|
fi
|
|
1122
1224
|
cmd+=("$AUTODREAM_PROMPT")
|
|
1123
1225
|
|
|
1124
|
-
local
|
|
1125
|
-
|
|
1226
|
+
local fork_started_at_ms
|
|
1227
|
+
fork_started_at_ms=$(( $(date +%s) * 1000 ))
|
|
1126
1228
|
|
|
1127
1229
|
if "${cmd[@]}" >> "$AUTODREAM_LOG_FILE" 2>&1; then
|
|
1128
1230
|
log "Auto-dream consolidation completed successfully"
|
|
@@ -1133,7 +1235,7 @@ run_autodream_if_needed() {
|
|
|
1133
1235
|
rollback_consolidation_lock "$CONSOLIDATION_PRIOR_MTIME"
|
|
1134
1236
|
fi
|
|
1135
1237
|
|
|
1136
|
-
cleanup_forked_sessions "$
|
|
1238
|
+
cleanup_forked_sessions "$fork_started_at_ms" "$parent_session_title"
|
|
1137
1239
|
}
|
|
1138
1240
|
|
|
1139
1241
|
run_post_session_tasks() {
|
package/package.json
CHANGED