claude-code-tracker 1.2.4 → 1.4.0-beta.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 +26 -2
- package/bin/claude-tracker-cost.js +20 -0
- package/bin/claude-tracker-setup +10 -0
- package/install.js +21 -0
- package/install.sh +26 -2
- package/package.json +7 -3
- package/skills/view-tracking/SKILL.md +54 -0
- package/src/__pycache__/cost.cpython-312.pyc +0 -0
- package/src/__pycache__/parse_compactions.cpython-312.pyc +0 -0
- package/src/__pycache__/parse_friction.cpython-312.pyc +0 -0
- package/src/__pycache__/parse_skills.cpython-312.pyc +0 -0
- package/src/__pycache__/platform_utils.cpython-312.pyc +0 -0
- package/src/__pycache__/storage.cpython-312.pyc +0 -0
- package/src/__pycache__/write-agent.cpython-312.pyc +0 -0
- package/src/__pycache__/write-turns.cpython-312.pyc +0 -0
- package/src/backfill.py +47 -52
- package/src/cost-summary.py +57 -11
- package/src/cost.py +7 -0
- package/src/export-json.py +27 -0
- package/src/generate-charts.py +691 -12
- package/src/init-templates.py +26 -0
- package/src/init-templates.sh +3 -3
- package/src/parse_compactions.py +112 -0
- package/src/parse_friction.py +277 -0
- package/src/parse_skills.py +133 -0
- package/src/patch-durations.py +14 -114
- package/src/platform_utils.py +36 -0
- package/src/stop-hook.js +26 -0
- package/src/stop-hook.sh +27 -155
- package/src/storage.py +538 -0
- package/src/subagent-stop-hook.sh +37 -0
- package/src/update-prompts-index.py +177 -20
- package/src/write-agent.py +113 -0
- package/src/write-turns.py +130 -0
- package/uninstall.js +20 -0
package/src/patch-durations.py
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
Patch duration_seconds for per-turn entries that have duration 0
|
|
4
|
-
and migrate old single-entry-per-session entries to per-turn format.
|
|
3
|
+
Patch duration_seconds for per-turn entries that have duration 0.
|
|
5
4
|
|
|
6
5
|
Usage:
|
|
7
6
|
python3 patch-durations.py <project_root>
|
|
8
7
|
"""
|
|
9
|
-
import sys, json, os
|
|
8
|
+
import sys, json, os
|
|
9
|
+
|
|
10
|
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
11
|
+
sys.path.insert(0, SCRIPT_DIR)
|
|
12
|
+
import storage
|
|
13
|
+
|
|
10
14
|
from datetime import datetime
|
|
11
15
|
|
|
12
16
|
project_root = os.path.abspath(sys.argv[1])
|
|
13
17
|
tracking_dir = os.path.join(project_root, ".claude", "tracking")
|
|
14
|
-
tokens_file = os.path.join(tracking_dir, "tokens.json")
|
|
15
18
|
|
|
16
19
|
slug = project_root.replace("/", "-")
|
|
17
20
|
transcripts_dir = os.path.expanduser("~/.claude/projects/" + slug)
|
|
18
|
-
project_name = os.path.basename(project_root)
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
data = json.load(f)
|
|
22
|
+
data = storage.get_all_turns(tracking_dir)
|
|
22
23
|
|
|
23
24
|
def parse_transcript(jf):
|
|
24
25
|
msgs = []
|
|
@@ -26,7 +27,7 @@ def parse_transcript(jf):
|
|
|
26
27
|
model = "unknown"
|
|
27
28
|
first_ts = None
|
|
28
29
|
try:
|
|
29
|
-
with open(jf) as f:
|
|
30
|
+
with open(jf, encoding='utf-8') as f:
|
|
30
31
|
for line in f:
|
|
31
32
|
try:
|
|
32
33
|
obj = json.loads(line)
|
|
@@ -52,15 +53,9 @@ def parse_transcript(jf):
|
|
|
52
53
|
pass
|
|
53
54
|
return msgs, usages, model, first_ts
|
|
54
55
|
|
|
55
|
-
# Separate old-format (no turn_index) from new-format entries
|
|
56
|
-
old_entries = [e for e in data if "turn_index" not in e]
|
|
57
|
-
new_entries = [e for e in data if "turn_index" in e]
|
|
58
|
-
|
|
59
|
-
# For new-format entries with duration 0, patch from transcript
|
|
60
|
-
existing_keys = {(e.get("session_id"), e.get("turn_index")): i for i, e in enumerate(new_entries)}
|
|
61
56
|
patched = 0
|
|
62
57
|
|
|
63
|
-
for entry in
|
|
58
|
+
for entry in data:
|
|
64
59
|
if entry.get("duration_seconds", 0) > 0:
|
|
65
60
|
continue
|
|
66
61
|
sid = entry.get("session_id")
|
|
@@ -85,7 +80,7 @@ for entry in new_entries:
|
|
|
85
80
|
t1 = datetime.fromisoformat(msgs[j][1].replace("Z", "+00:00"))
|
|
86
81
|
duration = max(0, int((t1 - t0).total_seconds()))
|
|
87
82
|
if duration > 0:
|
|
88
|
-
|
|
83
|
+
storage.patch_turn_duration(tracking_dir, sid, turn_index, duration)
|
|
89
84
|
patched += 1
|
|
90
85
|
print(f" patched {sid[:8]}#{turn_index} {duration}s")
|
|
91
86
|
except Exception:
|
|
@@ -99,103 +94,8 @@ for entry in new_entries:
|
|
|
99
94
|
else:
|
|
100
95
|
i += 1
|
|
101
96
|
|
|
102
|
-
|
|
103
|
-
migrated_sessions = 0
|
|
104
|
-
new_turn_entries = []
|
|
105
|
-
for old_entry in old_entries:
|
|
106
|
-
sid = old_entry.get("session_id")
|
|
107
|
-
if not sid:
|
|
108
|
-
continue
|
|
109
|
-
jf = os.path.join(transcripts_dir, sid + ".jsonl")
|
|
110
|
-
if not os.path.exists(jf):
|
|
111
|
-
# Keep old entry as-is if we can't reprocess
|
|
112
|
-
new_entries.append(old_entry)
|
|
113
|
-
continue
|
|
114
|
-
|
|
115
|
-
msgs, usages, model, first_ts = parse_transcript(jf)
|
|
116
|
-
|
|
117
|
-
turn_index = 0
|
|
118
|
-
usage_index = 0
|
|
119
|
-
i = 0
|
|
120
|
-
session_date = old_entry.get("date")
|
|
121
|
-
|
|
122
|
-
while i < len(msgs):
|
|
123
|
-
if msgs[i][0] == "user":
|
|
124
|
-
user_ts = msgs[i][1]
|
|
125
|
-
j = i + 1
|
|
126
|
-
while j < len(msgs) and msgs[j][0] != "assistant":
|
|
127
|
-
j += 1
|
|
128
|
-
if j < len(msgs):
|
|
129
|
-
asst_ts = msgs[j][1]
|
|
130
|
-
usage = {}
|
|
131
|
-
if usage_index < len(usages):
|
|
132
|
-
usage = usages[usage_index]
|
|
133
|
-
usage_index += 1
|
|
134
|
-
|
|
135
|
-
inp = usage.get("input_tokens", 0)
|
|
136
|
-
out = usage.get("output_tokens", 0)
|
|
137
|
-
cache_create = usage.get("cache_creation_input_tokens", 0)
|
|
138
|
-
cache_read = usage.get("cache_read_input_tokens", 0)
|
|
139
|
-
total = inp + cache_create + cache_read + out
|
|
140
|
-
|
|
141
|
-
if total > 0:
|
|
142
|
-
duration = 0
|
|
143
|
-
try:
|
|
144
|
-
t0 = datetime.fromisoformat(user_ts.replace("Z", "+00:00"))
|
|
145
|
-
t1 = datetime.fromisoformat(asst_ts.replace("Z", "+00:00"))
|
|
146
|
-
duration = max(0, int((t1 - t0).total_seconds()))
|
|
147
|
-
except Exception:
|
|
148
|
-
pass
|
|
149
|
-
|
|
150
|
-
if "opus" in model:
|
|
151
|
-
cost = inp * 15 / 1e6 + cache_create * 18.75 / 1e6 + cache_read * 1.50 / 1e6 + out * 75 / 1e6
|
|
152
|
-
else:
|
|
153
|
-
cost = inp * 3 / 1e6 + cache_create * 3.75 / 1e6 + cache_read * 0.30 / 1e6 + out * 15 / 1e6
|
|
154
|
-
|
|
155
|
-
try:
|
|
156
|
-
turn_ts = datetime.fromisoformat(user_ts.replace("Z", "+00:00")).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
157
|
-
turn_date = datetime.fromisoformat(user_ts.replace("Z", "+00:00")).strftime("%Y-%m-%d")
|
|
158
|
-
except Exception:
|
|
159
|
-
turn_ts = user_ts
|
|
160
|
-
turn_date = session_date
|
|
161
|
-
|
|
162
|
-
new_turn_entries.append({
|
|
163
|
-
"date": turn_date,
|
|
164
|
-
"project": project_name,
|
|
165
|
-
"session_id": sid,
|
|
166
|
-
"turn_index": turn_index,
|
|
167
|
-
"turn_timestamp": turn_ts,
|
|
168
|
-
"input_tokens": inp,
|
|
169
|
-
"cache_creation_tokens": cache_create,
|
|
170
|
-
"cache_read_tokens": cache_read,
|
|
171
|
-
"output_tokens": out,
|
|
172
|
-
"total_tokens": total,
|
|
173
|
-
"estimated_cost_usd": round(cost, 4),
|
|
174
|
-
"model": model,
|
|
175
|
-
"duration_seconds": duration,
|
|
176
|
-
})
|
|
177
|
-
turn_index += 1
|
|
178
|
-
i = j + 1
|
|
179
|
-
else:
|
|
180
|
-
i += 1
|
|
181
|
-
else:
|
|
182
|
-
i += 1
|
|
183
|
-
|
|
184
|
-
if turn_index > 0:
|
|
185
|
-
migrated_sessions += 1
|
|
186
|
-
print(f" migrated {sid[:8]} {turn_index} turn(s)")
|
|
187
|
-
else:
|
|
188
|
-
new_entries.append(old_entry)
|
|
189
|
-
|
|
190
|
-
new_entries.extend(new_turn_entries)
|
|
191
|
-
new_entries.sort(key=lambda x: (x.get("date", ""), x.get("session_id", ""), x.get("turn_index", 0)))
|
|
192
|
-
|
|
193
|
-
if patched > 0 or migrated_sessions > 0:
|
|
194
|
-
with open(tokens_file, "w") as f:
|
|
195
|
-
json.dump(new_entries, f, indent=2)
|
|
196
|
-
f.write("\n")
|
|
197
|
-
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
97
|
+
if patched > 0:
|
|
198
98
|
charts_html = os.path.join(tracking_dir, "charts.html")
|
|
199
|
-
os.system(f'python3 "{
|
|
99
|
+
os.system(f'python3 "{SCRIPT_DIR}/generate-charts.py" "{tracking_dir}" "{charts_html}" 2>/dev/null')
|
|
200
100
|
|
|
201
|
-
print(f"{patched} turn(s) patched
|
|
101
|
+
print(f"{patched} turn(s) patched.")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Cross-platform utility helpers for claude-tracker."""
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
def get_transcripts_dir():
|
|
7
|
+
"""Return the Claude transcripts directory for the current platform."""
|
|
8
|
+
if sys.platform == 'win32':
|
|
9
|
+
appdata = os.environ.get('APPDATA', '')
|
|
10
|
+
return os.path.join(appdata, 'Claude', 'claude_code', 'transcripts')
|
|
11
|
+
elif sys.platform == 'darwin':
|
|
12
|
+
home = os.path.expanduser('~')
|
|
13
|
+
return os.path.join(home, 'Library', 'Application Support', 'Claude', 'claude_code', 'transcripts')
|
|
14
|
+
else:
|
|
15
|
+
home = os.path.expanduser('~')
|
|
16
|
+
return os.path.join(home, '.config', 'Claude', 'claude_code', 'transcripts')
|
|
17
|
+
|
|
18
|
+
def slugify_path(path):
|
|
19
|
+
"""Convert an absolute path to a slug suitable for use as a directory name.
|
|
20
|
+
Handles Windows drive letters and backslashes."""
|
|
21
|
+
# Normalize separators
|
|
22
|
+
slug = path.replace('\\', '/').replace('/', '-')
|
|
23
|
+
# Remove drive letter colon on Windows (e.g. C: -> C)
|
|
24
|
+
slug = slug.replace(':', '')
|
|
25
|
+
# Strip leading/trailing dashes
|
|
26
|
+
slug = slug.strip('-')
|
|
27
|
+
return slug
|
|
28
|
+
|
|
29
|
+
def open_file(path):
|
|
30
|
+
"""Open a file with the default system application."""
|
|
31
|
+
if sys.platform == 'win32':
|
|
32
|
+
os.startfile(path)
|
|
33
|
+
elif sys.platform == 'darwin':
|
|
34
|
+
subprocess.run(['open', path])
|
|
35
|
+
else:
|
|
36
|
+
subprocess.run(['xdg-open', path])
|
package/src/stop-hook.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
|
|
7
|
+
const scriptDir = path.dirname(path.resolve(__filename));
|
|
8
|
+
const bashScript = path.join(scriptDir, 'stop-hook.sh');
|
|
9
|
+
|
|
10
|
+
// On Windows, run via bash (Git Bash / WSL); on Unix, run directly
|
|
11
|
+
const input = fs.readFileSync(process.stdin.fd, 'utf8');
|
|
12
|
+
|
|
13
|
+
if (process.platform === 'win32') {
|
|
14
|
+
const result = spawnSync('bash', [bashScript], {
|
|
15
|
+
input,
|
|
16
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
17
|
+
shell: false,
|
|
18
|
+
});
|
|
19
|
+
process.exit(result.status || 0);
|
|
20
|
+
} else {
|
|
21
|
+
const result = spawnSync('bash', [bashScript], {
|
|
22
|
+
input,
|
|
23
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
24
|
+
});
|
|
25
|
+
process.exit(result.status || 0);
|
|
26
|
+
}
|
package/src/stop-hook.sh
CHANGED
|
@@ -50,161 +50,33 @@ if [[ ! -d "$TRACKING_DIR" ]]; then
|
|
|
50
50
|
python3 "$SCRIPT_DIR/backfill.py" "$PROJECT_ROOT" 2>/dev/null || true
|
|
51
51
|
fi
|
|
52
52
|
|
|
53
|
-
# Parse token usage from JSONL
|
|
54
|
-
python3 - "$TRANSCRIPT" "$TRACKING_DIR
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
with open(transcript_path) as f:
|
|
68
|
-
for line in f:
|
|
69
|
-
try:
|
|
70
|
-
obj = json.loads(line)
|
|
71
|
-
t = obj.get('type')
|
|
72
|
-
ts = obj.get('timestamp')
|
|
73
|
-
if t == 'user' and not obj.get('isSidechain') and ts:
|
|
74
|
-
msgs.append(('user', ts))
|
|
75
|
-
elif t == 'assistant' and ts:
|
|
76
|
-
msgs.append(('assistant', ts))
|
|
77
|
-
msg = obj.get('message', {})
|
|
78
|
-
if isinstance(msg, dict) and msg.get('role') == 'assistant':
|
|
79
|
-
usage = msg.get('usage', {})
|
|
80
|
-
if usage:
|
|
81
|
-
usages.append(usage)
|
|
82
|
-
m = msg.get('model', '')
|
|
83
|
-
if m:
|
|
84
|
-
model = m
|
|
85
|
-
except:
|
|
86
|
-
pass
|
|
87
|
-
|
|
88
|
-
# Build per-turn entries
|
|
89
|
-
turn_entries = []
|
|
90
|
-
turn_index = 0
|
|
91
|
-
usage_index = 0
|
|
92
|
-
i = 0
|
|
93
|
-
while i < len(msgs):
|
|
94
|
-
if msgs[i][0] == 'user':
|
|
95
|
-
user_ts = msgs[i][1]
|
|
96
|
-
j = i + 1
|
|
97
|
-
while j < len(msgs) and msgs[j][0] != 'assistant':
|
|
98
|
-
j += 1
|
|
99
|
-
if j < len(msgs):
|
|
100
|
-
asst_ts = msgs[j][1]
|
|
101
|
-
usage = {}
|
|
102
|
-
if usage_index < len(usages):
|
|
103
|
-
usage = usages[usage_index]
|
|
104
|
-
usage_index += 1
|
|
105
|
-
|
|
106
|
-
inp = usage.get('input_tokens', 0)
|
|
107
|
-
out = usage.get('output_tokens', 0)
|
|
108
|
-
cache_create = usage.get('cache_creation_input_tokens', 0)
|
|
109
|
-
cache_read = usage.get('cache_read_input_tokens', 0)
|
|
110
|
-
total = inp + cache_create + cache_read + out
|
|
111
|
-
|
|
112
|
-
if total > 0:
|
|
113
|
-
duration = 0
|
|
114
|
-
try:
|
|
115
|
-
t0 = datetime.fromisoformat(user_ts.replace('Z', '+00:00'))
|
|
116
|
-
t1 = datetime.fromisoformat(asst_ts.replace('Z', '+00:00'))
|
|
117
|
-
duration = max(0, int((t1 - t0).total_seconds()))
|
|
118
|
-
except:
|
|
119
|
-
pass
|
|
120
|
-
|
|
121
|
-
if 'opus' in model:
|
|
122
|
-
cost = inp * 15 / 1e6 + cache_create * 18.75 / 1e6 + cache_read * 1.50 / 1e6 + out * 75 / 1e6
|
|
123
|
-
else:
|
|
124
|
-
cost = inp * 3 / 1e6 + cache_create * 3.75 / 1e6 + cache_read * 0.30 / 1e6 + out * 15 / 1e6
|
|
125
|
-
|
|
126
|
-
try:
|
|
127
|
-
turn_ts = datetime.fromisoformat(user_ts.replace('Z', '+00:00')).strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
128
|
-
turn_date = datetime.fromisoformat(user_ts.replace('Z', '+00:00')).strftime('%Y-%m-%d')
|
|
129
|
-
except:
|
|
130
|
-
turn_ts = user_ts
|
|
131
|
-
turn_date = date.today().isoformat()
|
|
132
|
-
|
|
133
|
-
turn_entries.append({
|
|
134
|
-
'date': turn_date,
|
|
135
|
-
'project': project_name,
|
|
136
|
-
'session_id': session_id,
|
|
137
|
-
'turn_index': turn_index,
|
|
138
|
-
'turn_timestamp': turn_ts,
|
|
139
|
-
'input_tokens': inp,
|
|
140
|
-
'cache_creation_tokens': cache_create,
|
|
141
|
-
'cache_read_tokens': cache_read,
|
|
142
|
-
'output_tokens': out,
|
|
143
|
-
'total_tokens': total,
|
|
144
|
-
'estimated_cost_usd': round(cost, 4),
|
|
145
|
-
'model': model,
|
|
146
|
-
'duration_seconds': duration,
|
|
147
|
-
})
|
|
148
|
-
turn_index += 1
|
|
149
|
-
i = j + 1
|
|
150
|
-
else:
|
|
151
|
-
i += 1
|
|
152
|
-
else:
|
|
153
|
-
i += 1
|
|
154
|
-
|
|
155
|
-
if not turn_entries:
|
|
156
|
-
sys.exit(0)
|
|
157
|
-
|
|
158
|
-
# Load existing data
|
|
159
|
-
data = []
|
|
160
|
-
if os.path.exists(tokens_file):
|
|
161
|
-
try:
|
|
162
|
-
with open(tokens_file) as f:
|
|
163
|
-
data = json.load(f)
|
|
164
|
-
except:
|
|
165
|
-
data = []
|
|
166
|
-
|
|
167
|
-
# Build index of existing (session_id, turn_index) -> position
|
|
168
|
-
existing_idx = {}
|
|
169
|
-
for pos, e in enumerate(data):
|
|
170
|
-
key = (e.get('session_id'), e.get('turn_index'))
|
|
171
|
-
existing_idx[key] = pos
|
|
172
|
-
|
|
173
|
-
# Check if anything actually changed
|
|
174
|
-
changed = False
|
|
175
|
-
for entry in turn_entries:
|
|
176
|
-
key = (entry['session_id'], entry['turn_index'])
|
|
177
|
-
if key not in existing_idx:
|
|
178
|
-
changed = True
|
|
179
|
-
break
|
|
180
|
-
existing = data[existing_idx[key]]
|
|
181
|
-
if (existing.get('total_tokens') != entry['total_tokens'] or
|
|
182
|
-
existing.get('output_tokens') != entry['output_tokens']):
|
|
183
|
-
changed = True
|
|
184
|
-
break
|
|
185
|
-
|
|
186
|
-
if not changed:
|
|
187
|
-
sys.exit(0)
|
|
188
|
-
|
|
189
|
-
# Upsert: update existing entries or append new ones
|
|
190
|
-
for entry in turn_entries:
|
|
191
|
-
key = (entry['session_id'], entry['turn_index'])
|
|
192
|
-
if key in existing_idx:
|
|
193
|
-
data[existing_idx[key]] = entry
|
|
194
|
-
else:
|
|
195
|
-
data.append(entry)
|
|
196
|
-
existing_idx[key] = len(data) - 1
|
|
197
|
-
|
|
198
|
-
# Sort by (date, session_id, turn_index)
|
|
199
|
-
data.sort(key=lambda x: (x.get('date', ''), x.get('session_id', ''), x.get('turn_index', 0)))
|
|
200
|
-
|
|
201
|
-
with open(tokens_file, 'w') as f:
|
|
202
|
-
json.dump(data, f, indent=2)
|
|
203
|
-
f.write('\n')
|
|
204
|
-
PYEOF
|
|
53
|
+
# Parse token usage from JSONL and write to SQLite
|
|
54
|
+
python3 "$SCRIPT_DIR/write-turns.py" "$TRANSCRIPT" "$TRACKING_DIR" "$SESSION_ID" "$(basename "$PROJECT_ROOT")"
|
|
55
|
+
|
|
56
|
+
# Parse friction events from JSONL
|
|
57
|
+
python3 "$SCRIPT_DIR/parse_friction.py" "$TRANSCRIPT" "$TRACKING_DIR" \
|
|
58
|
+
"$SESSION_ID" "$(basename "$PROJECT_ROOT")" "main" 2>/dev/null || true
|
|
59
|
+
|
|
60
|
+
# Parse skill invocations from JSONL
|
|
61
|
+
python3 "$SCRIPT_DIR/parse_skills.py" "$TRANSCRIPT" "$TRACKING_DIR" \
|
|
62
|
+
"$SESSION_ID" "$(basename "$PROJECT_ROOT")" 2>/dev/null || true
|
|
63
|
+
|
|
64
|
+
# Parse context compaction events from JSONL
|
|
65
|
+
python3 "$SCRIPT_DIR/parse_compactions.py" "$TRANSCRIPT" "$TRACKING_DIR" \
|
|
66
|
+
"$SESSION_ID" "$(basename "$PROJECT_ROOT")" 2>/dev/null || true
|
|
205
67
|
|
|
206
68
|
# Regenerate charts
|
|
207
|
-
python3 "$SCRIPT_DIR/generate-charts.py" "$TRACKING_DIR
|
|
69
|
+
python3 "$SCRIPT_DIR/generate-charts.py" "$TRACKING_DIR" "$TRACKING_DIR/charts.html" 2>/dev/null || true
|
|
70
|
+
|
|
71
|
+
# Regenerate key-prompts index + shadow to OpenMemory
|
|
72
|
+
OM_DB="$HOME/.claude/.claude/openmemory.sqlite"
|
|
73
|
+
LEARNINGS="$HOME/.claude/tool-learnings.md"
|
|
74
|
+
OM_ARGS=""
|
|
75
|
+
if [[ -f "$OM_DB" ]]; then
|
|
76
|
+
OM_ARGS="--om-db $OM_DB"
|
|
77
|
+
if [[ -f "$LEARNINGS" ]]; then
|
|
78
|
+
OM_ARGS="$OM_ARGS --learnings $LEARNINGS"
|
|
79
|
+
fi
|
|
80
|
+
fi
|
|
81
|
+
python3 "$SCRIPT_DIR/update-prompts-index.py" "$TRACKING_DIR" $OM_ARGS 2>/dev/null || true
|
|
208
82
|
|
|
209
|
-
# Regenerate key-prompts index
|
|
210
|
-
python3 "$SCRIPT_DIR/update-prompts-index.py" "$TRACKING_DIR" 2>/dev/null || true
|