ai-worklog 1.0.5 → 1.0.6

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": "ai-worklog",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "AI 对话工作日志自动收集工具 —— 从 Claude Code / Codex 对话记录生成每日工作日志并推送到 GitLab",
5
5
  "bin": {
6
6
  "ai-worklog": "./bin/index.js"
@@ -19,6 +19,80 @@ from pathlib import Path
19
19
  from typing import Optional
20
20
 
21
21
 
22
+ # ─── TUI 输出 ────────────────────────────────────────────────────────────────
23
+
24
+ _COLOR = sys.stdout.isatty()
25
+
26
+ def _c(text: str, code: str) -> str:
27
+ return f"\033[{code}m{text}\033[0m" if _COLOR else text
28
+
29
+ def bold(t): return _c(t, "1")
30
+ def dim(t): return _c(t, "2")
31
+ def green(t): return _c(t, "32")
32
+ def cyan(t): return _c(t, "36")
33
+ def yellow(t): return _c(t, "33")
34
+ def gray(t): return _c(t, "90")
35
+ def red(t): return _c(t, "31")
36
+ def blue(t): return _c(t, "34")
37
+
38
+
39
+ def print_header(date_str: str, incremental: bool = False, prev_time: str = "") -> None:
40
+ line = f" ai-worklog · {date_str} "
41
+ w = max(len(line), 44)
42
+ print(f"\n╭{'─' * w}╮")
43
+ print(f"│{bold(line):<{w + (10 if _COLOR else 0)}}│")
44
+ print(f"╰{'─' * w}╯")
45
+ if incremental:
46
+ print(f"\n {yellow('↻')} 增量更新模式 {dim(f'上次生成于 {prev_time}')}")
47
+
48
+
49
+ def print_step(msg: str) -> None:
50
+ print(f"\n{bold(cyan('●'))} {bold(msg)}")
51
+
52
+
53
+ def print_ok(msg: str) -> None:
54
+ print(f" {green('✓')} {msg}")
55
+
56
+
57
+ def print_info(msg: str) -> None:
58
+ print(f" {dim('│')} {dim(msg)}")
59
+
60
+
61
+ def print_warn(msg: str) -> None:
62
+ print(f" {yellow('⚠')} {msg}", file=sys.stderr)
63
+
64
+
65
+ def print_fail(msg: str) -> None:
66
+ print(f" {red('✗')} {msg}", file=sys.stderr)
67
+
68
+
69
+ def print_project_tree(data: dict, is_sessions: bool = True) -> None:
70
+ """打印项目列表(树形)"""
71
+ if not data:
72
+ print(f" {gray('└─')} {gray('(无记录)')}")
73
+ return
74
+ items = list(data.items())
75
+ max_len = max(len(p) for p in data)
76
+ for i, (proj, val) in enumerate(items):
77
+ connector = gray("└─") if i == len(items) - 1 else gray("├─")
78
+ if is_sessions:
79
+ n_sessions = len(val)
80
+ n_turns = sum(len(t) for t in val)
81
+ stats = gray(f"{n_sessions} 对话 {n_turns} 轮")
82
+ else:
83
+ stats = gray(f"{len(val)} 条")
84
+ print(f" {connector} {bold(proj.ljust(max_len))} {stats}")
85
+
86
+
87
+ def print_footer(log_file: Path, total_projects: int, total_turns: int) -> None:
88
+ w = 46
89
+ print(f"\n{'─' * w}")
90
+ rel = str(log_file).replace(str(Path.home()), "~")
91
+ print(f" {green('✓')} {bold('完成')} {total_projects} 个项目 · {total_turns} 轮对话")
92
+ print(f" {dim(rel)}")
93
+ print(f"{'─' * w}\n")
94
+
95
+
22
96
  # ─── 配置 ───────────────────────────────────────────────────────────────────
23
97
 
24
98
  WORKLOG_CONFIG_FILE = Path.home() / ".config" / "worklog.json"
@@ -467,7 +541,6 @@ def save_log(target_date: str, content: str) -> Path:
467
541
  log_dir.mkdir(parents=True, exist_ok=True)
468
542
  log_file = log_dir / f"{target_date}.md"
469
543
  log_file.write_text(content, encoding="utf-8")
470
- print(f"日志已保存: {log_file}")
471
544
  return log_file
472
545
 
473
546
 
@@ -636,67 +709,67 @@ def git_commit_and_push(log_file: Path, target_date: str, push: bool = True) ->
636
709
  env["LC_ALL"] = "C" # 强制英文输出,保证字符串匹配不受本地化影响
637
710
  r = subprocess.run(cmd, cwd=REPO_DIR, capture_output=True, text=True, env=env)
638
711
  if check_err and r.returncode != 0:
639
- print(f"命令失败: {' '.join(cmd)}\n{r.stderr.strip()}", file=sys.stderr)
712
+ print_fail(f"{' '.join(cmd[:2])}: {r.stderr.strip()}")
640
713
  return r
641
714
 
715
+ print_step("同步 Git")
716
+
642
717
  # ── 1. 初始化 git 仓库 ──────────────────────────────────────────────────
643
718
  is_git = run(["git", "rev-parse", "--git-dir"], check_err=False).returncode == 0
644
719
  if not is_git:
645
- print("Git: 初始化仓库...")
720
+ print_info("初始化仓库...")
646
721
  if run(["git", "init"]).returncode != 0:
647
722
  return
648
723
  run(["git", "checkout", "-b", "main"], check_err=False)
724
+ print_ok("git init")
649
725
 
650
726
  # ── 2. 检查并创建 GitLab 远程(无需 glab)────────────────────────────────
651
727
  has_remote = run(["git", "remote", "get-url", "origin"], check_err=False).returncode == 0
652
728
  if not has_remote:
653
729
  repo_name = REPO_DIR.name
654
- print(f"GitLab: 创建公开项目 {repo_name} ...")
730
+ print_info(f"GitLab 创建公开项目 {repo_name} ...")
655
731
  try:
656
732
  cfg = load_gitlab_config()
657
733
  ssh_url = gitlab_create_project(cfg, repo_name)
658
734
  run(["git", "remote", "add", "origin", ssh_url])
659
- print(f"GitLab: 项目已创建 → {ssh_url}")
735
+ print_ok(f"远程仓库 → {ssh_url}")
660
736
  except Exception as e:
661
- print(f"GitLab 创建失败: {e}", file=sys.stderr)
662
- print("请手动设置 remote: git remote add origin <url>", file=sys.stderr)
737
+ print_fail(f"GitLab 创建失败: {e}")
738
+ print_info("请手动: git remote add origin <url>")
663
739
  return
664
740
 
665
741
  # ── 3. 确保有初始提交 ─────────────────────────────────────────────────────
666
742
  if run(["git", "rev-parse", "HEAD"], check_err=False).returncode != 0:
667
- print("Git: 创建初始提交...")
668
743
  run(["git", "add", "-A"])
669
744
  run(["git", "commit", "-m", "init: 初始化工作日志仓库"])
670
745
 
671
746
  # ── 4. 提交日志 ───────────────────────────────────────────────────────────
672
747
  rel_path = log_file.relative_to(REPO_DIR)
673
- print(f"Git: 添加 {rel_path}")
674
748
  if run(["git", "add", str(rel_path)]).returncode != 0:
675
749
  return
676
750
 
677
751
  commit_msg = f"docs: 工作日志 {target_date}"
678
- print(f"Git: 提交 '{commit_msg}'")
679
752
  r = run(["git", "commit", "-m", commit_msg], check_err=False)
680
753
  if r.returncode != 0:
681
754
  if "nothing to commit" in r.stdout + r.stderr:
682
- print("Git: 无变更,跳过提交(已在初始提交中)")
755
+ print_info("无新变更,跳过提交")
683
756
  else:
684
- print(f"Git commit 失败: {r.stderr.strip()}", file=sys.stderr)
757
+ print_fail(f"commit 失败: {r.stderr.strip()}")
685
758
  return
686
- # nothing to commit 不代表不需要 push,继续走推送流程
759
+ else:
760
+ print_ok(f"commit: {dim(commit_msg)}")
687
761
 
688
762
  # ── 5. 推送 ────────────────────────────────────────────────────────────────
689
763
  if push:
690
- print("Git: 推送到远程...")
691
764
  r = run(["git", "push", "--set-upstream", "origin", "main"], check_err=False)
692
765
  if r.returncode != 0:
693
766
  r = run(["git", "push"])
694
767
  if r.returncode == 0:
695
- print("Git: 推送成功 ")
768
+ print_ok("push origin/main")
696
769
  else:
697
- print(f"Git push 失败: {r.stderr.strip()}", file=sys.stderr)
770
+ print_fail(f"push 失败: {r.stderr.strip()}")
698
771
  else:
699
- print("Git: 已跳过 push(--no-push 模式)")
772
+ print_info("已跳过 push(--no-push")
700
773
 
701
774
 
702
775
  # ─── 主入口 ───────────────────────────────────────────────────────────────────
@@ -741,68 +814,61 @@ def main():
741
814
  args = parser.parse_args()
742
815
 
743
816
  target_date = parse_date_arg(args.date)
744
- print(f"=== 收集 {target_date} 的工作日志 ===")
745
817
 
746
- # 1. 检查是否存在 checkpoint(判断是否为增量更新)
818
+ # 1. 检查 checkpoint
747
819
  checkpoint = load_checkpoint(target_date)
748
820
  is_incremental = checkpoint is not None
749
821
  prev_file_counts = checkpoint["file_counts"] if is_incremental else {}
822
+ prev_time = checkpoint["generated_at"].split("T")[-1] if is_incremental else ""
750
823
 
751
- if is_incremental:
752
- print(f"检测到已有日志(生成于 {checkpoint['generated_at']}),进入增量更新模式")
824
+ print_header(target_date, is_incremental, prev_time)
753
825
 
754
- # 2. 收集数据(增量模式只返回新增轮次)
755
- print("收集 Claude Code 对话记录...")
756
- claude_data, file_counts = collect_claude_sessions(target_date, prev_file_counts if is_incremental else None)
826
+ # 2. 收集 Claude Code 对话
827
+ print_step("收集 Claude Code 对话记录")
828
+ claude_data, file_counts = collect_claude_sessions(
829
+ target_date, prev_file_counts if is_incremental else None
830
+ )
831
+ print_project_tree(claude_data, is_sessions=True)
757
832
  total_claude_turns = sum(sum(len(t) for t in s) for s in claude_data.values())
758
- total_claude_sessions = sum(len(s) for s in claude_data.values())
759
- label = "新增" if is_incremental else "找到"
760
- print(f" {label} {len(claude_data)} 个项目,{total_claude_sessions} 个对话,{total_claude_turns} 轮交互")
761
833
 
762
- print("收集 Codex 对话记录...")
834
+ # 3. 收集 Codex 对话
835
+ print_step("收集 Codex 对话记录")
763
836
  codex_data = collect_codex_sessions(target_date)
837
+ print_project_tree(codex_data, is_sessions=False)
764
838
  total_codex = sum(len(v) for v in codex_data.values())
765
- print(f" {label} {len(codex_data)} 个项目,{total_codex} 条消息")
839
+
840
+ total_projects = len(set(list(claude_data.keys()) + list(codex_data.keys())))
841
+ total_turns = total_claude_turns + total_codex
766
842
 
767
843
  if args.dry_run:
768
- print(f"\n[dry-run] {'新增' if is_incremental else ''}数据预览:")
769
- for proj, sessions in claude_data.items():
770
- total_turns = sum(len(t) for t in sessions)
771
- print(f" [Claude/{proj}] {len(sessions)} 个对话,{total_turns} 轮")
772
- for si, turns in enumerate(sessions[:2], 1):
773
- print(f" 对话{si}: {len(turns)} 轮")
774
- for user_msg, asst_msg in turns[:2]:
775
- print(f" [用户] {user_msg[:60]}")
776
- if asst_msg:
777
- print(f" [Claude] {asst_msg[:60]}")
778
- for proj, msgs in codex_data.items():
779
- print(f" [Codex/{proj}] {len(msgs)} 条")
780
- for m in msgs[:2]:
781
- print(f" - {m[:60]}")
844
+ print(f"\n{dim('[dry-run] 预览完毕,未调用 API')}\n")
782
845
  return
783
846
 
784
- # 3. 生成日志内容
847
+ # 4. 生成日志
785
848
  if is_incremental:
786
849
  if total_claude_turns == 0 and total_codex == 0:
787
- print("没有新增内容,无需更新")
850
+ print(f"\n {yellow('─')} 没有新增内容,无需更新\n")
788
851
  return
789
- print("调用 Claude API 更新日志(增量)...")
852
+ print_step(f"调用 {bold(MODEL)} 更新日志(增量)")
790
853
  log_file_path = LOGS_DIR / target_date[:4] / f"{target_date}.md"
791
854
  existing_log = log_file_path.read_text(encoding="utf-8") if log_file_path.exists() else ""
792
855
  content = generate_incremental_update(target_date, existing_log, claude_data, codex_data)
793
856
  else:
794
- print("调用 Claude API 生成摘要...")
857
+ print_step(f"调用 {bold(MODEL)} 生成摘要")
795
858
  content = generate_summary(target_date, claude_data, codex_data)
796
859
 
797
- # 4. 保存文件 + checkpoint
860
+ # 5. 保存文件 + checkpoint
861
+ print_step("保存日志")
798
862
  log_file = save_log(target_date, content)
799
863
  save_checkpoint(target_date, file_counts)
864
+ rel = str(log_file).replace(str(Path.home()), "~")
865
+ print_ok(rel)
800
866
 
801
- # 5. Git 操作
867
+ # 6. Git 操作
802
868
  if not args.no_git:
803
869
  git_commit_and_push(log_file, target_date, push=not args.no_push)
804
870
 
805
- print(f"\n完成!日志文件: {log_file}")
871
+ print_footer(log_file, total_projects, total_turns)
806
872
 
807
873
 
808
874
  if __name__ == "__main__":