elliot-stack 1.0.40 → 1.0.41

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elliot-stack",
3
- "version": "1.0.40",
3
+ "version": "1.0.41",
4
4
  "description": "Elliot's skill stack for Claude Code — install via npx elliot-stack@latest",
5
5
  "bin": {
6
6
  "elliot-stack": "bin/install.cjs"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: estack-read-claude-session-history
3
- version: 1.3.0
3
+ version: 1.3.1
4
4
  description: >-
5
5
  (read-claude-session-history) Invoke for ANY task involving Claude Code
6
6
  session history, transcripts, or .jsonl files - this is the only way to read,
@@ -80,6 +80,13 @@ output. If you need a different zone, pass `--tz` (IANA name like
80
80
  `America/New_York`, `UTC`, or an offset like `-4`) — never convert manually.
81
81
  `--since/--until/--date` specs are interpreted in that same display timezone.
82
82
 
83
+ **Report times to the user in 12-hour format unless they ask otherwise.** The
84
+ report modes (`session-report`, `engagement`, `timeline`) already emit each clock
85
+ time as `7:00pm (19:00)` — 12-hour with the 24-hour value in parens — so quoting
86
+ them directly satisfies this. When you paraphrase or summarize a time instead of
87
+ quoting raw output, keep the 12-hour form (the parenthetical 24-hour is optional
88
+ in prose). Switch to 24-hour only if the user requests it.
89
+
83
90
  ## Decision tree
84
91
 
85
92
  ```
@@ -231,7 +238,7 @@ When the user asks a natural-language "what did I do" / "review my day" / "break
231
238
  - **One to two sentences per session** describing what they did and why — synthesized from the `intent` + `last` + files, not a raw dump of either. The mode hands you the inputs; you write the sentence.
232
239
  - **Show both clocks and name the overlap.** `active` is deduped attention (parallel chats never double-counted); `ran`/elapsed is the session's own first→last span and *will* overlap others. Say so once — users work on several things at once and want the overlap reflected in the span but removed from active minutes.
233
240
  - **Counts are clean already.** `you N msgs` is real typed prompts (tool-results and hook/skill injections excluded); `assistant N msgs` is text replies (tool-only turns excluded). Do NOT hand-count raw `type:user`/`type:assistant` entries from the .jsonl — that over-counts both (tool-result envelopes inflate "user"; multi-block turns inflate "assistant"). Trust the mode's numbers.
234
- - **24-hour time** as the CLI emits it; convert only if the user asks.
241
+ - **12-hour time** as the report modes emit it (`7:00pm (19:00)` — 12-hour with 24-hour in parens); keep the 12-hour form in prose, and switch to 24-hour only if the user asks.
235
242
  - **Scope to a sub-window with `--since/--until`** (omit `--date` — `--date` overrides `--since`). The metrics are windowed to the range, so "from 7pm" counts and active-time reflect only that slice.
236
243
 
237
244
  For a normal day (16–20 sessions) `session-report` text output stays well within the read budget; prefer it over `--mode list --format json`, which dumps full per-session arrays and can overflow into a persisted-file round-trip.
@@ -18,6 +18,7 @@ Global flag notes:
18
18
  - `--since <spec>` / `--until <spec>` — the time window is half-open `[since, until)`: `since` is inclusive, `until` is exclusive, so an event stamped exactly at `--until` is not included. This holds for every message-level mode (`search`, `timeline`, `engagement`, `tool-usage`). Session-level modes (`list`, `journal`, `count`) instead filter whole sessions by file mtime, so a session last written at or before `--until` is shown.
19
19
  - `--project <name>` — case-insensitive substring filter on project directory names (encoded or decoded form). Applies to `list`, `journal`, `search`, `count`, `find`, `timeline`, `engagement`, `tool-usage`. Exit 1 when nothing matches.
20
20
  - `--tz <spec>` — display timezone: IANA name (`America/New_York`), `UTC`, or fixed offset (`+5`, `-4`, `+05:30`, `UTC-4`). Default is system local time. All displayed timestamps AND `--since/--until/--date` interpretation use this zone.
21
+ - **Clock format:** the report modes (`session-report`, `engagement`, `timeline`) render every time-of-day as 12-hour with the 24-hour value in parens — `7:00pm (19:00)` — and date-prefix it (`2026-06-19 7:00pm (19:00)`) when the window spans more than one day; their headers carry a `12h (24h)` label. Forensic modes (`changelog`, `tool-calls`) keep `HH:MM:SS` for density. JSON output is always ISO-8601 (unaffected by the 12-hour rendering).
21
22
  - `--format json` (alias `--json`) — structured output on every mode except the legacy `--list`/`--list-subagents` aliases. Shapes per mode are listed below.
22
23
 
23
24
  Legacy flags are preserved unchanged:
@@ -1036,6 +1036,21 @@ def _fmt_dur(td: timedelta) -> str:
1036
1036
  return f"{h}h{m:02d}m" if h else f"{m}m"
1037
1037
 
1038
1038
 
1039
+ def _fmt_tod(dt: datetime) -> str:
1040
+ """Time-of-day as 12-hour with 24-hour in parens: '7:00pm (19:00)'.
1041
+
1042
+ Computed by hand (not strftime %-I/%#I) so it's identical on every platform.
1043
+ """
1044
+ h12 = dt.hour % 12 or 12
1045
+ ampm = "am" if dt.hour < 12 else "pm"
1046
+ return f"{h12}:{dt.minute:02d}{ampm} ({dt.hour:02d}:{dt.minute:02d})"
1047
+
1048
+
1049
+ def _fmt_clock(dt: datetime, with_date: bool) -> str:
1050
+ """A report clock value — `_fmt_tod`, date-prefixed when the window spans days."""
1051
+ return f"{dt:%Y-%m-%d} {_fmt_tod(dt)}" if with_date else _fmt_tod(dt)
1052
+
1053
+
1039
1054
  _GAP_RE = re.compile(r"^(\d+)\s*(m|h)?$", re.IGNORECASE)
1040
1055
 
1041
1056
 
@@ -1112,10 +1127,9 @@ def render_timeline(data: dict, tz_label: str) -> str:
1112
1127
  since, until = data["since"], data["until"]
1113
1128
  blocks, sessions = data["blocks"], data["sessions"]
1114
1129
  multi_day = (until - since) > timedelta(days=1)
1115
- tfmt = "%Y-%m-%d %H:%M" if multi_day else "%H:%M"
1116
1130
  head = (
1117
- f"=== Timeline {since:%Y-%m-%d %H:%M} → {until:%Y-%m-%d %H:%M} "
1118
- f"(times: {tz_label}, gap={data['gap_minutes']}m) ==="
1131
+ f"=== Timeline {_fmt_clock(since, True)} → {_fmt_clock(until, True)} "
1132
+ f"(times: {tz_label} 12h (24h), gap={data['gap_minutes']}m) ==="
1119
1133
  )
1120
1134
  if not blocks:
1121
1135
  return head + "\n\n(no activity in range)"
@@ -1125,7 +1139,7 @@ def render_timeline(data: dict, tz_label: str) -> str:
1125
1139
  if prev_end is not None:
1126
1140
  out.append(f" ── idle {_fmt_dur(b['start'] - prev_end)} ──")
1127
1141
  dur = b["end"] - b["start"]
1128
- out.append(f"{b['start'].strftime(tfmt)}–{b['end'].strftime('%H:%M')} ({_fmt_dur(dur)})")
1142
+ out.append(f"{_fmt_clock(b['start'], multi_day)}–{_fmt_tod(b['end'])} ({_fmt_dur(dur)})")
1129
1143
  for f, n in sorted(b["counts"].items(), key=lambda x: -x[1]):
1130
1144
  out.append(f" · {_session_label(sessions[f])} — {n} msgs")
1131
1145
  prev_end = b["end"]
@@ -1135,7 +1149,7 @@ def render_timeline(data: dict, tz_label: str) -> str:
1135
1149
  # no claim about user attention time. For that, use --mode engagement.
1136
1150
  out.append(
1137
1151
  f"Total: {len(blocks)} block(s) across a {_fmt_dur(span)} span "
1138
- f"({blocks[0]['start'].strftime(tfmt)}–{blocks[-1]['end'].strftime('%H:%M')}), "
1152
+ f"({_fmt_clock(blocks[0]['start'], multi_day)}–{_fmt_tod(blocks[-1]['end'])}), "
1139
1153
  f"{len(sessions)} session(s)"
1140
1154
  )
1141
1155
  return "\n".join(out)
@@ -1376,10 +1390,9 @@ def render_engagement(data: dict, tz_label: str) -> str:
1376
1390
  since, until = data["since"], data["until"]
1377
1391
  sessions = data["sessions"]
1378
1392
  multi_day = (until - since) > timedelta(days=1)
1379
- tfmt = "%Y-%m-%d %H:%M" if multi_day else "%H:%M"
1380
1393
  head = (
1381
- f"=== Engagement {since:%Y-%m-%d %H:%M} → {until:%Y-%m-%d %H:%M} "
1382
- f"(times: {tz_label}, break={data['break_minutes']}m) ==="
1394
+ f"=== Engagement {_fmt_clock(since, True)} → {_fmt_clock(until, True)} "
1395
+ f"(times: {tz_label} 12h (24h), break={data['break_minutes']}m) ==="
1383
1396
  )
1384
1397
  if not sessions:
1385
1398
  return head + "\n\n(no user messages in range)"
@@ -1396,7 +1409,7 @@ def render_engagement(data: dict, tz_label: str) -> str:
1396
1409
  out.append(
1397
1410
  f"{_fmt_dur(s['active']):>7} ratio {ratio} "
1398
1411
  f"you {s['user_messages']:<3} ai {s['assistant_messages']:<4} "
1399
- f"{s['first'].strftime(tfmt)}–{s['last'].strftime('%H:%M')} "
1412
+ f"{_fmt_clock(s['first'], multi_day)}–{_fmt_tod(s['last'])} "
1400
1413
  f"{_session_label(s['summary'])}"
1401
1414
  )
1402
1415
  total_active = sum((s["active"] for s in sessions.values()), timedelta())
@@ -1405,13 +1418,13 @@ def render_engagement(data: dict, tz_label: str) -> str:
1405
1418
  out.append("")
1406
1419
  out.append(
1407
1420
  f"Total: {_fmt_dur(total_active)} active across {len(sessions)} session(s), "
1408
- f"{first.strftime(tfmt)}–{last.strftime('%H:%M')} span ({_fmt_dur(last - first)})"
1421
+ f"{_fmt_clock(first, multi_day)}–{_fmt_tod(last)} span ({_fmt_dur(last - first)})"
1409
1422
  )
1410
1423
  breaks = data["breaks"]
1411
1424
  if breaks:
1412
1425
  shown = breaks[:6]
1413
1426
  items = ", ".join(
1414
- f"{a.strftime(tfmt)}→{b.strftime('%H:%M')} ({_fmt_dur(b - a)})"
1427
+ f"{_fmt_clock(a, multi_day)}→{_fmt_tod(b)} ({_fmt_dur(b - a)})"
1415
1428
  for a, b in shown
1416
1429
  )
1417
1430
  more = f" (+{len(breaks) - len(shown)} more)" if len(breaks) > len(shown) else ""
@@ -1502,10 +1515,9 @@ def render_session_report(data: dict, tz_label: str) -> str:
1502
1515
  since, until = data["since"], data["until"]
1503
1516
  sessions = data["sessions"]
1504
1517
  multi_day = (until - since) > timedelta(days=1)
1505
- tfmt = "%Y-%m-%d %H:%M" if multi_day else "%H:%M"
1506
1518
  head = (
1507
- f"=== Session report {since:%Y-%m-%d %H:%M} → {until:%Y-%m-%d %H:%M} "
1508
- f"(times: {tz_label}, break={data['break_minutes']}m) ==="
1519
+ f"=== Session report {_fmt_clock(since, True)} → {_fmt_clock(until, True)} "
1520
+ f"(times: {tz_label} 12h (24h), break={data['break_minutes']}m) ==="
1509
1521
  )
1510
1522
  if not sessions:
1511
1523
  return head + "\n\n(no user activity in range)"
@@ -1518,7 +1530,7 @@ def render_session_report(data: dict, tz_label: str) -> str:
1518
1530
  out.append(f"{i}. {title}")
1519
1531
  out.append(
1520
1532
  f" {summary['decoded_project']} · "
1521
- f"{s['first'].strftime(tfmt)}–{s['last'].strftime('%H:%M')} "
1533
+ f"{_fmt_clock(s['first'], multi_day)}–{_fmt_tod(s['last'])} "
1522
1534
  f"(ran {_fmt_dur(elapsed)} · active {_fmt_dur(s['active'])})"
1523
1535
  )
1524
1536
  out.append(
@@ -1535,7 +1547,7 @@ def render_session_report(data: dict, tz_label: str) -> str:
1535
1547
  out.append(
1536
1548
  f"Total: {len(sessions)} session(s) · {_fmt_dur(total_active)} active "
1537
1549
  f"(overlap removed) across a {_fmt_dur(last - first)} span "
1538
- f"({first.strftime(tfmt)}–{last.strftime('%H:%M')})."
1550
+ f"({_fmt_clock(first, multi_day)}–{_fmt_tod(last)})."
1539
1551
  )
1540
1552
  out.append(
1541
1553
  "(active = your attention, parallel chats never double-counted; "