personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add sol call journal imports/import commands for import manifest querying

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+176
+176
think/tools/call.py
··· 489 489 else: 490 490 typer.echo(f"Agent '{agent}' not found and no outputs exist.", err=True) 491 491 raise typer.Exit(1) 492 + 493 + 494 + # ============================================================================ 495 + # Import Commands 496 + # ============================================================================ 497 + 498 + 499 + def _derive_status(info: dict) -> str: 500 + """Derive import status from info dict fields.""" 501 + if info.get("error"): 502 + return "failed" 503 + if info.get("processed"): 504 + return "success" 505 + if info.get("task_id"): 506 + return "running" 507 + return "pending" 508 + 509 + 510 + def _get_source_type(journal_root: Path, timestamp: str, info: dict) -> str: 511 + """Get source type from manifest.json or infer from mime_type.""" 512 + manifest_path = journal_root / "imports" / timestamp / "manifest.json" 513 + if manifest_path.exists(): 514 + try: 515 + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) 516 + if manifest.get("source_type"): 517 + return manifest["source_type"] 518 + except Exception: 519 + pass 520 + 521 + # Infer from mime_type 522 + mime = info.get("mime_type", "") 523 + if "calendar" in mime or "ics" in mime: 524 + return "ics" 525 + if "zip" in mime: 526 + return "archive" 527 + if "audio" in mime: 528 + return "audio" 529 + if "text" in mime: 530 + return "text" 531 + return "unknown" 532 + 533 + 534 + def _get_entry_count(journal_root: Path, timestamp: str, info: dict) -> int | None: 535 + """Get entry count from manifest.json or total_files_created.""" 536 + manifest_path = journal_root / "imports" / timestamp / "manifest.json" 537 + if manifest_path.exists(): 538 + try: 539 + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) 540 + if manifest.get("entry_count") is not None: 541 + return manifest["entry_count"] 542 + except Exception: 543 + pass 544 + count = info.get("total_files_created") 545 + if count is not None and count > 0: 546 + return count 547 + return None 548 + 549 + 550 + def _match_import_id(timestamps: list[str], prefix: str) -> str | None: 551 + """Match a partial import ID prefix to a full timestamp.""" 552 + matches = [t for t in timestamps if t.startswith(prefix)] 553 + if len(matches) == 1: 554 + return matches[0] 555 + if len(matches) > 1: 556 + # Check for exact match first 557 + if prefix in matches: 558 + return prefix 559 + typer.echo( 560 + f"Ambiguous prefix '{prefix}' matches {len(matches)} imports. " 561 + "Be more specific.", 562 + err=True, 563 + ) 564 + raise typer.Exit(1) 565 + return None 566 + 567 + 568 + @app.command(name="imports") 569 + def imports_list( 570 + limit: int = typer.Option(20, "--limit", "-n", help="Max results."), 571 + source: str | None = typer.Option( 572 + None, "--source", "-s", help="Filter by source type." 573 + ), 574 + json_output: bool = typer.Option(False, "--json", help="Output as JSON."), 575 + ) -> None: 576 + """List recent imports with metadata.""" 577 + journal_root = Path(get_journal()) 578 + timestamps = list_import_timestamps(journal_root) 579 + 580 + if not timestamps: 581 + typer.echo("No imports found.") 582 + return 583 + 584 + # Reverse chronological 585 + timestamps.sort(reverse=True) 586 + 587 + # Build info for each import 588 + rows = [] 589 + for ts in timestamps: 590 + info = build_import_info(journal_root, ts) 591 + source_type = _get_source_type(journal_root, ts, info) 592 + 593 + if source and source_type != source: 594 + continue 595 + 596 + status = _derive_status(info) 597 + filename = info.get("original_filename", "unknown") 598 + entry_count = _get_entry_count(journal_root, ts, info) 599 + 600 + rows.append( 601 + { 602 + "timestamp": ts, 603 + "status": status, 604 + "source_type": source_type, 605 + "filename": filename, 606 + "entry_count": entry_count, 607 + "error": info.get("error"), 608 + } 609 + ) 610 + 611 + if len(rows) >= limit: 612 + break 613 + 614 + if not rows: 615 + typer.echo("No imports found.") 616 + return 617 + 618 + if json_output: 619 + typer.echo(json.dumps(rows, indent=2)) 620 + return 621 + 622 + for row in rows: 623 + parts = [f"{row['timestamp']} [{row['status']}]"] 624 + parts.append(f"{row['source_type']:8s}") 625 + parts.append(row["filename"]) 626 + if row["status"] == "failed" and row.get("error"): 627 + parts.append(f"— error: {row['error']}") 628 + elif row.get("entry_count") is not None: 629 + parts.append(f"({row['entry_count']} entries)") 630 + typer.echo(" ".join(parts)) 631 + 632 + 633 + @app.command(name="import") 634 + def import_detail( 635 + id: str = typer.Argument(help="Import ID or prefix (e.g. 20260309_143000)."), 636 + ) -> None: 637 + """Show full metadata for a single import.""" 638 + journal_root = Path(get_journal()) 639 + timestamps = list_import_timestamps(journal_root) 640 + 641 + if not timestamps: 642 + typer.echo("No imports found.", err=True) 643 + raise typer.Exit(1) 644 + 645 + # Match partial prefix 646 + matched = _match_import_id(timestamps, id) 647 + if matched is None: 648 + typer.echo(f"Import '{id}' not found.", err=True) 649 + raise typer.Exit(1) 650 + 651 + info = build_import_info(journal_root, matched) 652 + info["status"] = _derive_status(info) 653 + info["source_type"] = _get_source_type(journal_root, matched, info) 654 + 655 + # Merge manifest and detail data 656 + try: 657 + details = get_import_details(journal_root, matched) 658 + if details.get("import_json"): 659 + info["import_metadata"] = details["import_json"] 660 + if details.get("imported_json"): 661 + info["imported_results"] = details["imported_json"] 662 + if details.get("segments_json"): 663 + info["segments"] = details["segments_json"] 664 + except FileNotFoundError: 665 + pass 666 + 667 + typer.echo(json.dumps(info, indent=2, default=str))