@cyperx/clawforge 1.2.0

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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +312 -0
  3. package/VERSION +1 -0
  4. package/bin/attach.sh +98 -0
  5. package/bin/check-agents.sh +343 -0
  6. package/bin/clawforge +257 -0
  7. package/bin/clawforge-dashboard +0 -0
  8. package/bin/clean.sh +257 -0
  9. package/bin/config.sh +111 -0
  10. package/bin/conflicts.sh +224 -0
  11. package/bin/cost.sh +273 -0
  12. package/bin/dashboard.sh +557 -0
  13. package/bin/diff.sh +109 -0
  14. package/bin/doctor.sh +196 -0
  15. package/bin/eval.sh +217 -0
  16. package/bin/history.sh +91 -0
  17. package/bin/init.sh +182 -0
  18. package/bin/learn.sh +230 -0
  19. package/bin/logs.sh +126 -0
  20. package/bin/memory.sh +207 -0
  21. package/bin/merge-helper.sh +174 -0
  22. package/bin/multi-review.sh +215 -0
  23. package/bin/notify.sh +93 -0
  24. package/bin/on-complete.sh +149 -0
  25. package/bin/parse-cost.sh +205 -0
  26. package/bin/pr.sh +167 -0
  27. package/bin/resume.sh +183 -0
  28. package/bin/review-mode.sh +163 -0
  29. package/bin/review-pr.sh +145 -0
  30. package/bin/routing.sh +88 -0
  31. package/bin/scope-task.sh +169 -0
  32. package/bin/spawn-agent.sh +190 -0
  33. package/bin/sprint.sh +320 -0
  34. package/bin/steer.sh +107 -0
  35. package/bin/stop.sh +136 -0
  36. package/bin/summary.sh +182 -0
  37. package/bin/swarm.sh +525 -0
  38. package/bin/templates.sh +244 -0
  39. package/lib/common.sh +302 -0
  40. package/lib/templates/bugfix.json +6 -0
  41. package/lib/templates/migration.json +7 -0
  42. package/lib/templates/refactor.json +6 -0
  43. package/lib/templates/security-audit.json +5 -0
  44. package/lib/templates/test-coverage.json +6 -0
  45. package/package.json +31 -0
  46. package/registry/conflicts.jsonl +0 -0
  47. package/registry/costs.jsonl +0 -0
  48. package/tui/PRD.md +106 -0
  49. package/tui/agent.go +266 -0
  50. package/tui/animation.go +192 -0
  51. package/tui/dashboard.go +219 -0
  52. package/tui/filter.go +68 -0
  53. package/tui/go.mod +25 -0
  54. package/tui/go.sum +46 -0
  55. package/tui/keybindings.go +229 -0
  56. package/tui/main.go +61 -0
  57. package/tui/model.go +166 -0
  58. package/tui/steer.go +69 -0
  59. package/tui/styles.go +69 -0
