inscript 0.2.0__tar.gz → 0.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inscript
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Universal agent activity ledger. Records what AI agents do.
5
5
  Author: Andrew Park
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inscript
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Universal agent activity ledger. Records what AI agents do.
5
5
  Author: Andrew Park
6
6
  License-Expression: MIT
@@ -17,7 +17,7 @@ import time
17
17
  from datetime import datetime, timezone
18
18
  from pathlib import Path
19
19
 
20
- __version__ = "0.2.0"
20
+ __version__ = "0.3.0"
21
21
 
22
22
  INSCRIPT_DIR = Path.home() / ".inscript"
23
23
  ACTIVE_PROJECT_FILE = INSCRIPT_DIR / "active_project"
@@ -202,6 +202,20 @@ def _cmd_status():
202
202
  print(f" {s['session_id']} — {s.get('project', '?')}")
203
203
 
204
204
 
205
+ def _load_prompts(sdir: Path) -> list[dict]:
206
+ """Load prompts for a session."""
207
+ prompts_file = sdir / "prompts.jsonl"
208
+ if not prompts_file.exists():
209
+ return []
210
+ prompts = []
211
+ for line in prompts_file.open():
212
+ try:
213
+ prompts.append(json.loads(line))
214
+ except json.JSONDecodeError:
215
+ pass
216
+ return prompts
217
+
218
+
205
219
  def _cmd_log(session_id: str | None):
206
220
  if session_id is None:
207
221
  session_id = active_session()
@@ -212,25 +226,54 @@ def _cmd_log(session_id: str | None):
212
226
  return
213
227
  session_id = sessions[0]["session_id"]
214
228
 
215
- touches_file = session_dir(session_id) / "touches.jsonl"
229
+ sdir = session_dir(session_id)
230
+ touches_file = sdir / "touches.jsonl"
216
231
  if not touches_file.exists():
217
232
  print(f"No activity log for {session_id}", file=__import__("sys").stderr)
218
233
  return
219
234
 
220
- print(f"Session: {session_id}\n")
235
+ prompts = _load_prompts(sdir)
236
+
237
+ # Group touches by prompt_idx
238
+ touches_by_prompt: dict[int | None, list[dict]] = {}
221
239
  files_seen = set()
222
240
  edits = 0
223
241
  for line in touches_file.open():
224
242
  try:
225
243
  e = json.loads(line)
226
- extra = f" ({e['lines_changed']} lines)" if e.get("lines_changed") else ""
227
- print(f" {e.get('ts', '?')} {e.get('action', '?'):6s} {e.get('file', '?')}{extra}")
244
+ pidx = e.get("prompt_idx")
245
+ touches_by_prompt.setdefault(pidx, []).append(e)
228
246
  files_seen.add(e.get("file"))
229
247
  if e.get("action") in ("edit", "write"):
230
248
  edits += 1
231
249
  except json.JSONDecodeError:
232
250
  pass
233
- print(f"\n {len(files_seen)} files, {edits} edits")
251
+
252
+ print(f"Session: {session_id}\n")
253
+
254
+ if prompts:
255
+ for p in prompts:
256
+ idx = p.get("idx", 0)
257
+ prompt_text = p.get("prompt", "")
258
+ # Truncate long prompts
259
+ if len(prompt_text) > 80:
260
+ prompt_text = prompt_text[:77] + "..."
261
+ print(f" [{p.get('ts', '?')}] \"{prompt_text}\"")
262
+ for t in touches_by_prompt.get(idx, []):
263
+ extra = f" ({t['lines_changed']} lines)" if t.get("lines_changed") else ""
264
+ print(f" {t.get('action', '?'):6s} {t.get('file', '?')}{extra}")
265
+ print()
266
+ else:
267
+ # No prompts recorded — flat list
268
+ for line in touches_file.open():
269
+ try:
270
+ e = json.loads(line)
271
+ extra = f" ({e['lines_changed']} lines)" if e.get("lines_changed") else ""
272
+ print(f" {e.get('ts', '?')} {e.get('action', '?'):6s} {e.get('file', '?')}{extra}")
273
+ except json.JSONDecodeError:
274
+ pass
275
+
276
+ print(f" {len(files_seen)} files, {edits} edits, {len(prompts)} prompts")
234
277
 
235
278
 
236
279
  def _cmd_overlap():
@@ -300,30 +343,75 @@ def _cmd_export(session_id: str):
300
343
  if summary_file.exists():
301
344
  s = json.loads(summary_file.read_text())
302
345
  print(f"- **Ended**: {s.get('end_time', '?')}")
346
+ print(f"- **Prompts**: {s.get('prompts', '?')}")
303
347
  print(f"- **Files read**: {s.get('files_read', '?')}")
