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.
Files changed (38) hide show
  1. {sessionpulse-0.1.3/src/sessionpulse.egg-info → sessionpulse-0.2.0}/PKG-INFO +5 -2
  2. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/README.md +4 -1
  3. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/pyproject.toml +1 -1
  4. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/__init__.py +1 -1
  5. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/cli.py +94 -4
  6. sessionpulse-0.2.0/src/sessionpulse/followups.py +600 -0
  7. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/server.py +36 -1
  8. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/static/dashboard.html +153 -11
  9. sessionpulse-0.2.0/src/sessionpulse/todos.py +463 -0
  10. {sessionpulse-0.1.3 → sessionpulse-0.2.0/src/sessionpulse.egg-info}/PKG-INFO +5 -2
  11. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/tests/test_sessionpulse.py +413 -3
  12. sessionpulse-0.1.3/src/sessionpulse/followups.py +0 -311
  13. sessionpulse-0.1.3/src/sessionpulse/todos.py +0 -244
  14. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/LICENSE +0 -0
  15. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/setup.cfg +0 -0
  16. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/__main__.py +0 -0
  17. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/config.py +0 -0
  18. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/hook.py +0 -0
  19. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/icons/sessionpulse-busy.svg +0 -0
  20. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/icons/sessionpulse-idle.svg +0 -0
  21. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/icons/sessionpulse-none.svg +0 -0
  22. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/install.py +0 -0
  23. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/learn.py +0 -0
  24. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/reader.py +0 -0
  25. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/static/brand/favicon.svg +0 -0
  26. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/static/brand/session-pulse-idle-animated.svg +0 -0
  27. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/static/brand/session-pulse-idle-static.svg +0 -0
  28. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/static/brand/session-pulse-none-animated.svg +0 -0
  29. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/static/brand/session-pulse-none-static.svg +0 -0
  30. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/static/brand/session-pulse-working-animated.svg +0 -0
  31. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/static/brand/session-pulse-working-static.svg +0 -0
  32. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/status.py +0 -0
  33. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse/tray.py +0 -0
  34. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse.egg-info/SOURCES.txt +0 -0
  35. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse.egg-info/dependency_links.txt +0 -0
  36. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse.egg-info/entry_points.txt +0 -0
  37. {sessionpulse-0.1.3 → sessionpulse-0.2.0}/src/sessionpulse.egg-info/requires.txt +0 -0
  38. {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.1.3
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 dedupe [-p PROJECT] [--apply]` | Finds open to-dos that are the same work reworded; `--apply` archives the copies |
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 dedupe [-p PROJECT] [--apply]` | Finds open to-dos that are the same work reworded; `--apply` archives the copies |
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.1.3"
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"
@@ -1,3 +1,3 @@
1
1
  """Session Pulse: a local dashboard and panel icon for Claude Code sessions."""
2
2
 
3
- __version__ = "0.1.3"
3
+ __version__ = "0.2.0"
@@ -31,7 +31,11 @@ def remote_hint(cfg):
31
31
 
32
32
 
33
33
  def cmd_serve(cfg, args):
34
- srv = server.make_server(cfg)
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()