package/bin/learn.sh ADDED
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env bash
2
+ # learn.sh — Module 9: Capture learnings from completed tasks
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ source "${SCRIPT_DIR}/../lib/common.sh"
7
+
8
+ LEARNINGS_FILE="${CLAWFORGE_DIR}/registry/learnings.jsonl"
9
+ MEMORY_DIR="$HOME/.openclaw/agents/builder/memory"
10
+
11
+ # ── Help ───────────────────────────────────────────────────────────────
12
+ usage() {
13
+ cat <<EOF
14
+ Usage: learn.sh [options]
15
+
16
+ Options:
17
+ --task-id <id> Task ID to learn from (required unless --summary)
18
+ --auto Auto-generate notes from task data
19
+ --notes <text> Manual notes to attach
20
+ --tags <t1,t2> Comma-separated pattern tags
21
+ --summary Output summary of all learnings
22
+ --memory Also append to Builder's daily memory
23
+ --help Show this help
24
+ EOF
25
+ }
26
+
27
+ # ── Parse args ─────────────────────────────────────────────────────────
28
+ TASK_ID="" AUTO=false NOTES="" TAGS="" SUMMARY=false MEMORY=false
29
+
30
+ while [[ $# -gt 0 ]]; do
31
+ case "$1" in
32
+ --task-id) TASK_ID="$2"; shift 2 ;;
33
+ --auto) AUTO=true; shift ;;
34
+ --notes) NOTES="$2"; shift 2 ;;
35
+ --tags) TAGS="$2"; shift 2 ;;
36
+ --summary) SUMMARY=true; shift ;;
37
+ --memory) MEMORY=true; shift ;;
38
+ --help|-h) usage; exit 0 ;;
39
+ *) log_error "Unknown option: $1"; usage; exit 1 ;;
40
+ esac
41
+ done
42
+
43
+ mkdir -p "$(dirname "$LEARNINGS_FILE")"
44
+
45
+ # ── Summary mode ─────────────────────────────────────────────────────
46
+ if $SUMMARY; then
47
+ if [[ ! -f "$LEARNINGS_FILE" ]]; then
48
+ echo "No learnings recorded yet."
49
+ exit 0
50
+ fi
51
+
52
+ TOTAL=$(wc -l < "$LEARNINGS_FILE" | tr -d ' ')
53
+ SUCCESSES=$(grep -c '"success":true' "$LEARNINGS_FILE" 2>/dev/null || echo 0)
54
+ FAILURES=$(grep -c '"success":false' "$LEARNINGS_FILE" 2>/dev/null || echo 0)
55
+
56
+ echo "=== Learning Summary ==="
57
+ echo "Total entries: $TOTAL"
58
+ echo "Successes: $SUCCESSES"
59
+ echo "Failures: $FAILURES"
60
+ if [[ "$TOTAL" -gt 0 ]]; then
61
+ RATE=$(python3 -c "print(f'{($SUCCESSES/$TOTAL)*100:.0f}%')" 2>/dev/null || echo "N/A")
62
+ echo "Success rate: $RATE"
63
+ fi
64
+ echo ""
65
+
66
+ # Average duration
67
+ AVG_DURATION=$(cat "$LEARNINGS_FILE" | jq -s '[.[].duration_minutes | select(. != null and . > 0)] | if length > 0 then (add / length | floor) else 0 end' 2>/dev/null || echo 0)
68
+ echo "Avg duration: ${AVG_DURATION} min"
69
+
70
+ # Agent breakdown
71
+ echo ""
72
+ echo "By agent:"
73
+ cat "$LEARNINGS_FILE" | jq -r '.agent' | sort | uniq -c | sort -rn | while read count agent; do
74
+ echo " $agent: $count"
75
+ done
76
+
77
+ # Model breakdown
78
+ echo ""
79
+ echo "By model:"
80
+ cat "$LEARNINGS_FILE" | jq -r '.model' | sort | uniq -c | sort -rn | while read count model; do
81
+ echo " $model: $count"
82
+ done
83
+
84
+ # Recent entries
85
+ echo ""
86
+ echo "Recent (last 5):"
87
+ tail -5 "$LEARNINGS_FILE" | jq -r '" [\(.taskId)] \(.agent)/\(.model) — \(.success | if . then "✅" else "❌" end) \(.duration_minutes // "?")min — \(.notes // "no notes")"' 2>/dev/null || true
88
+
89
+ exit 0
90
+ fi
91
+
92
+ # ── Learn from task ──────────────────────────────────────────────────
93
+ [[ -z "$TASK_ID" ]] && { log_error "--task-id is required (or use --summary)"; usage; exit 1; }
94
+
95
+ TASK_DATA=$(registry_get "$TASK_ID")
96
+ if [[ -z "$TASK_DATA" ]]; then
97
+ log_error "Task '$TASK_ID' not found in registry"
98
+ exit 1
99
+ fi
100
+
101
+ AGENT=$(echo "$TASK_DATA" | jq -r '.agent // "unknown"')
102
+ MODEL=$(echo "$TASK_DATA" | jq -r '.model // "unknown"')
103
+ RETRIES=$(echo "$TASK_DATA" | jq -r '.retries // 0')
104
+ STATUS=$(echo "$TASK_DATA" | jq -r '.status // "unknown"')
105
+ STARTED=$(echo "$TASK_DATA" | jq -r '.startedAt // 0')
106
+ COMPLETED=$(echo "$TASK_DATA" | jq -r '.completedAt // 0')
107
+ BRANCH=$(echo "$TASK_DATA" | jq -r '.branch // ""')
108
+ CHECKS=$(echo "$TASK_DATA" | jq '.checks // {}')
109
+ DESC=$(echo "$TASK_DATA" | jq -r '.description // ""')
110
+
111
+ # Calculate duration
112
+ DURATION_MIN=0
113
+ if [[ "$STARTED" -gt 0 && "$COMPLETED" -gt 0 ]]; then
114
+ DURATION_MS=$((COMPLETED - STARTED))
115
+ DURATION_MIN=$((DURATION_MS / 60000))
116
+ fi
117
+
118
+ # Determine success
119
+ SUCCESS=false
120
+ if [[ "$STATUS" == "done" || "$STATUS" == "archived" ]]; then
121
+ SUCCESS=true
122
+ fi
123
+
124
+ # Reviews passed
125
+ REVIEWS_PASSED=$(echo "$CHECKS" | jq '[to_entries[] | select(.value | test("APPROVE"; "i")) | .key]' 2>/dev/null || echo '[]')
126
+
127
+ # Auto-generate notes
128
+ if $AUTO && [[ -z "$NOTES" ]]; then
129
+ if [[ "$RETRIES" -eq 0 ]] && $SUCCESS; then
130
+ NOTES="One-shot success."
131
+ elif [[ "$RETRIES" -gt 0 ]] && $SUCCESS; then
132
+ NOTES="Succeeded after $RETRIES retries."
133
+ elif ! $SUCCESS; then
134
+ NOTES="Failed. Status: $STATUS."
135
+ fi
136
+
137
+ if [[ "$DURATION_MIN" -gt 60 ]]; then
138
+ NOTES+=" Long-running task (${DURATION_MIN}min)."
139
+ elif [[ "$DURATION_MIN" -gt 0 && "$DURATION_MIN" -le 10 ]]; then
140
+ NOTES+=" Quick task (${DURATION_MIN}min)."
141
+ fi
142
+ fi
143
+
144
+ # Parse tags
145
+ TAGS_JSON="[]"
146
+ if [[ -n "$TAGS" ]]; then
147
+ TAGS_JSON=$(echo "$TAGS" | tr ',' '\n' | jq -R . | jq -s .)
148
+ else
149
+ # Auto-tag from branch name
150
+ TAGS_JSON="[]"
151
+ if [[ "$BRANCH" == feat/* || "$BRANCH" == feature/* ]]; then
152
+ TAGS_JSON='["feature"]'
153
+ elif [[ "$BRANCH" == fix/* || "$BRANCH" == bugfix/* ]]; then
154
+ TAGS_JSON='["bugfix"]'
155
+ elif [[ "$BRANCH" == refactor/* ]]; then
156
+ TAGS_JSON='["refactor"]'
157
+ fi
158
+ fi
159
+
160
+ # Build learning entry
161
+ NOW=$(epoch_ms)
162
+ LEARNING=$(jq -cn \
163
+ --argjson timestamp "$NOW" \
164
+ --arg taskId "$TASK_ID" \
165
+ --arg agent "$AGENT" \
166
+ --arg model "$MODEL" \
167
+ --argjson duration_minutes "$DURATION_MIN" \
168
+ --argjson retries "$RETRIES" \
169
+ --argjson success "$SUCCESS" \
170
+ --argjson reviews_passed "$REVIEWS_PASSED" \
171
+ --arg branch "$BRANCH" \
172
+ --argjson pattern_tags "$TAGS_JSON" \
173
+ --arg notes "${NOTES:-}" \
174
+ '{
175
+ timestamp: $timestamp,
176
+ taskId: $taskId,
177
+ agent: $agent,
178
+ model: $model,
179
+ duration_minutes: $duration_minutes,
180
+ retries: $retries,
181
+ success: $success,
182
+ reviews_passed: $reviews_passed,
183
+ branch: $branch,
184
+ pattern_tags: $pattern_tags,
185
+ notes: $notes
186
+ }')
187
+
188
+ # Write learning
189
+ echo "$LEARNING" >> "$LEARNINGS_FILE"
190
+ log_info "Learning recorded for task: $TASK_ID"
191
+
192
+ # Output
193
+ echo "$LEARNING" | jq .
194
+
195
+ # Append to clawforge memory with source=learn
196
+ CLAWFORGE_MEMORY_BASE="$HOME/.clawforge/memory"
197
+ LEARN_REPO=$(echo "$TASK_DATA" | jq -r '.repo // empty')
198
+ if [[ -n "$LEARN_REPO" ]]; then
199
+ LEARN_REPO_NAME=$(basename "$LEARN_REPO")
200
+ LEARN_REMOTE=$(git -C "$LEARN_REPO" config --get remote.origin.url 2>/dev/null || true)
201
+ [[ -n "$LEARN_REMOTE" ]] && LEARN_REPO_NAME=$(basename "$LEARN_REMOTE" .git)
202
+ LEARN_MEMORY_FILE="${CLAWFORGE_MEMORY_BASE}/${LEARN_REPO_NAME}.jsonl"
203
+ mkdir -p "$CLAWFORGE_MEMORY_BASE"
204
+ MEMORY_TEXT="[learn] ${DESC}: ${NOTES:-$AGENT/$MODEL, ${DURATION_MIN}min, retries=$RETRIES}"
205
+ LEARN_MEM_ENTRY=$(jq -cn \
206
+ --arg id "learn-$(date +%s)" \
207
+ --arg text "$MEMORY_TEXT" \
208
+ --argjson tags "$TAGS_JSON" \
209
+ --arg created "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
210
+ --arg source "learn" \
211
+ '{id:$id, text:$text, tags:$tags, created:$created, source:$source}')
212
+ echo "$LEARN_MEM_ENTRY" >> "$LEARN_MEMORY_FILE"
213
+ log_info "Appended to clawforge memory: $LEARN_MEMORY_FILE"
214
+ fi
215
+
216
+ # Append to Builder's daily memory
217
+ if $MEMORY; then
218
+ mkdir -p "$MEMORY_DIR"
219
+ TODAY=$(date +%Y-%m-%d)
220
+ MEMORY_FILE="${MEMORY_DIR}/${TODAY}.md"
221
+
222
+ {
223
+ echo ""
224
+ echo "## Learning: $TASK_ID"
225
+ echo "- Agent: $AGENT / $MODEL"
226
+ echo "- Duration: ${DURATION_MIN}min | Retries: $RETRIES | Success: $SUCCESS"
227
+ [[ -n "$NOTES" ]] && echo "- Notes: $NOTES"
228
+ } >> "$MEMORY_FILE"
229
+ log_info "Appended to memory: $MEMORY_FILE"
230
+ fi
package/bin/logs.sh ADDED
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env bash
2
+ # logs.sh — Capture and display agent output from tmux pane
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ source "${SCRIPT_DIR}/../lib/common.sh"
7
+
8
+ usage() {
9
+ cat <<EOF
10
+ Usage: clawforge logs <id> [options]
11
+
12
+ Capture output from a running agent's tmux session without attaching.
13
+
14
+ Arguments:
15
+ <id> Task ID or short ID (e.g., 1 or sprint-add-jwt)
16
+
17
+ Options:
18
+ --lines <N> Number of lines to capture (default: 50)
19
+ --follow Stream output continuously (Ctrl+C to stop)
20
+ --raw Don't strip ANSI escape codes
21
+ --save <path> Save output to file
22
+ --help Show this help
23
+
24
+ Examples:
25
+ clawforge logs 1
26
+ clawforge logs 1 --lines 100
27
+ clawforge logs 1 --follow
28
+ clawforge logs sprint-jwt --save /tmp/agent-output.log
29
+ EOF
30
+ }
31
+
32
+ TASK_REF="" LINES=50 FOLLOW=false RAW=false SAVE_PATH=""
33
+
34
+ while [[ $# -gt 0 ]]; do
35
+ case "$1" in
36
+ --lines) LINES="$2"; shift 2 ;;
37
+ --follow) FOLLOW=true; shift ;;
38
+ --raw) RAW=true; shift ;;
39
+ --save) SAVE_PATH="$2"; shift 2 ;;
40
+ --help|-h) usage; exit 0 ;;
41
+ --*) log_error "Unknown option: $1"; usage; exit 1 ;;
42
+ *) TASK_REF="$1"; shift ;;
43
+ esac
44
+ done
45
+
46
+ [[ -z "$TASK_REF" ]] && { log_error "Task ID required"; usage; exit 1; }
47
+
48
+ # Resolve task: try short ID first, then full ID
49
+ _ensure_registry
50
+ TASK_DATA=""
51
+ if [[ "$TASK_REF" =~ ^[0-9]+$ ]]; then
52
+ TASK_DATA=$(jq -r --argjson sid "$TASK_REF" '.tasks[] | select(.short_id == $sid)' "$REGISTRY_FILE" 2>/dev/null || true)
53
+ fi
54
+ if [[ -z "$TASK_DATA" ]]; then
55
+ TASK_DATA=$(registry_get "$TASK_REF" 2>/dev/null || true)
56
+ fi
57
+ if [[ -z "$TASK_DATA" ]]; then
58
+ log_error "Task '$TASK_REF' not found in registry"
59
+ exit 1
60
+ fi
61
+
62
+ TMUX_SESSION=$(echo "$TASK_DATA" | jq -r '.tmuxSession // empty')
63
+ TASK_ID=$(echo "$TASK_DATA" | jq -r '.id')
64
+
65
+ if [[ -z "$TMUX_SESSION" ]]; then
66
+ TMUX_SESSION="agent-${TASK_ID}"
67
+ fi
68
+
69
+ # Check tmux session exists
70
+ if ! tmux has-session -t "$TMUX_SESSION" 2>/dev/null; then
71
+ log_error "tmux session '$TMUX_SESSION' not found (agent may have exited)"
72
+ echo "Tip: Use 'clawforge history' to see completed task records."
73
+ exit 1
74
+ fi
75
+
76
+ # Capture function
77
+ capture_output() {
78
+ local output
79
+ output=$(tmux capture-pane -t "$TMUX_SESSION" -p -S "-${LINES}" 2>/dev/null || true)
80
+ if [[ -z "$output" ]]; then
81
+ echo "(no output captured)"
82
+ return
83
+ fi
84
+ if ! $RAW; then
85
+ # Strip ANSI escape codes
86
+ output=$(printf '%s' "$output" | sed $'s/\x1b\[[0-9;]*[a-zA-Z]//g')
87
+ fi
88
+ echo "$output"
89
+ }
90
+
91
+ # Follow mode
92
+ if $FOLLOW; then
93
+ echo "Following output from agent #${TASK_REF} (${TMUX_SESSION}). Ctrl+C to stop."
94
+ echo "────────────────────────────────────────"
95
+ LAST_HASH=""
96
+ while true; do
97
+ OUTPUT=$(capture_output)
98
+ HASH=$(printf '%s' "$OUTPUT" | md5 2>/dev/null || printf '%s' "$OUTPUT" | md5sum 2>/dev/null | cut -d' ' -f1 || echo "x")
99
+ if [[ "$HASH" != "$LAST_HASH" ]]; then
100
+ clear 2>/dev/null || true
101
+ echo "Following agent #${TASK_REF} (${TMUX_SESSION}) — $(date +%H:%M:%S)"
102
+ echo "────────────────────────────────────────"
103
+ echo "$OUTPUT"
104
+ LAST_HASH="$HASH"
105
+ fi
106
+ sleep 1
107
+ # Stop if session dies
108
+ if ! tmux has-session -t "$TMUX_SESSION" 2>/dev/null; then
109
+ echo ""
110
+ echo "── Session ended ──"
111
+ break
112
+ fi
113
+ done
114
+ exit 0
115
+ fi
116
+
117
+ # One-shot capture
118
+ OUTPUT=$(capture_output)
119
+
120
+ if [[ -n "$SAVE_PATH" ]]; then
121
+ echo "$OUTPUT" > "$SAVE_PATH"
122
+ echo "Saved ${LINES} lines to $SAVE_PATH"
123
+ else
124
+ echo "── Agent #${TASK_REF} (${TMUX_SESSION}) ──"
125
+ echo "$OUTPUT"
126
+ fi
package/bin/memory.sh ADDED
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env bash
2
+ # memory.sh — Agent memory: per-repo JSONL knowledge base
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ source "${SCRIPT_DIR}/../lib/common.sh"
7
+
8
+ MEMORY_BASE="$HOME/.clawforge/memory"
9
+
10
+ # ── Help ───────────────────────────────────────────────────────────────
11
+ usage() {
12
+ cat <<EOF
13
+ Usage: memory.sh [subcommand] [options]
14
+
15
+ Subcommands:
16
+ (none) Show memory stats for current repo
17
+ show List all memories for current repo
18
+ add <text> Add a memory entry
19
+ search <query> Search memories by text
20
+ forget --id <id> Remove a specific memory
21
+ clear Wipe all memories for current repo
22
+
23
+ Options:
24
+ --tags <t1,t2> Tags for the memory (with add)
25
+ --source <src> Source label (default: manual)
26
+ --repo-name <name> Override auto-detected repo name
27
+ --help Show this help
28
+ EOF
29
+ }
30
+
31
+ # ── Detect repo name ──────────────────────────────────────────────────
32
+ get_repo_name() {
33
+ local override="${REPO_NAME_OVERRIDE:-}"
34
+ if [[ -n "$override" ]]; then
35
+ echo "$override"
36
+ return
37
+ fi
38
+ # Try git remote
39
+ local remote_url
40
+ remote_url=$(git config --get remote.origin.url 2>/dev/null || true)
41
+ if [[ -n "$remote_url" ]]; then
42
+ basename "$remote_url" .git
43
+ return
44
+ fi
45
+ # Fallback: directory name
46
+ basename "$(pwd)"
47
+ }
48
+
49
+ memory_file() {
50
+ local name
51
+ name=$(get_repo_name)
52
+ echo "${MEMORY_BASE}/${name}.jsonl"
53
+ }
54
+
55
+ # ── Generate UUID ─────────────────────────────────────────────────────
56
+ gen_id() {
57
+ python3 -c 'import uuid; print(uuid.uuid4().hex[:12])' 2>/dev/null || \
58
+ head -c 12 /dev/urandom | xxd -p 2>/dev/null | head -c 12 || \
59
+ echo "$(date +%s)$$"
60
+ }
61
+
62
+ # ── Subcommands ───────────────────────────────────────────────────────
63
+
64
+ cmd_stats() {
65
+ local file
66
+ file=$(memory_file)
67
+ local name
68
+ name=$(get_repo_name)
69
+ if [[ ! -f "$file" ]] || [[ ! -s "$file" ]]; then
70
+ echo "No memories for repo: $name"
71
+ return 0
72
+ fi
73
+ local count
74
+ count=$(wc -l < "$file" | tr -d ' ')
75
+ local sources
76
+ sources=$(jq -r '.source' "$file" | sort | uniq -c | sort -rn | head -5)
77
+ echo "Memory: $name ($count entries)"
78
+ echo "File: $file"
79
+ echo ""
80
+ echo "By source:"
81
+ echo "$sources" | while read -r cnt src; do
82
+ echo " $src: $cnt"
83
+ done
84
+ }
85
+
86
+ cmd_show() {
87
+ local file
88
+ file=$(memory_file)
89
+ if [[ ! -f "$file" ]] || [[ ! -s "$file" ]]; then
90
+ echo "No memories for repo: $(get_repo_name)"
91
+ return 0
92
+ fi
93
+ jq -r '"[\(.id)] [\(.source)] \(.text)" + if (.tags | length) > 0 then " [" + (.tags | join(",")) + "]" else "" end' "$file"
94
+ }
95
+
96
+ cmd_add() {
97
+ local text="$1"
98
+ local tags_csv="${TAGS:-}"
99
+ local source="${SOURCE:-manual}"
100
+ local file
101
+ file=$(memory_file)
102
+ mkdir -p "$(dirname "$file")"
103
+
104
+ local tags_json="[]"
105
+ if [[ -n "$tags_csv" ]]; then
106
+ tags_json=$(echo "$tags_csv" | tr ',' '\n' | jq -R . | jq -s .)
107
+ fi
108
+
109
+ local entry
110
+ entry=$(jq -cn \
111
+ --arg id "$(gen_id)" \
112
+ --arg text "$text" \
113
+ --argjson tags "$tags_json" \
114
+ --arg created "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
115
+ --arg source "$source" \
116
+ '{id:$id, text:$text, tags:$tags, created:$created, source:$source}')
117
+
118
+ echo "$entry" >> "$file"
119
+ log_info "Memory added for $(get_repo_name)"
120
+ echo "$entry" | jq .
121
+ }
122
+
123
+ cmd_search() {
124
+ local query="$1"
125
+ local file
126
+ file=$(memory_file)
127
+ if [[ ! -f "$file" ]]; then
128
+ echo "No memories for repo: $(get_repo_name)"
129
+ return 0
130
+ fi
131
+ grep -i "$query" "$file" | jq -r '"[\(.id)] \(.text)"' 2>/dev/null || echo "No matches."
132
+ }
133
+
134
+ cmd_forget() {
135
+ local target_id="$1"
136
+ local file
137
+ file=$(memory_file)
138
+ if [[ ! -f "$file" ]]; then
139
+ log_error "No memory file for $(get_repo_name)"
140
+ return 1
141
+ fi
142
+ local before
143
+ before=$(wc -l < "$file" | tr -d ' ')
144
+ local tmp
145
+ tmp=$(mktemp)
146
+ jq -c "select(.id != \"$target_id\")" "$file" > "$tmp"
147
+ mv "$tmp" "$file"
148
+ local after
149
+ after=$(wc -l < "$file" | tr -d ' ')
150
+ if [[ "$before" -eq "$after" ]]; then
151
+ echo "No memory with id: $target_id"
152
+ return 1
153
+ fi
154
+ echo "Removed memory: $target_id ($before → $after entries)"
155
+ }
156
+
157
+ cmd_clear() {
158
+ local file
159
+ file=$(memory_file)
160
+ if [[ -f "$file" ]]; then
161
+ rm -f "$file"
162
+ echo "Cleared all memories for $(get_repo_name)"
163
+ else
164
+ echo "No memories to clear for $(get_repo_name)"
165
+ fi
166
+ }
167
+
168
+ # ── Parse args ────────────────────────────────────────────────────────
169
+ SUBCMD="" TEXT="" FORGET_ID="" TAGS="" SOURCE="manual" REPO_NAME_OVERRIDE=""
170
+
171
+ # First pass: extract global flags
172
+ POSITIONAL=()
173
+ while [[ $# -gt 0 ]]; do
174
+ case "$1" in
175
+ --tags) TAGS="$2"; shift 2 ;;
176
+ --source) SOURCE="$2"; shift 2 ;;
177
+ --repo-name) REPO_NAME_OVERRIDE="$2"; shift 2 ;;
178
+ --id) FORGET_ID="$2"; shift 2 ;;
179
+ --help|-h) usage; exit 0 ;;
180
+ *) POSITIONAL+=("$1"); shift ;;
181
+ esac
182
+ done
183
+ set -- "${POSITIONAL[@]+"${POSITIONAL[@]}"}"
184
+
185
+ SUBCMD="${1:-}"
186
+ shift 2>/dev/null || true
187
+
188
+ case "$SUBCMD" in
189
+ "") cmd_stats ;;
190
+ show) cmd_show ;;
191
+ add)
192
+ TEXT="${1:-}"
193
+ [[ -z "$TEXT" ]] && { log_error "Usage: memory add <text>"; exit 1; }
194
+ cmd_add "$TEXT"
195
+ ;;
196
+ search)
197
+ QUERY="${1:-}"
198
+ [[ -z "$QUERY" ]] && { log_error "Usage: memory search <query>"; exit 1; }
199
+ cmd_search "$QUERY"
200
+ ;;
201
+ forget)
202
+ [[ -z "$FORGET_ID" ]] && { log_error "Usage: memory forget --id <id>"; exit 1; }
203
+ cmd_forget "$FORGET_ID"
204
+ ;;
205
+ clear) cmd_clear ;;
206
+ *) log_error "Unknown subcommand: $SUBCMD"; usage; exit 1 ;;
207
+ esac