304
348
  print(f"- **Files written**: {s.get('files_written', '?')}")
305
349
  print(f"- **Total edits**: {s.get('total_edits', '?')}")
306
350
 
351
+ # Load prompts, touches, and diffs
352
+ prompts = _load_prompts(sdir)
353
+
354
+ touches_by_prompt: dict[int | None, list[dict]] = {}
307
355
  if touches_file.exists():
308
- print(f"\n## Activity\n")
309
- print("| Time | Action | File |")
310
- print("|------|--------|------|")
311
356
  for line in touches_file.open():
312
357
  try:
313
358
  e = json.loads(line)
314
- print(f"| {e.get('ts', '')} | {e.get('action', '')} | `{e.get('file', '')}` |")
359
+ touches_by_prompt.setdefault(e.get("prompt_idx"), []).append(e)
315
360
  except json.JSONDecodeError:
316
361
  pass
317
362
 
363
+ diffs_by_prompt: dict[int | None, list[dict]] = {}
318
364
  if diffs_file.exists():
319
- print(f"\n## Changes\n")
320
365
  for line in diffs_file.open():
321
366
  try:
322
367
  d = json.loads(line)
323
- print(f"### `{d.get('file', '?')}` ({d.get('ts', '')})\n")
368
+ diffs_by_prompt.setdefault(d.get("prompt_idx"), []).append(d)
369
+ except json.JSONDecodeError:
370
+ pass
371
+
372
+ if prompts:
373
+ # Group output by prompt
374
+ for p in prompts:
375
+ idx = p.get("idx", 0)
376
+ print(f"\n## \"{p.get('prompt', '?')}\"\n")
377
+ print(f"*{p.get('ts', '')}*\n")
378
+
379
+ touches = touches_by_prompt.get(idx, [])
380
+ if touches:
381
+ print("| Action | File | Details |")
382
+ print("|--------|------|---------|")
383
+ for t in touches:
384
+ details = ""
385
+ if t.get("lines_changed"):
386
+ details = f"{t['lines_changed']} lines"
387
+ elif t.get("lines"):
388
+ details = f"{t['lines']} lines"
389
+ print(f"| {t.get('action', '')} | `{t.get('file', '')}` | {details} |")
390
+ print()
391
+
392
+ diffs = diffs_by_prompt.get(idx, [])
393
+ for d in diffs:
324
394
  if d.get("old_string") is not None:
395
+ print(f"**`{d.get('file', '?')}`**")
325
396
  print(f"```diff\n- {d['old_string']}\n+ {d.get('new_string', '')}\n```\n")
326
397
  elif d.get("is_new"):
