sessionpulse 0.1.3__tar.gz → 0.2.0__tar.gz
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.
- {sessionpulse-0.1.3/src/sessionpulse.egg-info → sessionpulse-0.2.0}/PKG-INFO +5 -2
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/README.md +4 -1
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/pyproject.toml +1 -1
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/__init__.py +1 -1
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/cli.py +94 -4
- sessionpulse-0.2.0/src/sessionpulse/followups.py +600 -0
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/server.py +36 -1
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/static/dashboard.html +153 -11
- sessionpulse-0.2.0/src/sessionpulse/todos.py +463 -0
- {sessionpulse-0.1.3 → sessionpulse-0.2.0/src/sessionpulse.egg-info}/PKG-INFO +5 -2
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/tests/test_sessionpulse.py +413 -3
- sessionpulse-0.1.3/src/sessionpulse/followups.py +0 -311
- sessionpulse-0.1.3/src/sessionpulse/todos.py +0 -244
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/LICENSE +0 -0
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/setup.cfg +0 -0
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/__main__.py +0 -0
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/config.py +0 -0
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/hook.py +0 -0
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/icons/sessionpulse-busy.svg +0 -0
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/icons/sessionpulse-idle.svg +0 -0
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/icons/sessionpulse-none.svg +0 -0
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/install.py +0 -0
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/learn.py +0 -0
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/reader.py +0 -0
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/static/brand/favicon.svg +0 -0
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/static/brand/session-pulse-idle-animated.svg +0 -0
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/static/brand/session-pulse-idle-static.svg +0 -0
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/static/brand/session-pulse-none-animated.svg +0 -0
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/static/brand/session-pulse-none-static.svg +0 -0
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/static/brand/session-pulse-working-animated.svg +0 -0
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/static/brand/session-pulse-working-static.svg +0 -0
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/status.py +0 -0
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/tray.py +0 -0
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse.egg-info/SOURCES.txt +0 -0
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse.egg-info/dependency_links.txt +0 -0
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse.egg-info/entry_points.txt +0 -0
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse.egg-info/requires.txt +0 -0
- {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sessionpulse
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A quiet instrument panel for Claude Code: live sessions, usage, and a to-do board that fills itself from your sessions. Local only.
|
|
5
5
|
Author: Vijay Shukla
|
|
6
6
|
License-Expression: MIT
|
|
@@ -85,6 +85,7 @@ It reads the files Claude Code already writes under `~/.claude`. **Nothing leave
|
|
|
85
85
|
|
|
86
86
|
- **Live sessions:** working or idle, running time, last change, tokens, model. The panel icon shows the count and turns orange while Claude is working.
|
|
87
87
|
- **Follow-ups (optional):** Claude reads each session after 20 quiet minutes and adds what was left open to your to-do list, linked back to the session, with a one-click resume command. A later session that finishes an item closes it automatically.
|
|
88
|
+
- **Why is this here?** Each follow-up keeps a short quote from the session, checked against the transcript; **Why?** shows it in context. The same work showing up in another project is flagged, with **Merge** and **Not the same**.
|
|
88
89
|
- **Board:** every to-do as a card, colour-coded by kind (Decide, Outside work, Waiting on, Checklist, Later), or by status (To do, Doing, Done). Drag and drop, project filter, search.
|
|
89
90
|
- **Your own to-dos too:** add them on the dashboard or with `sessionpulse todo add "…"`.
|
|
90
91
|
- **Usage:** input, output and cache tokens per day, split by model; active hours per day; busiest projects.
|
|
@@ -187,8 +188,10 @@ Log out and back in (on X11 you can press Alt+F2 and type `r` instead), then run
|
|
|
187
188
|
| `sessionpulse open` | Opens the dashboard in your browser, signed in (add `#board` to the address for the board) |
|
|
188
189
|
| `sessionpulse doctor` | Checks the setup and warns if Claude Code's file formats look different |
|
|
189
190
|
| `sessionpulse todo [add TEXT -p PROJECT \| done ID \| undo ID \| delete ID \| list-done]` | Your to-do list from the terminal |
|
|
191
|
+
| `sessionpulse todo export [--format md\|csv\|json] [-p PROJECT] [--status open\|all]` | Prints the list as Markdown (grouped by project), CSV or JSON; the board's Export menu downloads the same ([fields](https://github.com/vijayshuklaHL/sessionpulse/blob/main/docs/todos-json.md)) |
|
|
190
192
|
| `sessionpulse followups [--days N] [--session ID] [--now] [--again]` | Read recent sessions now and add their open follow-ups |
|
|
191
|
-
| `sessionpulse
|
|
193
|
+
| `sessionpulse followups status` | What follow-ups used (calls, tokens, cost), results per kind, and sessions that failed |
|
|
194
|
+
| `sessionpulse dedupe [-p PROJECT] [--across] [--apply]` | Finds open to-dos that are the same work reworded (`--across`: in different projects); `--apply` archives the copies |
|
|
192
195
|
| `sessionpulse snapshot` | Prints the dashboard data as JSON |
|
|
193
196
|
|
|
194
197
|
## Settings
|
|
@@ -57,6 +57,7 @@ It reads the files Claude Code already writes under `~/.claude`. **Nothing leave
|
|
|
57
57
|
|
|
58
58
|
- **Live sessions:** working or idle, running time, last change, tokens, model. The panel icon shows the count and turns orange while Claude is working.
|
|
59
59
|
- **Follow-ups (optional):** Claude reads each session after 20 quiet minutes and adds what was left open to your to-do list, linked back to the session, with a one-click resume command. A later session that finishes an item closes it automatically.
|
|
60
|
+
- **Why is this here?** Each follow-up keeps a short quote from the session, checked against the transcript; **Why?** shows it in context. The same work showing up in another project is flagged, with **Merge** and **Not the same**.
|
|
60
61
|
- **Board:** every to-do as a card, colour-coded by kind (Decide, Outside work, Waiting on, Checklist, Later), or by status (To do, Doing, Done). Drag and drop, project filter, search.
|
|
61
62
|
- **Your own to-dos too:** add them on the dashboard or with `sessionpulse todo add "…"`.
|
|
62
63
|
- **Usage:** input, output and cache tokens per day, split by model; active hours per day; busiest projects.
|
|
@@ -159,8 +160,10 @@ Log out and back in (on X11 you can press Alt+F2 and type `r` instead), then run
|
|
|
159
160
|
| `sessionpulse open` | Opens the dashboard in your browser, signed in (add `#board` to the address for the board) |
|
|
160
161
|
| `sessionpulse doctor` | Checks the setup and warns if Claude Code's file formats look different |
|
|
161
162
|
| `sessionpulse todo [add TEXT -p PROJECT \| done ID \| undo ID \| delete ID \| list-done]` | Your to-do list from the terminal |
|
|
163
|
+
| `sessionpulse todo export [--format md\|csv\|json] [-p PROJECT] [--status open\|all]` | Prints the list as Markdown (grouped by project), CSV or JSON; the board's Export menu downloads the same ([fields](https://github.com/vijayshuklaHL/sessionpulse/blob/main/docs/todos-json.md)) |
|
|
162
164
|
| `sessionpulse followups [--days N] [--session ID] [--now] [--again]` | Read recent sessions now and add their open follow-ups |
|
|
163
|
-
| `sessionpulse
|
|
165
|
+
| `sessionpulse followups status` | What follow-ups used (calls, tokens, cost), results per kind, and sessions that failed |
|
|
166
|
+
| `sessionpulse dedupe [-p PROJECT] [--across] [--apply]` | Finds open to-dos that are the same work reworded (`--across`: in different projects); `--apply` archives the copies |
|
|
164
167
|
| `sessionpulse snapshot` | Prints the dashboard data as JSON |
|
|
165
168
|
|
|
166
169
|
## Settings
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sessionpulse"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "A quiet instrument panel for Claude Code: live sessions, usage, and a to-do board that fills itself from your sessions. Local only."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -31,7 +31,11 @@ def remote_hint(cfg):
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def cmd_serve(cfg, args):
|
|
34
|
-
|
|
34
|
+
try:
|
|
35
|
+
srv = server.make_server(cfg)
|
|
36
|
+
except OSError as exc:
|
|
37
|
+
raise SystemExit(f"Can't serve on port {cfg.port}: {exc.strerror or exc}. Something else is using it "
|
|
38
|
+
f"(another `sessionpulse serve`/`tray`?); stop that, or set `port` in {config.CONFIG_FILE}.")
|
|
35
39
|
followups.start_worker(cfg)
|
|
36
40
|
print(f"Session Pulse on {cfg.url}\n" + (remote_hint(cfg) if headless() else "Open it with: sessionpulse open"),
|
|
37
41
|
flush=True)
|
|
@@ -144,16 +148,34 @@ def cmd_doctor(cfg, args):
|
|
|
144
148
|
fresh = a < status.INTERVAL * 3
|
|
145
149
|
print(f" {'ok ' if fresh else '-- '} status.json {'updated' if fresh else 'stale, last updated'} "
|
|
146
150
|
f"{int(a)}s ago{'' if fresh else ': is `sessionpulse daemon` running?'}")
|
|
151
|
+
bad = followups.failing(cfg)
|
|
152
|
+
if bad:
|
|
153
|
+
sid, e = bad[0]
|
|
154
|
+
print(f" -- {len(bad)} session(s) failed when read for follow-ups; the latest error: "
|
|
155
|
+
f"{e.get('last_error')}. See `sessionpulse followups status`.")
|
|
156
|
+
week = followups.usage_totals(cfg, 7)
|
|
157
|
+
if week["calls"]:
|
|
158
|
+
print(f" ok follow-ups used {week['calls']} model calls, {tokens(week['tokens'])} tokens in the last 7 days")
|
|
147
159
|
for w in h["warnings"]:
|
|
148
160
|
line(False, w)
|
|
149
161
|
return 0 if ok else 1
|
|
150
162
|
|
|
151
163
|
|
|
164
|
+
def tokens(n):
|
|
165
|
+
return f"{n / 1e6:.1f}M" if n >= 1e6 else f"{n / 1e3:.0f}k" if n >= 1e3 else str(n)
|
|
166
|
+
|
|
167
|
+
|
|
152
168
|
def cmd_snapshot(cfg, args):
|
|
153
169
|
json.dump(reader.snapshot(cfg), sys.stdout, indent=1)
|
|
154
170
|
|
|
155
171
|
|
|
156
172
|
def cmd_todo(cfg, args):
|
|
173
|
+
if args.action == "export":
|
|
174
|
+
try:
|
|
175
|
+
sys.stdout.write(todos.export(todos.load(cfg), args.format, args.project, args.status))
|
|
176
|
+
except ValueError as exc:
|
|
177
|
+
sys.exit(f"error: {exc}")
|
|
178
|
+
return
|
|
157
179
|
try:
|
|
158
180
|
if args.action == "add":
|
|
159
181
|
todos.apply(cfg, {"op": "add", "text": " ".join(args.text), "project": args.project})
|
|
@@ -177,7 +199,42 @@ def cmd_todo(cfg, args):
|
|
|
177
199
|
print("Nothing here." if args.action == "list-done" else "No open to-dos.")
|
|
178
200
|
|
|
179
201
|
|
|
202
|
+
def followups_status(cfg):
|
|
203
|
+
print(f"Follow-ups are {'on' if cfg.followups else 'off (add `followups = true` to config.toml)'}; "
|
|
204
|
+
f"model {cfg.followups_model}.")
|
|
205
|
+
st = followups.load_state(cfg)
|
|
206
|
+
read = [e for e in st.values() if isinstance(e, dict) and e.get("at")]
|
|
207
|
+
print(f"Sessions read: {len(read)}, adding {sum(e.get('added') or 0 for e in read)} "
|
|
208
|
+
f"and closing {sum(e.get('closed') or 0 for e in read)} to-dos.")
|
|
209
|
+
print("Model use (the cost is what the API would charge; on a subscription it counts toward your limits):")
|
|
210
|
+
for label, days in (("today", 1), ("7 days", 7), ("30 days", 30)):
|
|
211
|
+
u = followups.usage_totals(cfg, days)
|
|
212
|
+
print(f" {label:8} {u['calls']:4} calls {tokens(u['tokens']):>6} tokens "
|
|
213
|
+
f"({tokens(u['output_tokens'])} written) ${u['cost_usd']:.2f}")
|
|
214
|
+
kinds = todos.kind_stats(cfg)
|
|
215
|
+
if kinds:
|
|
216
|
+
print("By kind (dismissed items count as added; \"noise\" = dismissed as not useful):")
|
|
217
|
+
print(f" {'':10} {'added':>6} {'open':>6} {'done':>6} {'parked':>6} {'noise':>6} {'other':>6}")
|
|
218
|
+
for k in (*todos.KINDS, "unknown"):
|
|
219
|
+
if k in kinds:
|
|
220
|
+
r = kinds[k]
|
|
221
|
+
print(f" {k:10} " + " ".join(f"{r[c]:>6}" for c in ("added", "open", "done", "parked",
|
|
222
|
+
"not_useful", "dismissed")))
|
|
223
|
+
bad = followups.failing(cfg)
|
|
224
|
+
if not bad:
|
|
225
|
+
print("No session is failing.")
|
|
226
|
+
return
|
|
227
|
+
print(f"{len(bad)} session(s) failed when last read:")
|
|
228
|
+
for sid, e in bad:
|
|
229
|
+
when = f"next try after {e['next_try'][:16].replace('T', ' ')}" if e.get("next_try") else \
|
|
230
|
+
"waiting until the session grows"
|
|
231
|
+
print(f" {sid[:8]} {e.get('project') or '?'}: {e['fails']}x, {when}\n {e.get('last_error')}")
|
|
232
|
+
print("Retry one now with `sessionpulse followups --session <id>`.")
|
|
233
|
+
|
|
234
|
+
|
|
180
235
|
def cmd_followups(cfg, args):
|
|
236
|
+
if args.action == "status":
|
|
237
|
+
return followups_status(cfg)
|
|
181
238
|
if not followups.claude_bin(cfg):
|
|
182
239
|
sys.exit("error: the `claude` command was not found; set claude_command in config.toml")
|
|
183
240
|
try:
|
|
@@ -194,6 +251,8 @@ def cmd_followups(cfg, args):
|
|
|
194
251
|
def cmd_dedupe(cfg, args):
|
|
195
252
|
if not followups.claude_bin(cfg):
|
|
196
253
|
sys.exit("error: the `claude` command was not found; set claude_command in config.toml")
|
|
254
|
+
if args.across:
|
|
255
|
+
return dedupe_across(cfg, args)
|
|
197
256
|
items = todos.load(cfg)
|
|
198
257
|
counts = Counter(i.get("project") or "" for i in items if todos.is_open(i))
|
|
199
258
|
projects = [args.project] if args.project is not None else [p for p, n in counts.items() if n > 1]
|
|
@@ -223,6 +282,30 @@ def cmd_dedupe(cfg, args):
|
|
|
223
282
|
print(f"\n{total} duplicate(s) found. Run again with --apply to archive them, keeping the newest.")
|
|
224
283
|
|
|
225
284
|
|
|
285
|
+
def dedupe_across(cfg, args):
|
|
286
|
+
pairs = followups.pairs_across(cfg)
|
|
287
|
+
print(f"{len(pairs)} pair(s) across projects look alike; asking Claude which are the same work…", flush=True)
|
|
288
|
+
if not pairs:
|
|
289
|
+
return
|
|
290
|
+
try:
|
|
291
|
+
groups = followups.find_duplicates_across(cfg, pairs)
|
|
292
|
+
except (RuntimeError, OSError, ValueError, subprocess.TimeoutExpired) as exc:
|
|
293
|
+
sys.exit(f"error: {exc}")
|
|
294
|
+
by_id = {i["id"]: i for i in todos.load(cfg)}
|
|
295
|
+
for g in groups:
|
|
296
|
+
print("\n same:")
|
|
297
|
+
for x in g:
|
|
298
|
+
i = by_id.get(x, {})
|
|
299
|
+
print(f" {x} {i.get('text', '')} ({i.get('project') or 'no project'})")
|
|
300
|
+
if args.apply:
|
|
301
|
+
n = todos.merge_duplicates(cfg, groups, link=True)
|
|
302
|
+
print(f"\nArchived {n} duplicate(s), keeping the newest (or the one in progress). Their projects now count "
|
|
303
|
+
"as related, so follow-ups in one can close work listed in the other.")
|
|
304
|
+
else:
|
|
305
|
+
print(f"\n{sum(len(g) - 1 for g in groups)} duplicate(s) found. Run again with --apply to archive them, "
|
|
306
|
+
"keeping the newest, and link their projects.")
|
|
307
|
+
|
|
308
|
+
|
|
226
309
|
def main(argv=None):
|
|
227
310
|
p = argparse.ArgumentParser(prog="sessionpulse", description=__doc__)
|
|
228
311
|
p.add_argument("--version", action="version", version=f"sessionpulse {__version__}")
|
|
@@ -238,12 +321,17 @@ def main(argv=None):
|
|
|
238
321
|
sub.add_parser("open", help="open the dashboard in your browser")
|
|
239
322
|
sub.add_parser("doctor", help="check the setup and data formats")
|
|
240
323
|
sub.add_parser("snapshot", help="print the dashboard data as JSON")
|
|
241
|
-
t = sub.add_parser("todo", help="your to-do list: add, done, undo, delete, list, list-done")
|
|
324
|
+
t = sub.add_parser("todo", help="your to-do list: add, done, undo, delete, list, list-done, export")
|
|
242
325
|
t.add_argument("action", nargs="?", default="list",
|
|
243
|
-
choices=["list", "list-done", "add", "done", "undo", "delete"])
|
|
326
|
+
choices=["list", "list-done", "add", "done", "undo", "delete", "export"])
|
|
244
327
|
t.add_argument("text", nargs="*", help="the to-do text (add) or item ids (done/undo/delete)")
|
|
245
|
-
t.add_argument("-p", "--project", help="tag the new to-do with a project")
|
|
328
|
+
t.add_argument("-p", "--project", help="tag the new to-do with a project (export: only this project)")
|
|
329
|
+
t.add_argument("--format", choices=todos.FORMATS, default="md", help="export: md (default), csv or json")
|
|
330
|
+
t.add_argument("--status", choices=["open", "all"], default="open",
|
|
331
|
+
help="export: open to-dos (default) or all, including done, on hold and archived")
|
|
246
332
|
f = sub.add_parser("followups", help="pick open follow-ups out of recent sessions with Claude")
|
|
333
|
+
f.add_argument("action", nargs="?", default="run", choices=["run", "status"],
|
|
334
|
+
help="run (default), or status: model use and sessions that failed")
|
|
247
335
|
f.add_argument("--days", type=int, help="look back this many days (default: followups_days)")
|
|
248
336
|
f.add_argument("--session", help="only this session id (or its first characters)")
|
|
249
337
|
f.add_argument("--now", action="store_true", help="include sessions that are still active")
|
|
@@ -251,6 +339,8 @@ def main(argv=None):
|
|
|
251
339
|
d = sub.add_parser("dedupe", help="find open to-dos that are the same work reworded, with Claude")
|
|
252
340
|
d.add_argument("-p", "--project", help="only this project (default: every project)")
|
|
253
341
|
d.add_argument("--apply", action="store_true", help="archive the copies (default: only list them)")
|
|
342
|
+
d.add_argument("--across", action="store_true",
|
|
343
|
+
help="look for the same work listed in different projects (one model call per 40 likely pairs)")
|
|
254
344
|
sub.add_parser("hook", help=argparse.SUPPRESS)
|
|
255
345
|
args = p.parse_args(argv)
|
|
256
346
|
cfg = config.load()
|