@iseoane/agent-depot 0.5.1 → 0.6.0
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/CHANGELOG.md +37 -0
- package/README.es.md +120 -53
- package/README.md +101 -41
- package/dist/src/app-flow.js +56 -24
- package/dist/src/cli.js +55 -0
- package/dist/src/package-cli.js +2 -1
- package/dist/src/package-flow.js +1 -1
- package/dist/src/package-host-state.js +10 -6
- package/dist/src/profile-export.js +72 -9
- package/dist/src/profile-import.js +3 -2
- package/dist/src/profile.js +71 -7
- package/dist/src/self-update.js +141 -0
- package/dist/src/tui/app-actions.js +82 -33
- package/dist/src/tui/app.js +19 -14
- package/dist/src/tui/catalog-browse.js +6 -1
- package/dist/src/tui/catalog-panel.js +11 -11
- package/dist/src/tui/catalog-state.js +28 -0
- package/dist/src/tui/catalog-tree.js +22 -5
- package/dist/src/tui/catalog-view.js +59 -16
- package/dist/src/tui/header-layout.js +33 -0
- package/dist/src/tui/installations-panel.js +3 -3
- package/dist/src/tui/installations-rows.js +20 -5
- package/dist/src/tui/installations-tree.js +4 -7
- package/dist/src/tui/installations-view.js +15 -6
- package/dist/src/tui/installations.js +9 -7
- package/dist/src/tui/manage-panel.js +7 -6
- package/dist/src/tui/mode-keys.js +9 -0
- package/dist/src/tui/package-rows.js +21 -7
- package/dist/src/tui/panel-parts.js +38 -6
- package/dist/src/tui/preview-window.js +48 -0
- package/dist/src/tui/profile-view.js +261 -42
- package/dist/src/tui/sources-mode.js +3 -1
- package/dist/src/tui/sources-recipes.js +50 -19
- package/dist/src/tui/sources-view.js +50 -14
- package/dist/src/tui/theme.js +14 -7
- package/dist/src/tui/updates-flow.js +2 -2
- package/dist/src/tui/updates-keys.js +34 -5
- package/dist/src/tui/updates-model.js +2 -0
- package/dist/src/tui/updates-panel.js +40 -15
- package/dist/src/tui/updates-preview.js +65 -0
- package/dist/src/tui/updates-view.js +17 -2
- package/dist/src/tui/updates.js +14 -5
- package/dist/src/tui/window.js +6 -4
- package/package.json +35 -14
- package/resources/pi/agents-panel/contract.ts +62 -0
- package/resources/pi/agents-panel/index.ts +471 -0
- package/skills/doctor-md-agents/SKILL.md +2 -1
- package/skills/doctor-md-agents/scripts/scan_corrections.py +141 -13
- package/skills/doctor-md-skill/references/supported-harnesses.md +2 -0
- package/skills/doctor-md-skill/scripts/collect_sessions.py +26 -2
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
"""Mine the raw session logs for hard evidence that the user had to correct
|
|
3
3
|
the agent -- the whole window, not a sample.
|
|
4
4
|
|
|
5
|
-
Sources: Claude Code JSONL (`<claude-home>/projects/*/*.jsonl
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
Sources: Claude Code JSONL (`<claude-home>/projects/*/*.jsonl`), Codex rollout
|
|
6
|
+
JSONL (`<codex-home>/{sessions,archived_sessions}/**/rollout-*.jsonl`), and Pi
|
|
7
|
+
session JSONL (`<pi-home>/sessions/*/*.jsonl`). The report records each
|
|
8
|
+
source's availability and scanned-file count. Grok Build and ZCode are not
|
|
9
|
+
supported by this correction scanner.
|
|
9
10
|
|
|
10
11
|
Detection is a case-insensitive regex over the user's own words, seeded with
|
|
11
12
|
both Spanish and English phrasings. The patterns intentionally over-match --
|
|
@@ -468,6 +469,104 @@ def find_codex_files(codex_home, cutoff):
|
|
|
468
469
|
return sorted(files)
|
|
469
470
|
|
|
470
471
|
|
|
472
|
+
# ---------------------------------------------------------------------------
|
|
473
|
+
# Pi
|
|
474
|
+
# ---------------------------------------------------------------------------
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def pi_active_entry_ids(records):
|
|
478
|
+
"""Return Pi's active session-entry ancestry when parent links are complete."""
|
|
479
|
+
entries = [record for record in records if record.get("type") != "session"]
|
|
480
|
+
if not entries or any(
|
|
481
|
+
not entry.get("id") or "parentId" not in entry for entry in entries
|
|
482
|
+
):
|
|
483
|
+
return None
|
|
484
|
+
by_id = {entry["id"]: entry for entry in entries}
|
|
485
|
+
active = set()
|
|
486
|
+
current = entries[-1]["id"]
|
|
487
|
+
while current is not None:
|
|
488
|
+
if current in active or current not in by_id:
|
|
489
|
+
return None
|
|
490
|
+
active.add(current)
|
|
491
|
+
current = by_id[current].get("parentId")
|
|
492
|
+
return active
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def pi_text(content):
|
|
496
|
+
if isinstance(content, str):
|
|
497
|
+
return content
|
|
498
|
+
if not isinstance(content, list):
|
|
499
|
+
return None
|
|
500
|
+
parts = [
|
|
501
|
+
block["text"] for block in content
|
|
502
|
+
if isinstance(block, dict) and block.get("type") == "text"
|
|
503
|
+
and isinstance(block.get("text"), str) and block["text"]
|
|
504
|
+
]
|
|
505
|
+
return "\n".join(parts) if parts else None
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def iter_pi_turns(path, max_context):
|
|
509
|
+
try:
|
|
510
|
+
with path.open("r", encoding="utf-8", errors="replace") as handle:
|
|
511
|
+
records = []
|
|
512
|
+
for line in handle:
|
|
513
|
+
try:
|
|
514
|
+
records.append(json.loads(line))
|
|
515
|
+
except json.JSONDecodeError:
|
|
516
|
+
continue
|
|
517
|
+
except OSError:
|
|
518
|
+
return
|
|
519
|
+
records = [record for record in records if isinstance(record, dict)]
|
|
520
|
+
header = next((record for record in records if record.get("type") == "session"), {})
|
|
521
|
+
session_id = header.get("id") or path.stem
|
|
522
|
+
cwd = header.get("cwd")
|
|
523
|
+
messages = [record for record in records if record.get("type") == "message"]
|
|
524
|
+
active_ids = pi_active_entry_ids(records)
|
|
525
|
+
for index, record in enumerate(messages):
|
|
526
|
+
if active_ids is not None and record.get("id") not in active_ids:
|
|
527
|
+
continue
|
|
528
|
+
message = record.get("message")
|
|
529
|
+
if not isinstance(message, dict):
|
|
530
|
+
continue
|
|
531
|
+
role = message.get("role")
|
|
532
|
+
if role not in ("user", "assistant"):
|
|
533
|
+
continue
|
|
534
|
+
raw_text = pi_text(message.get("content"))
|
|
535
|
+
if role == "user":
|
|
536
|
+
text = clean_user_text(raw_text)
|
|
537
|
+
if text is not None:
|
|
538
|
+
yield {
|
|
539
|
+
"session_id": session_id, "harness": "pi",
|
|
540
|
+
"uuid": record.get("id") or f"{path}:{index}",
|
|
541
|
+
"timestamp": record.get("timestamp"), "cwd": cwd,
|
|
542
|
+
"git_branch": None, "role": "user", "text": text,
|
|
543
|
+
"condensed": truncate(text, max_context),
|
|
544
|
+
}
|
|
545
|
+
else:
|
|
546
|
+
yield {
|
|
547
|
+
"session_id": session_id, "harness": "pi",
|
|
548
|
+
"uuid": record.get("id") or f"{path}:{index}",
|
|
549
|
+
"timestamp": record.get("timestamp"), "cwd": cwd,
|
|
550
|
+
"git_branch": None, "role": "assistant", "text": None,
|
|
551
|
+
"condensed": truncate(raw_text, max_context) if raw_text else "",
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def find_pi_files(pi_home, cutoff):
|
|
556
|
+
root = pi_home / "sessions"
|
|
557
|
+
if not root.is_dir():
|
|
558
|
+
return []
|
|
559
|
+
files = []
|
|
560
|
+
for path in root.glob("*/*.jsonl"):
|
|
561
|
+
try:
|
|
562
|
+
mtime = datetime.fromtimestamp(path.stat().st_mtime, tz=timezone.utc)
|
|
563
|
+
except OSError:
|
|
564
|
+
continue
|
|
565
|
+
if mtime >= cutoff:
|
|
566
|
+
files.append(path)
|
|
567
|
+
return sorted(files)
|
|
568
|
+
|
|
569
|
+
|
|
471
570
|
# ---------------------------------------------------------------------------
|
|
472
571
|
# Timestamp / scope helpers
|
|
473
572
|
# ---------------------------------------------------------------------------
|
|
@@ -563,6 +662,7 @@ def parse_args(argv=None):
|
|
|
563
662
|
p.add_argument("--all-conversations", action="store_true")
|
|
564
663
|
p.add_argument("--claude-home", default=os.environ.get("CLAUDE_CONFIG_DIR", "~/.claude"))
|
|
565
664
|
p.add_argument("--codex-home", default="~/.codex")
|
|
665
|
+
p.add_argument("--pi-home", default=os.environ.get("PI_CODING_AGENT_DIR", "~/.pi/agent"))
|
|
566
666
|
p.add_argument("--patterns", default=None, help="extra newline-separated regex patterns")
|
|
567
667
|
p.add_argument("--max-context", type=int, default=400,
|
|
568
668
|
help="chars of preceding-assistant excerpt to keep (default: 400)")
|
|
@@ -589,23 +689,41 @@ def main(argv=None):
|
|
|
589
689
|
|
|
590
690
|
claude_home = Path(args.claude_home).expanduser()
|
|
591
691
|
codex_home = Path(args.codex_home).expanduser()
|
|
692
|
+
pi_home = Path(args.pi_home).expanduser()
|
|
592
693
|
cutoff = datetime.now(timezone.utc) - timedelta(days=args.days)
|
|
593
694
|
repo_norms = [normalize_repo(r) for r in args.repo]
|
|
594
695
|
|
|
595
696
|
patterns = compile_patterns(args.patterns)
|
|
596
697
|
|
|
597
698
|
claude_files = find_claude_files(claude_home, cutoff)
|
|
598
|
-
|
|
599
|
-
|
|
699
|
+
codex_files = find_codex_files(codex_home, cutoff)
|
|
700
|
+
pi_files = find_pi_files(pi_home, cutoff)
|
|
701
|
+
roots = {
|
|
702
|
+
"claude": (claude_home / "projects",),
|
|
703
|
+
"codex": (codex_home / "sessions", codex_home / "archived_sessions"),
|
|
704
|
+
"pi": (pi_home / "sessions",),
|
|
705
|
+
}
|
|
706
|
+
if not any(any(root.is_dir() for root in paths) for paths in roots.values()):
|
|
707
|
+
print("error: no supported session source found (Claude Code, Codex, or Pi)",
|
|
600
708
|
file=sys.stderr)
|
|
601
709
|
sys.exit(1)
|
|
602
710
|
|
|
603
|
-
|
|
604
|
-
sources = [
|
|
605
|
-
if
|
|
606
|
-
sources.append("
|
|
607
|
-
|
|
608
|
-
|
|
711
|
+
files_by_source = {"claude": claude_files, "codex": codex_files, "pi": pi_files}
|
|
712
|
+
sources = []
|
|
713
|
+
if roots["claude"][0].is_dir():
|
|
714
|
+
sources.append("claude")
|
|
715
|
+
sources.extend(source for source in ("codex", "pi") if files_by_source[source])
|
|
716
|
+
source_status = {
|
|
717
|
+
source: {
|
|
718
|
+
"status": "scanned" if files else "no_recent_sessions" if any(
|
|
719
|
+
root.is_dir() for root in roots[source]
|
|
720
|
+
) else "not_found",
|
|
721
|
+
"session_files": len(files),
|
|
722
|
+
"paths": [str(root) for root in roots[source]],
|
|
723
|
+
}
|
|
724
|
+
for source, files in files_by_source.items()
|
|
725
|
+
}
|
|
726
|
+
session_files_scanned = sum(len(files) for files in files_by_source.values())
|
|
609
727
|
|
|
610
728
|
# session_id -> {"turns": {turn_key: turn_dict}}
|
|
611
729
|
sessions = {}
|
|
@@ -619,6 +737,11 @@ def main(argv=None):
|
|
|
619
737
|
bucket = sessions.setdefault(turn["session_id"], {})
|
|
620
738
|
key = f"codex:{turn['uuid']}"
|
|
621
739
|
bucket[key] = turn
|
|
740
|
+
for path in pi_files:
|
|
741
|
+
for turn in iter_pi_turns(path, args.max_context):
|
|
742
|
+
bucket = sessions.setdefault(turn["session_id"], {})
|
|
743
|
+
key = f"pi:{turn['uuid']}"
|
|
744
|
+
bucket[key] = turn
|
|
622
745
|
|
|
623
746
|
# Pass 1: walk every in-scope session and collect pattern-matched
|
|
624
747
|
# candidates. Nothing here is decided to be a real correction yet --
|
|
@@ -744,6 +867,8 @@ def main(argv=None):
|
|
|
744
867
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
745
868
|
"window_days": args.days,
|
|
746
869
|
"sources": sources,
|
|
870
|
+
"source_status": source_status,
|
|
871
|
+
"unsupported_sources": ["grok", "zcode"],
|
|
747
872
|
"stats": {
|
|
748
873
|
"session_files_scanned": session_files_scanned,
|
|
749
874
|
"sessions_scanned": sessions_scanned,
|
|
@@ -763,7 +888,10 @@ def main(argv=None):
|
|
|
763
888
|
(out_dir / "corrections.json").write_text(json.dumps(report, indent=2))
|
|
764
889
|
|
|
765
890
|
print(f"corrections.json: {out_dir / 'corrections.json'}")
|
|
766
|
-
print(f"sources: {', '.join(sources)}")
|
|
891
|
+
print(f"sources: {', '.join(sources) if sources else 'none in window'}")
|
|
892
|
+
for source, status in source_status.items():
|
|
893
|
+
print(f" {source}: {status['status']} ({status['session_files']} files)")
|
|
894
|
+
print("not supported: Grok Build, ZCode")
|
|
767
895
|
print(f"session files: {session_files_scanned} scanned")
|
|
768
896
|
print(f"sessions: {sessions_scanned} scanned, {sessions_in_scope} in scope")
|
|
769
897
|
print(f"user turns: {user_turns_total}")
|
|
@@ -14,6 +14,8 @@ This file is the single source of truth for harness support in `doctor-md-skill`
|
|
|
14
14
|
|
|
15
15
|
At startup, identify the harness executing the skill from the runtime context. Do not infer it from conversation files found on disk.
|
|
16
16
|
|
|
17
|
+
Pi session parsing uses the session header's `cwd` for project scope and follows the active entry's `parentId` ancestry when the entry links form a valid tree. Non-message entries establish the branch path but do not become transcript turns; tool-result messages are not user turns. Older or incomplete logs without valid parent links are read in recorded message order, so their inactive branches cannot be distinguished.
|
|
18
|
+
|
|
17
19
|
If the executing harness is not listed above, or cannot be identified confidently, stop before creating a report directory or reading conversation history. Tell the user:
|
|
18
20
|
|
|
19
21
|
> doctor-md-skill currently supports Claude Code, Codex, Pi, Grok Build, and ZCode. This run appears to be using an unsupported harness, so no conversations were read.
|
|
@@ -437,6 +437,24 @@ class CodexReader(Reader):
|
|
|
437
437
|
return self._meta or {"id": self.path.stem, "cwd": None, "started_at": first_ts}
|
|
438
438
|
|
|
439
439
|
|
|
440
|
+
def pi_active_entry_ids(records):
|
|
441
|
+
"""Return Pi's active session-entry ancestry when parent links are complete."""
|
|
442
|
+
entries = [record for record in records if record.get("type") != "session"]
|
|
443
|
+
if not entries or any(
|
|
444
|
+
not entry.get("id") or "parentId" not in entry for entry in entries
|
|
445
|
+
):
|
|
446
|
+
return None
|
|
447
|
+
by_id = {entry["id"]: entry for entry in entries}
|
|
448
|
+
active = set()
|
|
449
|
+
current = entries[-1]["id"]
|
|
450
|
+
while current is not None:
|
|
451
|
+
if current in active or current not in by_id:
|
|
452
|
+
return None
|
|
453
|
+
active.add(current)
|
|
454
|
+
current = by_id[current].get("parentId")
|
|
455
|
+
return active
|
|
456
|
+
|
|
457
|
+
|
|
440
458
|
class PiReader(Reader):
|
|
441
459
|
key = "pi"
|
|
442
460
|
|
|
@@ -472,10 +490,16 @@ class PiReader(Reader):
|
|
|
472
490
|
self._meta["started_at"] = self._meta.get("started_at") or ts
|
|
473
491
|
|
|
474
492
|
def events(self):
|
|
475
|
-
|
|
493
|
+
records = list(read_jsonl(self.path))
|
|
494
|
+
active_ids = pi_active_entry_ids(records)
|
|
495
|
+
for obj in records:
|
|
496
|
+
kind = obj.get("type")
|
|
497
|
+
if kind not in ("session", "message"):
|
|
498
|
+
continue
|
|
499
|
+
if kind == "message" and active_ids is not None and obj.get("id") not in active_ids:
|
|
500
|
+
continue
|
|
476
501
|
if obj.get("timestamp"):
|
|
477
502
|
yield Event(STAMP, obj["timestamp"])
|
|
478
|
-
kind = obj.get("type")
|
|
479
503
|
if kind == "session":
|
|
480
504
|
self._read_header(obj)
|
|
481
505
|
elif kind == "message" and isinstance(obj.get("message"), dict):
|