inscript 0.3.0__tar.gz → 0.4.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.
- {inscript-0.3.0 → inscript-0.4.0}/PKG-INFO +1 -1
- {inscript-0.3.0 → inscript-0.4.0}/inscript.egg-info/PKG-INFO +1 -1
- {inscript-0.3.0 → inscript-0.4.0}/inscript_pkg/__init__.py +188 -2
- {inscript-0.3.0 → inscript-0.4.0}/inscript_pkg/hook.py +19 -2
- {inscript-0.3.0 → inscript-0.4.0}/pyproject.toml +1 -1
- {inscript-0.3.0 → inscript-0.4.0}/LICENSE +0 -0
- {inscript-0.3.0 → inscript-0.4.0}/README.md +0 -0
- {inscript-0.3.0 → inscript-0.4.0}/inscript.egg-info/SOURCES.txt +0 -0
- {inscript-0.3.0 → inscript-0.4.0}/inscript.egg-info/dependency_links.txt +0 -0
- {inscript-0.3.0 → inscript-0.4.0}/inscript.egg-info/entry_points.txt +0 -0
- {inscript-0.3.0 → inscript-0.4.0}/inscript.egg-info/top_level.txt +0 -0
- {inscript-0.3.0 → inscript-0.4.0}/setup.cfg +0 -0
|
@@ -17,7 +17,7 @@ import time
|
|
|
17
17
|
from datetime import datetime, timezone
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
|
|
20
|
-
__version__ = "0.
|
|
20
|
+
__version__ = "0.4.0"
|
|
21
21
|
|
|
22
22
|
INSCRIPT_DIR = Path.home() / ".inscript"
|
|
23
23
|
ACTIVE_PROJECT_FILE = INSCRIPT_DIR / "active_project"
|
|
@@ -133,6 +133,9 @@ def cli() -> None:
|
|
|
133
133
|
"cleanup": _cmd_cleanup,
|
|
134
134
|
"export": lambda: _cmd_export(args[1]) if len(args) > 1 else print("Usage: inscript export <session-id>", file=sys.stderr),
|
|
135
135
|
"set": lambda: (set_active_project(args[1]), print(f"Active project: {Path(args[1]).resolve()}")) if len(args) > 1 else print("Usage: inscript set <path>", file=sys.stderr),
|
|
136
|
+
"tag": lambda: _cmd_tag(args[1] if len(args) > 1 else None),
|
|
137
|
+
"untag": lambda: _cmd_tag(None),
|
|
138
|
+
"time": lambda: _cmd_time(args[1] if len(args) > 1 else None),
|
|
136
139
|
"help": _cmd_help, "--help": _cmd_help, "-h": _cmd_help,
|
|
137
140
|
}
|
|
138
141
|
|
|
@@ -155,7 +158,10 @@ Commands:
|
|
|
155
158
|
inscript overlap File collisions across concurrent sessions
|
|
156
159
|
inscript cleanup Enforce retention policy
|
|
157
160
|
inscript export <id> Export session as markdown
|
|
158
|
-
inscript set <path> Manually set active project
|
|
161
|
+
inscript set <path> Manually set active project
|
|
162
|
+
inscript tag <name> Tag current work with a feature/task name
|
|
163
|
+
inscript untag Clear the current tag
|
|
164
|
+
inscript time [tag] Show time spent, optionally filtered by tag""")
|
|
159
165
|
|
|
160
166
|
|
|
161
167
|
def _cmd_init():
|
|
@@ -276,6 +282,186 @@ def _cmd_log(session_id: str | None):
|
|
|
276
282
|
print(f" {len(files_seen)} files, {edits} edits, {len(prompts)} prompts")
|
|
277
283
|
|
|
278
284
|
|
|
285
|
+
def _cmd_tag(tag_name: str | None):
|
|
286
|
+
"""Set or clear the active tag for the current session."""
|
|
287
|
+
sess = active_session()
|
|
288
|
+
if not sess:
|
|
289
|
+
print("No active session", file=__import__("sys").stderr)
|
|
290
|
+
__import__("sys").exit(1)
|
|
291
|
+
|
|
292
|
+
sdir = session_dir(sess)
|
|
293
|
+
tag_file = sdir / "active_tag"
|
|
294
|
+
|
|
295
|
+
if tag_name is None:
|
|
296
|
+
# Clear tag
|
|
297
|
+
try:
|
|
298
|
+
tag_file.unlink()
|
|
299
|
+
except OSError:
|
|
300
|
+
pass
|
|
301
|
+
print("Tag cleared")
|
|
302
|
+
else:
|
|
303
|
+
tag_file.write_text(tag_name + "\n")
|
|
304
|
+
print(f"Tagged: {tag_name}")
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _format_duration(seconds: int) -> str:
|
|
308
|
+
"""Format seconds into human-readable duration."""
|
|
309
|
+
if seconds < 60:
|
|
310
|
+
return f"{seconds}s"
|
|
311
|
+
elif seconds < 3600:
|
|
312
|
+
m, s = divmod(seconds, 60)
|
|
313
|
+
return f"{m}m {s}s"
|
|
314
|
+
else:
|
|
315
|
+
h, remainder = divmod(seconds, 3600)
|
|
316
|
+
m, s = divmod(remainder, 60)
|
|
317
|
+
return f"{h}h {m}m"
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _cmd_time(tag_filter: str | None):
|
|
321
|
+
"""Show time spent, optionally filtered by tag."""
|
|
322
|
+
all_sessions = list_sessions()
|
|
323
|
+
if not all_sessions:
|
|
324
|
+
print("No sessions found")
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
# Collect timing data from prompts across all sessions
|
|
328
|
+
tag_data: dict[str | None, dict] = {} # tag -> {sessions, prompts, first_ts, last_ts, active_seconds, files, edits}
|
|
329
|
+
|
|
330
|
+
for s in all_sessions:
|
|
331
|
+
sid = s["session_id"]
|
|
332
|
+
sdir = session_dir(sid)
|
|
333
|
+
prompts = _load_prompts(sdir)
|
|
334
|
+
touches_file = sdir / "touches.jsonl"
|
|
335
|
+
|
|
336
|
+
# Load touches
|
|
337
|
+
touches: list[dict] = []
|
|
338
|
+
if touches_file.exists():
|
|
339
|
+
for line in touches_file.open():
|
|
340
|
+
try:
|
|
341
|
+
touches.append(json.loads(line))
|
|
342
|
+
except json.JSONDecodeError:
|
|
343
|
+
pass
|
|
344
|
+
|
|
345
|
+
if not prompts:
|
|
346
|
+
continue
|
|
347
|
+
|
|
348
|
+
# Compute time per prompt block
|
|
349
|
+
for i, p in enumerate(prompts):
|
|
350
|
+
tag = p.get("tag")
|
|
351
|
+
if tag_filter and tag != tag_filter:
|
|
352
|
+
continue
|
|
353
|
+
|
|
354
|
+
# Parse this prompt's timestamp
|
|
355
|
+
prompt_ts = p.get("ts", "")
|
|
356
|
+
|
|
357
|
+
# Find next prompt's timestamp (or session end) for duration
|
|
358
|
+
if i + 1 < len(prompts):
|
|
359
|
+
next_ts = prompts[i + 1].get("ts", "")
|
|
360
|
+
else:
|
|
361
|
+
# Use last touch timestamp or summary end_time
|
|
362
|
+
summary_file = sdir / "summary.json"
|
|
363
|
+
if summary_file.exists():
|
|
364
|
+
try:
|
|
365
|
+
summary = json.loads(summary_file.read_text())
|
|
366
|
+
end = summary.get("end_time", "")
|
|
367
|
+
next_ts = end.split("T")[-1] if "T" in end else ""
|
|
368
|
+
except (json.JSONDecodeError, OSError):
|
|
369
|
+
next_ts = ""
|
|
370
|
+
else:
|
|
371
|
+
# Use last touch
|
|
372
|
+
block_touches = [t for t in touches if t.get("prompt_idx") == p.get("idx")]
|
|
373
|
+
next_ts = block_touches[-1].get("ts", "") if block_touches else ""
|
|
374
|
+
|
|
375
|
+
# Compute duration for this prompt block
|
|
376
|
+
block_seconds = _ts_diff(prompt_ts, next_ts)
|
|
377
|
+
|
|
378
|
+
# Count files and edits for this block
|
|
379
|
+
block_touches = [t for t in touches if t.get("prompt_idx") == p.get("idx")]
|
|
380
|
+
block_files = {t.get("file") for t in block_touches}
|
|
381
|
+
block_edits = sum(1 for t in block_touches if t.get("action") in ("edit", "write"))
|
|
382
|
+
|
|
383
|
+
# Aggregate by tag
|
|
384
|
+
if tag not in tag_data:
|
|
385
|
+
tag_data[tag] = {
|
|
386
|
+
"sessions": set(),
|
|
387
|
+
"prompts": 0,
|
|
388
|
+
"active_seconds": 0,
|
|
389
|
+
"first_ts": s.get("start_time", ""),
|
|
390
|
+
"last_ts": s.get("start_time", ""),
|
|
391
|
+
"files": set(),
|
|
392
|
+
"edits": 0,
|
|
393
|
+
"prompt_texts": [],
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
td = tag_data[tag]
|
|
397
|
+
td["sessions"].add(sid)
|
|
398
|
+
td["prompts"] += 1
|
|
399
|
+
td["active_seconds"] += block_seconds
|
|
400
|
+
td["files"].update(block_files)
|
|
401
|
+
td["edits"] += block_edits
|
|
402
|
+
td["prompt_texts"].append(p.get("prompt", ""))
|
|
403
|
+
# Track first/last
|
|
404
|
+
session_time = s.get("start_time", "")
|
|
405
|
+
if session_time < td["first_ts"] or not td["first_ts"]:
|
|
406
|
+
td["first_ts"] = session_time
|
|
407
|
+
if session_time > td["last_ts"]:
|
|
408
|
+
td["last_ts"] = session_time
|
|
409
|
+
|
|
410
|
+
if not tag_data:
|
|
411
|
+
if tag_filter:
|
|
412
|
+
print(f"No data for tag: {tag_filter}")
|
|
413
|
+
else:
|
|
414
|
+
print("No prompt data found")
|
|
415
|
+
return
|
|
416
|
+
|
|
417
|
+
# Display
|
|
418
|
+
if tag_filter:
|
|
419
|
+
# Single tag detail view
|
|
420
|
+
td = tag_data.get(tag_filter)
|
|
421
|
+
if not td:
|
|
422
|
+
print(f"No data for tag: {tag_filter}")
|
|
423
|
+
return
|
|
424
|
+
print(f"Feature: {tag_filter}\n")
|
|
425
|
+
print(f" Sessions: {len(td['sessions'])}")
|
|
426
|
+
print(f" Prompts: {td['prompts']}")
|
|
427
|
+
print(f" Active time: {_format_duration(td['active_seconds'])}")
|
|
428
|
+
print(f" Files: {len(td['files'])}")
|
|
429
|
+
print(f" Edits: {td['edits']}")
|
|
430
|
+
print(f" First: {td['first_ts']}")
|
|
431
|
+
print(f" Last: {td['last_ts']}")
|
|
432
|
+
print(f"\n Prompts:")
|
|
433
|
+
for pt in td["prompt_texts"]:
|
|
434
|
+
display = pt[:70] + "..." if len(pt) > 70 else pt
|
|
435
|
+
print(f" - \"{display}\"")
|
|
436
|
+
else:
|
|
437
|
+
# Overview of all tags
|
|
438
|
+
print("Time by tag:\n")
|
|
439
|
+
# Sort: tagged first (alphabetical), then untagged
|
|
440
|
+
sorted_tags = sorted(
|
|
441
|
+
tag_data.keys(),
|
|
442
|
+
key=lambda t: (t is None, t or "")
|
|
443
|
+
)
|
|
444
|
+
for tag in sorted_tags:
|
|
445
|
+
td = tag_data[tag]
|
|
446
|
+
label = tag or "(untagged)"
|
|
447
|
+
print(f" {label}")
|
|
448
|
+
print(f" {_format_duration(td['active_seconds'])} active, {td['prompts']} prompts, {td['edits']} edits, {len(td['sessions'])} sessions")
|
|
449
|
+
print()
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _ts_diff(ts1: str, ts2: str) -> int:
|
|
453
|
+
"""Compute seconds between two HH:MM:SS timestamps. Returns 0 on failure."""
|
|
454
|
+
try:
|
|
455
|
+
parts1 = [int(x) for x in ts1.split(":")]
|
|
456
|
+
parts2 = [int(x) for x in ts2.split(":")]
|
|
457
|
+
s1 = parts1[0] * 3600 + parts1[1] * 60 + parts1[2]
|
|
458
|
+
s2 = parts2[0] * 3600 + parts2[1] * 60 + parts2[2]
|
|
459
|
+
diff = s2 - s1
|
|
460
|
+
return max(0, diff) # Clamp negative (midnight crossing edge case)
|
|
461
|
+
except (ValueError, IndexError):
|
|
462
|
+
return 0
|
|
463
|
+
|
|
464
|
+
|
|
279
465
|
def _cmd_overlap():
|
|
280
466
|
if not OVERLAP_DIR.exists():
|
|
281
467
|
print("No overlap data")
|
|
@@ -105,6 +105,15 @@ def _current_prompt_idx(sdir: Path) -> int | None:
|
|
|
105
105
|
return n - 1 if n > 0 else None
|
|
106
106
|
|
|
107
107
|
|
|
108
|
+
def _current_tag(sdir: Path) -> str | None:
|
|
109
|
+
"""Read the active tag for this session."""
|
|
110
|
+
tag_file = sdir / "active_tag"
|
|
111
|
+
try:
|
|
112
|
+
return tag_file.read_text().strip() or None
|
|
113
|
+
except OSError:
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
108
117
|
# ---------------------------------------------------------------------------
|
|
109
118
|
# SessionStart handler
|
|
110
119
|
# ---------------------------------------------------------------------------
|
|
@@ -158,12 +167,17 @@ def handle_prompt_submit(data: dict) -> None:
|
|
|
158
167
|
return
|
|
159
168
|
|
|
160
169
|
prompt_idx = _count_lines(sdir / "prompts.jsonl")
|
|
170
|
+
tag = _current_tag(sdir)
|
|
161
171
|
|
|
162
|
-
|
|
172
|
+
entry = {
|
|
163
173
|
"idx": prompt_idx,
|
|
164
174
|
"ts": _now_time(),
|
|
165
175
|
"prompt": prompt,
|
|
166
|
-
}
|
|
176
|
+
}
|
|
177
|
+
if tag:
|
|
178
|
+
entry["tag"] = tag
|
|
179
|
+
|
|
180
|
+
_append_jsonl(sdir / "prompts.jsonl", entry)
|
|
167
181
|
|
|
168
182
|
|
|
169
183
|
# ---------------------------------------------------------------------------
|
|
@@ -222,6 +236,7 @@ def handle_post_tool_use(data: dict) -> None:
|
|
|
222
236
|
# Append to touches.jsonl
|
|
223
237
|
action = _tool_action(tool_name)
|
|
224
238
|
prompt_idx = _current_prompt_idx(sdir)
|
|
239
|
+
tag = _current_tag(sdir)
|
|
225
240
|
touch = {
|
|
226
241
|
"ts": _now_time(),
|
|
227
242
|
"file": display_path,
|
|
@@ -230,6 +245,8 @@ def handle_post_tool_use(data: dict) -> None:
|
|
|
230
245
|
}
|
|
231
246
|
if prompt_idx is not None:
|
|
232
247
|
touch["prompt_idx"] = prompt_idx
|
|
248
|
+
if tag:
|
|
249
|
+
touch["tag"] = tag
|
|
233
250
|
|
|
234
251
|
# Add lines_changed for Edit
|
|
235
252
|
if tool_name == "Edit" and tool_input.get("new_string") is not None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|