opencode-claude-memory 1.6.1 → 1.6.3
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 +99 -10
- 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
|
@@ -478,17 +478,18 @@ get_session_target_id() {
|
|
|
478
478
|
local started_at_ms="$2"
|
|
479
479
|
local workdir="$3"
|
|
480
480
|
local project_dir="$4"
|
|
481
|
+
local allow_existing_fallback="${5:-1}"
|
|
481
482
|
local after_json
|
|
482
483
|
|
|
483
484
|
after_json=$(get_session_list_json "$AUTODREAM_SCAN_LIMIT") || return 1
|
|
484
485
|
|
|
485
486
|
if command -v python3 >/dev/null 2>&1; then
|
|
486
|
-
python3 - "$before_json" "$after_json" "$started_at_ms" "$workdir" "$project_dir" <<'PY'
|
|
487
|
+
python3 - "$before_json" "$after_json" "$started_at_ms" "$workdir" "$project_dir" "$allow_existing_fallback" <<'PY'
|
|
487
488
|
import json
|
|
488
489
|
import os
|
|
489
490
|
import sys
|
|
490
491
|
|
|
491
|
-
before_raw, after_raw, started_at_ms_raw, workdir, project_dir = sys.argv[1:
|
|
492
|
+
before_raw, after_raw, started_at_ms_raw, workdir, project_dir, allow_existing_fallback_raw = sys.argv[1:7]
|
|
492
493
|
|
|
493
494
|
def parse(raw):
|
|
494
495
|
try:
|
|
@@ -522,6 +523,7 @@ def normalize(path):
|
|
|
522
523
|
before = parse(before_raw)
|
|
523
524
|
after = parse(after_raw)
|
|
524
525
|
started_at_ms = int(started_at_ms_raw or "0")
|
|
526
|
+
allow_existing_fallback = allow_existing_fallback_raw == "1"
|
|
525
527
|
before_ids = {item.get("id") for item in before if item.get("id")}
|
|
526
528
|
workdir = normalize(workdir)
|
|
527
529
|
project_dir = normalize(project_dir)
|
|
@@ -544,11 +546,15 @@ def choose(candidates):
|
|
|
544
546
|
new_sessions = [item for item in after if item.get("id") not in before_ids]
|
|
545
547
|
updated_sessions = [item for item in after if timestamp(item) > started_at_ms]
|
|
546
548
|
|
|
547
|
-
|
|
549
|
+
candidate_pools = [
|
|
548
550
|
[item for item in new_sessions if in_scope(item)],
|
|
549
551
|
[item for item in updated_sessions if in_scope(item)],
|
|
550
|
-
|
|
551
|
-
|
|
552
|
+
]
|
|
553
|
+
|
|
554
|
+
if allow_existing_fallback:
|
|
555
|
+
candidate_pools.append([item for item in after if in_scope(item)])
|
|
556
|
+
|
|
557
|
+
for pool in candidate_pools:
|
|
552
558
|
if choose(pool):
|
|
553
559
|
break
|
|
554
560
|
PY
|
|
@@ -795,17 +801,19 @@ main_prompt_requests_ignore_memory() {
|
|
|
795
801
|
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
802
|
}
|
|
797
803
|
|
|
798
|
-
|
|
804
|
+
wait_for_scoped_session_id_since() {
|
|
799
805
|
local before_json="$1"
|
|
800
806
|
local started_at_ms="$2"
|
|
801
|
-
local
|
|
807
|
+
local timestamp_file="$3"
|
|
808
|
+
local wait_seconds="${4:-5}"
|
|
809
|
+
local allow_existing_fallback="${5:-1}"
|
|
802
810
|
local attempt=0
|
|
803
811
|
local session_id=""
|
|
804
812
|
|
|
805
813
|
while [ "$attempt" -lt "$wait_seconds" ]; do
|
|
806
|
-
session_id=$(get_session_target_id "$before_json" "$started_at_ms" "$WORKING_DIR" "$PROJECT_SCOPE_DIR" || true)
|
|
814
|
+
session_id=$(get_session_target_id "$before_json" "$started_at_ms" "$WORKING_DIR" "$PROJECT_SCOPE_DIR" "$allow_existing_fallback" || true)
|
|
807
815
|
if [ -z "$session_id" ]; then
|
|
808
|
-
session_id=$(get_scoped_artifact_session_id_since "$
|
|
816
|
+
session_id=$(get_scoped_artifact_session_id_since "$timestamp_file" "$WORKING_DIR" "$PROJECT_SCOPE_DIR" || true)
|
|
809
817
|
fi
|
|
810
818
|
if [ -n "$session_id" ]; then
|
|
811
819
|
printf '%s\n' "$session_id"
|
|
@@ -818,6 +826,14 @@ wait_for_session_target_id() {
|
|
|
818
826
|
return 1
|
|
819
827
|
}
|
|
820
828
|
|
|
829
|
+
wait_for_session_target_id() {
|
|
830
|
+
local before_json="$1"
|
|
831
|
+
local started_at_ms="$2"
|
|
832
|
+
local wait_seconds="${3:-5}"
|
|
833
|
+
|
|
834
|
+
wait_for_scoped_session_id_since "$before_json" "$started_at_ms" "$TIMESTAMP_FILE" "$wait_seconds"
|
|
835
|
+
}
|
|
836
|
+
|
|
821
837
|
file_mtime_secs() {
|
|
822
838
|
local file="$1"
|
|
823
839
|
if [ ! -f "$file" ]; then
|
|
@@ -956,6 +972,54 @@ rollback_consolidation_lock() {
|
|
|
956
972
|
set_file_mtime_secs "$path" "$prior_mtime"
|
|
957
973
|
}
|
|
958
974
|
|
|
975
|
+
cleanup_forked_sessions() {
|
|
976
|
+
local before_json="$1"
|
|
977
|
+
local started_at_ms="$2"
|
|
978
|
+
local timestamp_file="$3"
|
|
979
|
+
|
|
980
|
+
if [ -z "$before_json" ] || [ -z "$started_at_ms" ] || [ -z "$timestamp_file" ]; then
|
|
981
|
+
return 0
|
|
982
|
+
fi
|
|
983
|
+
|
|
984
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
985
|
+
return 0
|
|
986
|
+
fi
|
|
987
|
+
|
|
988
|
+
if ! python3 - <<'PY' >/dev/null 2>&1
|
|
989
|
+
pass
|
|
990
|
+
PY
|
|
991
|
+
then
|
|
992
|
+
return 0
|
|
993
|
+
fi
|
|
994
|
+
|
|
995
|
+
local fork_id
|
|
996
|
+
fork_id=$(wait_for_scoped_session_id_since "$before_json" "$started_at_ms" "$timestamp_file" "$SESSION_WAIT_SECONDS" 0 || true)
|
|
997
|
+
|
|
998
|
+
[ -n "$fork_id" ] || return 0
|
|
999
|
+
|
|
1000
|
+
if "$REAL_OPENCODE" session delete "$fork_id" >/dev/null 2>&1; then
|
|
1001
|
+
log "Cleaned up forked session $fork_id"
|
|
1002
|
+
fi
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
session_has_conversation() {
|
|
1006
|
+
local session_id="$1"
|
|
1007
|
+
local transcript_file
|
|
1008
|
+
transcript_file="$(get_transcripts_dir)/${session_id}.jsonl"
|
|
1009
|
+
|
|
1010
|
+
if [ -f "$transcript_file" ]; then
|
|
1011
|
+
local line_count
|
|
1012
|
+
line_count=$(wc -l < "$transcript_file")
|
|
1013
|
+
# Empty sessions have at most 1 line (the initial user entry with no content).
|
|
1014
|
+
# Real sessions have 3+ lines (user + tool_use + tool_result at minimum).
|
|
1015
|
+
[ "$line_count" -gt 1 ]
|
|
1016
|
+
return $?
|
|
1017
|
+
fi
|
|
1018
|
+
|
|
1019
|
+
# Transcript not found — assume content exists to avoid skipping a valid session.
|
|
1020
|
+
return 0
|
|
1021
|
+
}
|
|
1022
|
+
|
|
959
1023
|
run_extraction_if_needed() {
|
|
960
1024
|
local session_id="$1"
|
|
961
1025
|
local memory_written_during_session="$2"
|
|
@@ -985,6 +1049,13 @@ run_extraction_if_needed() {
|
|
|
985
1049
|
fi
|
|
986
1050
|
cmd+=("$EXTRACT_PROMPT")
|
|
987
1051
|
|
|
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
|
+
local fork_started_at_ms
|
|
1057
|
+
fork_started_at_ms=$(( $(date +%s) * 1000 ))
|
|
1058
|
+
|
|
988
1059
|
if "${cmd[@]}" >> "$EXTRACT_LOG_FILE" 2>&1; then
|
|
989
1060
|
log "Memory extraction completed successfully"
|
|
990
1061
|
else
|
|
@@ -992,6 +1063,8 @@ run_extraction_if_needed() {
|
|
|
992
1063
|
log "Memory extraction failed (exit code $code). Check $EXTRACT_LOG_FILE for details"
|
|
993
1064
|
fi
|
|
994
1065
|
|
|
1066
|
+
cleanup_forked_sessions "$pre_fork_json" "$fork_started_at_ms" "$fork_timestamp_file"
|
|
1067
|
+
rm -f "$fork_timestamp_file"
|
|
995
1068
|
release_simple_lock "$EXTRACT_LOCK_FILE"
|
|
996
1069
|
}
|
|
997
1070
|
|
|
@@ -1043,6 +1116,13 @@ run_autodream_if_needed() {
|
|
|
1043
1116
|
fi
|
|
1044
1117
|
cmd+=("$AUTODREAM_PROMPT")
|
|
1045
1118
|
|
|
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
|
+
local fork_started_at_ms
|
|
1124
|
+
fork_started_at_ms=$(( $(date +%s) * 1000 ))
|
|
1125
|
+
|
|
1046
1126
|
if "${cmd[@]}" >> "$AUTODREAM_LOG_FILE" 2>&1; then
|
|
1047
1127
|
log "Auto-dream consolidation completed successfully"
|
|
1048
1128
|
# Keep lock mtime at "now" to represent last consolidated timestamp.
|
|
@@ -1050,8 +1130,10 @@ run_autodream_if_needed() {
|
|
|
1050
1130
|
local code=$?
|
|
1051
1131
|
log "Auto-dream consolidation failed (exit code $code). Rolling back gate timestamp"
|
|
1052
1132
|
rollback_consolidation_lock "$CONSOLIDATION_PRIOR_MTIME"
|
|
1053
|
-
return 0
|
|
1054
1133
|
fi
|
|
1134
|
+
|
|
1135
|
+
cleanup_forked_sessions "$pre_fork_json" "$fork_started_at_ms" "$fork_timestamp_file"
|
|
1136
|
+
rm -f "$fork_timestamp_file"
|
|
1055
1137
|
}
|
|
1056
1138
|
|
|
1057
1139
|
run_post_session_tasks() {
|
|
@@ -1093,6 +1175,13 @@ if [ -z "$session_id" ]; then
|
|
|
1093
1175
|
exit $opencode_exit
|
|
1094
1176
|
fi
|
|
1095
1177
|
|
|
1178
|
+
# Step 3.5: Skip if session had no real conversation (e.g. user opened TUI and exited)
|
|
1179
|
+
if ! session_has_conversation "$session_id"; then
|
|
1180
|
+
log "Session $session_id has no conversation, skipping post-session memory maintenance"
|
|
1181
|
+
cleanup_timestamp
|
|
1182
|
+
exit $opencode_exit
|
|
1183
|
+
fi
|
|
1184
|
+
|
|
1096
1185
|
# Step 4: Check whether main session already wrote memory files
|
|
1097
1186
|
memory_written_during_session=0
|
|
1098
1187
|
if has_new_memories; then
|
package/package.json
CHANGED