327
- print(f"New file ({d.get('lines', '?')} lines)\n")
328
- except json.JSONDecodeError:
329
- pass
398
+ print(f"**`{d.get('file', '?')}`** — new file ({d.get('lines', '?')} lines)\n")
399
+ else:
400
+ # No prompts — flat output
401
+ if touches_by_prompt:
402
+ print(f"\n## Activity\n")
403
+ print("| Time | Action | File |")
404
+ print("|------|--------|------|")
405
+ for touches in touches_by_prompt.values():
406
+ for e in touches:
407
+ print(f"| {e.get('ts', '')} | {e.get('action', '')} | `{e.get('file', '')}` |")
408
+
409
+ if diffs_by_prompt:
410
+ print(f"\n## Changes\n")
411
+ for diffs in diffs_by_prompt.values():
412
+ for d in diffs:
413
+ if d.get("old_string") is not None:
414
+ print(f"### `{d.get('file', '?')}` ({d.get('ts', '')})\n")
415
+ print(f"```diff\n- {d['old_string']}\n+ {d.get('new_string', '')}\n```\n")
416
+ elif d.get("is_new"):
417
+ print(f"New file `{d.get('file', '?')}` ({d.get('lines', '?')} lines)\n")
@@ -1,9 +1,10 @@
1
1
  """Inscript hook — records agent activity to ~/.inscript/.
2
2
 
3
- Handles three Claude Code hook events:
4
- SessionStart → creates session directory + meta.json
5
- PostToolUse appends to touches.jsonl + diffs.jsonl, updates active_project
6
- Stop finalizes summary.json
3
+ Handles four Claude Code hook events:
4
+ SessionStart → creates session directory + meta.json
5
+ UserPromptSubmit records the user's prompt, starts a new work block
6
+ PostToolUse appends to touches.jsonl + diffs.jsonl, tags with prompt
7
+ Stop → finalizes summary.json
7
8
 
8
9
  All entry points read JSON from stdin and write to the filesystem.
9
10
  Designed to run async (non-blocking) so the agent never waits.
@@ -90,6 +91,20 @@ def _append_jsonl(path: Path, data: dict) -> None:
90
91
  f.write(json.dumps(data, default=str) + "\n")
91
92
 
92
93
 
94
+ def _count_lines(path: Path) -> int:
95
+ """Count lines in a JSONL file."""
96
+ try:
97
+ return sum(1 for _ in path.open())
98
+ except OSError:
99
+ return 0
100
+
101
+
102
+ def _current_prompt_idx(sdir: Path) -> int | None:
103
+ """Get the index of the most recent prompt (0-based)."""
104
+ n = _count_lines(sdir / "prompts.jsonl")
105
+ return n - 1 if n > 0 else None
106
+
107
+
93
108
  # ---------------------------------------------------------------------------
94
109
  # SessionStart handler
95
110
  # ---------------------------------------------------------------------------
@@ -120,6 +135,37 @@ def handle_session_start(data: dict) -> None:
120
135
  ACTIVE_SESSION_FILE.write_text(session_id + "\n")
121
136
 
122
137
 
138
+ # ---------------------------------------------------------------------------
139
+ # UserPromptSubmit handler
140
+ # ---------------------------------------------------------------------------
141
+
142
+ def handle_prompt_submit(data: dict) -> None:
143
+ """Record a user prompt, starting a new work block."""
144
+ session_id = active_session()
145
+ if not session_id:
146
+ session_id = f"s-{int(time.time())}"
147
+ handle_session_start({"session_id": session_id})
148
+
149
+ sdir = SESSIONS_DIR / session_id
150
+
151
+ # Extract prompt text from hook input
152
+ # Claude Code sends tool_input with the user's message
153
+ prompt = data.get("tool_input", {}).get("prompt", "")
154
+ if not prompt:
155
+ # Try alternate locations
156
+ prompt = data.get("prompt", "") or data.get("message", "")
157
+ if not prompt:
158
+ return
159
+
160
+ prompt_idx = _count_lines(sdir / "prompts.jsonl")
161
+
162
+ _append_jsonl(sdir / "prompts.jsonl", {
163
+ "idx": prompt_idx,
164
+ "ts": _now_time(),
165
+ "prompt": prompt,
166
+ })
167
+
168
+
123
169
  # ---------------------------------------------------------------------------
124
170
  # PostToolUse handler
125
171
  # ---------------------------------------------------------------------------
@@ -175,12 +221,15 @@ def handle_post_tool_use(data: dict) -> None:
175
221
 
176
222
  # Append to touches.jsonl
177
223
  action = _tool_action(tool_name)
224
+ prompt_idx = _current_prompt_idx(sdir)
178
225
  touch = {
179
226
  "ts": _now_time(),
180
227
  "file": display_path,
181
228
  "action": action,
182
229
  "tool": tool_name,
183
230
  }
231
+ if prompt_idx is not None:
232
+ touch["prompt_idx"] = prompt_idx
184
233
 
185
234
  # Add lines_changed for Edit
186
235
  if tool_name == "Edit" and tool_input.get("new_string") is not None:
@@ -201,6 +250,8 @@ def handle_post_tool_use(data: dict) -> None:
201
250
  "file": display_path,
202
251
  "tool": tool_name,
203
252
  }
253
+ if prompt_idx is not None:
254
+ diff_entry["prompt_idx"] = prompt_idx
204
255
 
205
256
  if tool_name == "Edit":
206
257
  diff_entry["old_string"] = tool_input.get("old_string", "")
@@ -297,9 +348,14 @@ def handle_stop(data: dict) -> None:
297
348
  except ValueError:
298
349
  pass
299
350
 
351
+ # Count prompts
352
+ prompts_file = sdir / "prompts.jsonl"
353
+ prompt_count = _count_lines(prompts_file)
354
+
300
355
  summary = {
301
356
  "end_time": end_time,
302
357
  "duration_seconds": duration,
358
+ "prompts": prompt_count,
303
359
  "files_read": len(files_read),
304
360
  "files_written": len(files_written),
305
361
  "files_read_list": sorted(files_read),
@@ -334,6 +390,8 @@ def main() -> None:
334
390
 
335
391
  if hook_event == "SessionStart":
336
392
  handle_session_start(data)
393
+ elif hook_event == "UserPromptSubmit":
394
+ handle_prompt_submit(data)
337
395
  elif hook_event == "Stop":
338
396
  handle_stop(data)
339
397
  else:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "inscript"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Universal agent activity ledger. Records what AI agents do."
9
9
  readme = "README.md"
10
10
  license = "MIT"
File without changes
File without changes
File without changes