paperfit-cli 1.0.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/.claude/commands/adjust-length.md +21 -0
- package/.claude/commands/check-visual.md +27 -0
- package/.claude/commands/fix-layout.md +31 -0
- package/.claude/commands/migrate-template.md +23 -0
- package/.claude/commands/repair-table.md +21 -0
- package/.claude/commands/show-status.md +32 -0
- package/.claude-plugin/README.md +77 -0
- package/.claude-plugin/marketplace.json +41 -0
- package/.claude-plugin/plugin.json +39 -0
- package/CLAUDE.md +266 -0
- package/CONTRIBUTING.md +131 -0
- package/LICENSE +21 -0
- package/README.md +164 -0
- package/agents/code-surgeon-agent.md +214 -0
- package/agents/layout-detective-agent.md +229 -0
- package/agents/orchestrator-agent.md +254 -0
- package/agents/quality-gatekeeper-agent.md +270 -0
- package/agents/rule-engine-agent.md +224 -0
- package/agents/semantic-polish-agent.md +250 -0
- package/bin/paperfit.js +176 -0
- package/config/agent_roles.yaml +56 -0
- package/config/layout_rules.yaml +54 -0
- package/config/templates.yaml +241 -0
- package/config/vto_taxonomy.yaml +489 -0
- package/config/writing_rules.yaml +64 -0
- package/install.sh +30 -0
- package/package.json +52 -0
- package/requirements.txt +5 -0
- package/scripts/benchmark_runner.py +629 -0
- package/scripts/compile.sh +244 -0
- package/scripts/config_validator.py +339 -0
- package/scripts/cv_detector.py +600 -0
- package/scripts/evidence_collector.py +167 -0
- package/scripts/float_fixers.py +861 -0
- package/scripts/inject_defects.py +549 -0
- package/scripts/install-claude-global.js +148 -0
- package/scripts/install.js +66 -0
- package/scripts/install.sh +106 -0
- package/scripts/overflow_fixers.py +656 -0
- package/scripts/package-for-opensource.sh +138 -0
- package/scripts/parse_log.py +260 -0
- package/scripts/postinstall.js +38 -0
- package/scripts/pre_tool_use.py +265 -0
- package/scripts/render_pages.py +244 -0
- package/scripts/session_logger.py +329 -0
- package/scripts/space_util_fixers.py +773 -0
- package/scripts/state_manager.py +352 -0
- package/scripts/test_commands.py +187 -0
- package/scripts/test_cv_detector.py +214 -0
- package/scripts/test_integration.py +290 -0
- package/skills/consistency-polisher/SKILL.md +337 -0
- package/skills/float-optimizer/SKILL.md +284 -0
- package/skills/latex_fixers/__init__.py +82 -0
- package/skills/latex_fixers/float_fixers.py +392 -0
- package/skills/latex_fixers/fullwidth_fixers.py +375 -0
- package/skills/latex_fixers/overflow_fixers.py +250 -0
- package/skills/latex_fixers/semantic_micro_tuning.py +362 -0
- package/skills/latex_fixers/space_util_fixers.py +389 -0
- package/skills/latex_fixers/utils.py +55 -0
- package/skills/overflow-repair/SKILL.md +304 -0
- package/skills/space-util-fixer/SKILL.md +307 -0
- package/skills/taxonomy-vto/SKILL.md +486 -0
- package/skills/template-migrator/SKILL.md +251 -0
- package/skills/visual-inspector/SKILL.md +217 -0
- package/skills/writing-polish/SKILL.md +289 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PDF 页图渲染器
|
|
4
|
+
|
|
5
|
+
将 LaTeX 编译生成的 PDF 文件转换为逐页高分辨率 PNG 图片,
|
|
6
|
+
供 layout-detective-agent 和 visual-inspector 使用。
|
|
7
|
+
|
|
8
|
+
依赖:
|
|
9
|
+
- pdf2image
|
|
10
|
+
- Poppler (系统安装)
|
|
11
|
+
|
|
12
|
+
用法:
|
|
13
|
+
python render_pages.py <pdf_file> [--output <dir>] [--dpi <value>] [--pages <range>]
|
|
14
|
+
|
|
15
|
+
示例:
|
|
16
|
+
python render_pages.py main.pdf --output data/pages --dpi 220
|
|
17
|
+
python render_pages.py main.pdf --pages 1-5,7 --dpi 320
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
import json
|
|
23
|
+
import argparse
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import List, Optional, Tuple, Dict, Any
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
from pdf2image import convert_from_path
|
|
29
|
+
from PIL import Image
|
|
30
|
+
except ImportError as e:
|
|
31
|
+
print(f"Error: Missing required library. {e}")
|
|
32
|
+
print("Install with: pip install pdf2image pillow")
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PDFRenderer:
|
|
37
|
+
"""PDF 页图渲染器"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, pdf_path: str, output_dir: str = "data/pages", dpi: int = 220):
|
|
40
|
+
self.pdf_path = Path(pdf_path)
|
|
41
|
+
self.output_dir = Path(output_dir)
|
|
42
|
+
self.dpi = dpi
|
|
43
|
+
self.page_files: List[Dict[str, Any]] = []
|
|
44
|
+
self.errors: List[str] = []
|
|
45
|
+
|
|
46
|
+
def render_pages(self, page_range: Optional[str] = None) -> Dict[str, Any]:
|
|
47
|
+
"""渲染指定页码范围的页面"""
|
|
48
|
+
if not self.pdf_path.exists():
|
|
49
|
+
return {"status": "failed", "error": f"PDF not found: {self.pdf_path}"}
|
|
50
|
+
|
|
51
|
+
# 创建输出目录
|
|
52
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
# 解析页码范围
|
|
56
|
+
page_numbers = self._parse_page_range(page_range)
|
|
57
|
+
|
|
58
|
+
# 渲染页面
|
|
59
|
+
pages = convert_from_path(
|
|
60
|
+
str(self.pdf_path),
|
|
61
|
+
dpi=self.dpi,
|
|
62
|
+
fmt='png',
|
|
63
|
+
thread_count=2,
|
|
64
|
+
grayscale=False,
|
|
65
|
+
first_page=min(page_numbers) if page_numbers else None,
|
|
66
|
+
last_page=max(page_numbers) if page_numbers else None
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# 确定起始页码(如果指定了范围,first_page 已生效,但索引需对齐)
|
|
70
|
+
start_page = min(page_numbers) if page_numbers else 1
|
|
71
|
+
|
|
72
|
+
for i, page_image in enumerate(pages):
|
|
73
|
+
page_num = start_page + i
|
|
74
|
+
if page_numbers and page_num not in page_numbers:
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
file_name = f"page_{page_num:03d}.png"
|
|
78
|
+
file_path = self.output_dir / file_name
|
|
79
|
+
page_image.save(str(file_path), "PNG")
|
|
80
|
+
|
|
81
|
+
self.page_files.append({
|
|
82
|
+
"page": page_num,
|
|
83
|
+
"file": str(file_path),
|
|
84
|
+
"width": page_image.width,
|
|
85
|
+
"height": page_image.height
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
return self._build_report()
|
|
89
|
+
|
|
90
|
+
except Exception as e:
|
|
91
|
+
error_msg = f"PDF rendering failed: {str(e)}"
|
|
92
|
+
self.errors.append(error_msg)
|
|
93
|
+
|
|
94
|
+
# 检查是否是 Poppler 缺失
|
|
95
|
+
if "poppler" in str(e).lower() or "pdfinfo" in str(e).lower():
|
|
96
|
+
self.errors.append(
|
|
97
|
+
"Poppler not found. Install with:\n"
|
|
98
|
+
" Ubuntu/Debian: sudo apt-get install poppler-utils\n"
|
|
99
|
+
" macOS: brew install poppler\n"
|
|
100
|
+
" Windows: download from http://blog.alivate.com.au/poppler-windows/"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return self._build_report(status="failed")
|
|
104
|
+
|
|
105
|
+
def render_cropped_region(
|
|
106
|
+
self,
|
|
107
|
+
page: int,
|
|
108
|
+
bbox: Tuple[int, int, int, int],
|
|
109
|
+
object_name: str,
|
|
110
|
+
high_dpi: int = 320
|
|
111
|
+
) -> Optional[Dict[str, Any]]:
|
|
112
|
+
"""
|
|
113
|
+
渲染指定页面的局部区域(用于表格/公式复查)
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
page: 页码
|
|
117
|
+
bbox: (x1, y1, x2, y2) 裁剪区域坐标
|
|
118
|
+
object_name: 对象标识,用于文件命名
|
|
119
|
+
high_dpi: 高精度渲染时的 DPI
|
|
120
|
+
"""
|
|
121
|
+
try:
|
|
122
|
+
pages = convert_from_path(
|
|
123
|
+
str(self.pdf_path),
|
|
124
|
+
dpi=high_dpi,
|
|
125
|
+
fmt='png',
|
|
126
|
+
first_page=page,
|
|
127
|
+
last_page=page
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if not pages:
|
|
131
|
+
self.errors.append(f"Page {page} not found")
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
page_image = pages[0]
|
|
135
|
+
cropped = page_image.crop(bbox)
|
|
136
|
+
|
|
137
|
+
file_name = f"page_{page:03d}_{object_name}.png"
|
|
138
|
+
file_path = self.output_dir / file_name
|
|
139
|
+
cropped.save(str(file_path), "PNG")
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
"page": page,
|
|
143
|
+
"object": object_name,
|
|
144
|
+
"file": str(file_path),
|
|
145
|
+
"bbox": bbox,
|
|
146
|
+
"width": cropped.width,
|
|
147
|
+
"height": cropped.height
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
except Exception as e:
|
|
151
|
+
self.errors.append(f"Cropped rendering failed: {str(e)}")
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
def _parse_page_range(self, range_str: Optional[str]) -> List[int]:
|
|
155
|
+
"""解析页码范围字符串,如 '1-5,7,9-11'"""
|
|
156
|
+
if not range_str:
|
|
157
|
+
return []
|
|
158
|
+
|
|
159
|
+
pages = set()
|
|
160
|
+
parts = range_str.split(',')
|
|
161
|
+
|
|
162
|
+
for part in parts:
|
|
163
|
+
part = part.strip()
|
|
164
|
+
if '-' in part:
|
|
165
|
+
start, end = part.split('-')
|
|
166
|
+
pages.update(range(int(start), int(end) + 1))
|
|
167
|
+
else:
|
|
168
|
+
pages.add(int(part))
|
|
169
|
+
|
|
170
|
+
return sorted(pages)
|
|
171
|
+
|
|
172
|
+
def _build_report(self, status: str = "success") -> Dict[str, Any]:
|
|
173
|
+
"""构建渲染报告"""
|
|
174
|
+
report = {
|
|
175
|
+
"skill": "visual-inspector",
|
|
176
|
+
"status": status if not self.errors else "failed",
|
|
177
|
+
"pdf_path": str(self.pdf_path),
|
|
178
|
+
"output_dir": str(self.output_dir),
|
|
179
|
+
"dpi": self.dpi,
|
|
180
|
+
"pages_rendered": len(self.page_files),
|
|
181
|
+
"page_files": self.page_files,
|
|
182
|
+
"errors": self.errors
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if status == "success" and not self.errors:
|
|
186
|
+
report["status"] = "success"
|
|
187
|
+
elif self.page_files and self.errors:
|
|
188
|
+
report["status"] = "partial"
|
|
189
|
+
|
|
190
|
+
return report
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def main():
|
|
194
|
+
parser = argparse.ArgumentParser(
|
|
195
|
+
description="Render PDF pages to PNG images for visual inspection"
|
|
196
|
+
)
|
|
197
|
+
parser.add_argument("pdf_file", help="Path to PDF file")
|
|
198
|
+
parser.add_argument("--output", "-o", default="data/pages", help="Output directory")
|
|
199
|
+
parser.add_argument("--dpi", type=int, default=220, help="Rendering DPI (default: 220)")
|
|
200
|
+
parser.add_argument("--pages", "-p", help="Page range (e.g., '1-5,7,9-11')")
|
|
201
|
+
parser.add_argument("--json", "-j", action="store_true", help="Output JSON report only")
|
|
202
|
+
parser.add_argument("--crop", help="Crop region: 'page,x1,y1,x2,y2,object_name'")
|
|
203
|
+
|
|
204
|
+
args = parser.parse_args()
|
|
205
|
+
|
|
206
|
+
renderer = PDFRenderer(args.pdf_file, args.output, args.dpi)
|
|
207
|
+
|
|
208
|
+
if args.crop:
|
|
209
|
+
# 解析裁剪参数
|
|
210
|
+
parts = args.crop.split(',')
|
|
211
|
+
if len(parts) != 6:
|
|
212
|
+
print("Error: --crop requires 'page,x1,y1,x2,y2,object_name'")
|
|
213
|
+
sys.exit(1)
|
|
214
|
+
|
|
215
|
+
page = int(parts[0])
|
|
216
|
+
bbox = (int(parts[1]), int(parts[2]), int(parts[3]), int(parts[4]))
|
|
217
|
+
object_name = parts[5]
|
|
218
|
+
|
|
219
|
+
result = renderer.render_cropped_region(page, bbox, object_name)
|
|
220
|
+
if result:
|
|
221
|
+
print(json.dumps(result, indent=2))
|
|
222
|
+
else:
|
|
223
|
+
print(json.dumps({"status": "failed", "errors": renderer.errors}, indent=2))
|
|
224
|
+
sys.exit(1)
|
|
225
|
+
else:
|
|
226
|
+
report = renderer.render_pages(args.pages)
|
|
227
|
+
|
|
228
|
+
if args.json:
|
|
229
|
+
print(json.dumps(report, indent=2, ensure_ascii=False))
|
|
230
|
+
else:
|
|
231
|
+
if report["status"] in ("success", "partial"):
|
|
232
|
+
print(f"Rendered {report['pages_rendered']} pages to {report['output_dir']}")
|
|
233
|
+
for pf in report["page_files"]:
|
|
234
|
+
print(f" Page {pf['page']}: {pf['file']} ({pf['width']}x{pf['height']})")
|
|
235
|
+
if report.get("errors"):
|
|
236
|
+
print("\nErrors:", file=sys.stderr)
|
|
237
|
+
for err in report["errors"]:
|
|
238
|
+
print(f" - {err}", file=sys.stderr)
|
|
239
|
+
|
|
240
|
+
sys.exit(0 if report["status"] == "success" else 1)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
if __name__ == "__main__":
|
|
244
|
+
main()
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PaperFit Session Logger
|
|
4
|
+
|
|
5
|
+
Provides observability for PaperFit agent sessions.
|
|
6
|
+
Inspired by ECC's observability features (dimension 8).
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
- Session-level logging
|
|
10
|
+
- Agent decision tracking
|
|
11
|
+
- Token usage estimation
|
|
12
|
+
- Metrics collection
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
python session_logger.py start <session_id>
|
|
16
|
+
python session_logger.py log <session_id> <message>
|
|
17
|
+
python session_logger.py track <session_id> <agent> <action>
|
|
18
|
+
python session_logger.py metrics <session_id>
|
|
19
|
+
python session_logger.py export <session_id>
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import json
|
|
24
|
+
import sys
|
|
25
|
+
from datetime import datetime
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Optional
|
|
28
|
+
|
|
29
|
+
# Session log directory
|
|
30
|
+
LOG_DIR = Path(__file__).parent.parent / 'data' / 'logs'
|
|
31
|
+
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SessionLogger:
|
|
35
|
+
"""Manages session logging and metrics collection."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, session_id: str):
|
|
38
|
+
self.session_id = session_id
|
|
39
|
+
self.log_file = LOG_DIR / f'{session_id}.jsonl'
|
|
40
|
+
self.meta_file = LOG_DIR / f'{session_id}.meta.json'
|
|
41
|
+
|
|
42
|
+
def start(self, user_id: str = 'anonymous', project: str = None):
|
|
43
|
+
"""Initialize a new session."""
|
|
44
|
+
meta = {
|
|
45
|
+
'session_id': self.session_id,
|
|
46
|
+
'user_id': user_id,
|
|
47
|
+
'project': project,
|
|
48
|
+
'started_at': datetime.utcnow().isoformat(),
|
|
49
|
+
'ended_at': None,
|
|
50
|
+
'status': 'active',
|
|
51
|
+
'events': [],
|
|
52
|
+
'agents': {},
|
|
53
|
+
'metrics': {
|
|
54
|
+
'total_events': 0,
|
|
55
|
+
'agent_calls': 0,
|
|
56
|
+
'file_edits': 0,
|
|
57
|
+
'compile_runs': 0,
|
|
58
|
+
'visual_defects_found': 0,
|
|
59
|
+
'visual_defects_fixed': 0,
|
|
60
|
+
'iterations': 0
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
with open(self.meta_file, 'w', encoding='utf-8') as f:
|
|
65
|
+
json.dump(meta, f, indent=2)
|
|
66
|
+
|
|
67
|
+
# Write initial log entry
|
|
68
|
+
self._log_event('session_start', {
|
|
69
|
+
'user_id': user_id,
|
|
70
|
+
'project': project
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
print(f"✅ Session started: {self.session_id}")
|
|
74
|
+
print(f" Log file: {self.log_file}")
|
|
75
|
+
print(f" Meta file: {self.meta_file}")
|
|
76
|
+
return meta
|
|
77
|
+
|
|
78
|
+
def _log_event(self, event_type: str, data: dict):
|
|
79
|
+
"""Append an event to the session log."""
|
|
80
|
+
event = {
|
|
81
|
+
'timestamp': datetime.utcnow().isoformat(),
|
|
82
|
+
'event_type': event_type,
|
|
83
|
+
'data': data
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
with open(self.log_file, 'a', encoding='utf-8') as f:
|
|
87
|
+
f.write(json.dumps(event) + '\n')
|
|
88
|
+
|
|
89
|
+
# Update meta
|
|
90
|
+
if self.meta_file.exists():
|
|
91
|
+
with open(self.meta_file, 'r', encoding='utf-8') as f:
|
|
92
|
+
meta = json.load(f)
|
|
93
|
+
|
|
94
|
+
meta['events'].append(event_type)
|
|
95
|
+
meta['metrics']['total_events'] += 1
|
|
96
|
+
|
|
97
|
+
with open(self.meta_file, 'w', encoding='utf-8') as f:
|
|
98
|
+
json.dump(meta, f, indent=2)
|
|
99
|
+
|
|
100
|
+
def log(self, message: str, category: str = 'info'):
|
|
101
|
+
"""Log a general message."""
|
|
102
|
+
self._log_event(category, {'message': message})
|
|
103
|
+
print(f"[{category.upper()}] {message}")
|
|
104
|
+
|
|
105
|
+
def track_agent(self, agent_name: str, action: str, details: dict = None):
|
|
106
|
+
"""Track an agent invocation."""
|
|
107
|
+
event_data = {
|
|
108
|
+
'agent': agent_name,
|
|
109
|
+
'action': action,
|
|
110
|
+
'details': details or {}
|
|
111
|
+
}
|
|
112
|
+
self._log_event('agent_call', event_data)
|
|
113
|
+
|
|
114
|
+
# Update agent stats
|
|
115
|
+
if self.meta_file.exists():
|
|
116
|
+
with open(self.meta_file, 'r', encoding='utf-8') as f:
|
|
117
|
+
meta = json.load(f)
|
|
118
|
+
|
|
119
|
+
if agent_name not in meta['agents']:
|
|
120
|
+
meta['agents'][agent_name] = {'calls': 0, 'actions': {}}
|
|
121
|
+
|
|
122
|
+
meta['agents'][agent_name]['calls'] += 1
|
|
123
|
+
if action not in meta['agents'][agent_name]['actions']:
|
|
124
|
+
meta['agents'][agent_name]['actions'][action] = 0
|
|
125
|
+
meta['agents'][agent_name]['actions'][action] += 1
|
|
126
|
+
meta['metrics']['agent_calls'] += 1
|
|
127
|
+
|
|
128
|
+
with open(self.meta_file, 'w', encoding='utf-8') as f:
|
|
129
|
+
json.dump(meta, f, indent=2)
|
|
130
|
+
|
|
131
|
+
def track_file_edit(self, file_path: str, reason: str):
|
|
132
|
+
"""Track a file edit."""
|
|
133
|
+
self._log_event('file_edit', {
|
|
134
|
+
'file': file_path,
|
|
135
|
+
'reason': reason
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
if self.meta_file.exists():
|
|
139
|
+
with open(self.meta_file, 'r', encoding='utf-8') as f:
|
|
140
|
+
meta = json.load(f)
|
|
141
|
+
meta['metrics']['file_edits'] += 1
|
|
142
|
+
with open(self.meta_file, 'w', encoding='utf-8') as f:
|
|
143
|
+
json.dump(meta, f, indent=2)
|
|
144
|
+
|
|
145
|
+
def track_compile(self, success: bool, errors: list = None):
|
|
146
|
+
"""Track a compilation run."""
|
|
147
|
+
self._log_event('compile', {
|
|
148
|
+
'success': success,
|
|
149
|
+
'errors': errors or []
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
if self.meta_file.exists():
|
|
153
|
+
with open(self.meta_file, 'r', encoding='utf-8') as f:
|
|
154
|
+
meta = json.load(f)
|
|
155
|
+
meta['metrics']['compile_runs'] += 1
|
|
156
|
+
with open(self.meta_file, 'w', encoding='utf-8') as f:
|
|
157
|
+
json.dump(meta, f, indent=2)
|
|
158
|
+
|
|
159
|
+
def track_defect(self, category: str, page: int, severity: str, fixed: bool = False):
|
|
160
|
+
"""Track a visual defect."""
|
|
161
|
+
self._log_event('defect', {
|
|
162
|
+
'category': category,
|
|
163
|
+
'page': page,
|
|
164
|
+
'severity': severity,
|
|
165
|
+
'fixed': fixed
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
if self.meta_file.exists():
|
|
169
|
+
with open(self.meta_file, 'r', encoding='utf-8') as f:
|
|
170
|
+
meta = json.load(f)
|
|
171
|
+
|
|
172
|
+
if fixed:
|
|
173
|
+
meta['metrics']['visual_defects_fixed'] += 1
|
|
174
|
+
else:
|
|
175
|
+
meta['metrics']['visual_defects_found'] += 1
|
|
176
|
+
|
|
177
|
+
with open(self.meta_file, 'w', encoding='utf-8') as f:
|
|
178
|
+
json.dump(meta, f, indent=2)
|
|
179
|
+
|
|
180
|
+
def track_iteration(self, round_num: int, status: str):
|
|
181
|
+
"""Track an iteration round."""
|
|
182
|
+
self._log_event('iteration', {
|
|
183
|
+
'round': round_num,
|
|
184
|
+
'status': status
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
if self.meta_file.exists():
|
|
188
|
+
with open(self.meta_file, 'r', encoding='utf-8') as f:
|
|
189
|
+
meta = json.load(f)
|
|
190
|
+
meta['metrics']['iterations'] += 1
|
|
191
|
+
with open(self.meta_file, 'w', encoding='utf-8') as f:
|
|
192
|
+
json.dump(meta, f, indent=2)
|
|
193
|
+
|
|
194
|
+
def end(self, status: str = 'completed'):
|
|
195
|
+
"""End the session."""
|
|
196
|
+
self._log_event('session_end', {'status': status})
|
|
197
|
+
|
|
198
|
+
if self.meta_file.exists():
|
|
199
|
+
with open(self.meta_file, 'r', encoding='utf-8') as f:
|
|
200
|
+
meta = json.load(f)
|
|
201
|
+
|
|
202
|
+
meta['ended_at'] = datetime.utcnow().isoformat()
|
|
203
|
+
meta['status'] = status
|
|
204
|
+
|
|
205
|
+
with open(self.meta_file, 'w', encoding='utf-8') as f:
|
|
206
|
+
json.dump(meta, f, indent=2)
|
|
207
|
+
|
|
208
|
+
print(f"✅ Session ended: {self.session_id} ({status})")
|
|
209
|
+
|
|
210
|
+
def get_metrics(self) -> dict:
|
|
211
|
+
"""Get session metrics."""
|
|
212
|
+
if not self.meta_file.exists():
|
|
213
|
+
return {'error': 'Session not found'}
|
|
214
|
+
|
|
215
|
+
with open(self.meta_file, 'r', encoding='utf-8') as f:
|
|
216
|
+
meta = json.load(f)
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
'session_id': meta['session_id'],
|
|
220
|
+
'duration': self._calculate_duration(meta['started_at'], meta['ended_at']),
|
|
221
|
+
'status': meta['status'],
|
|
222
|
+
'metrics': meta['metrics'],
|
|
223
|
+
'agents': meta['agents']
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
def _calculate_duration(self, start: str, end: Optional[str]) -> str:
|
|
227
|
+
"""Calculate session duration."""
|
|
228
|
+
try:
|
|
229
|
+
start_dt = datetime.fromisoformat(start)
|
|
230
|
+
end_dt = datetime.fromisoformat(end) if end else datetime.utcnow()
|
|
231
|
+
duration = end_dt - start_dt
|
|
232
|
+
return str(duration)
|
|
233
|
+
except Exception:
|
|
234
|
+
return 'unknown'
|
|
235
|
+
|
|
236
|
+
def export(self) -> str:
|
|
237
|
+
"""Export full session data."""
|
|
238
|
+
if not self.meta_file.exists():
|
|
239
|
+
return json.dumps({'error': 'Session not found'})
|
|
240
|
+
|
|
241
|
+
# Load meta
|
|
242
|
+
with open(self.meta_file, 'r', encoding='utf-8') as f:
|
|
243
|
+
meta = json.load(f)
|
|
244
|
+
|
|
245
|
+
# Load events
|
|
246
|
+
events = []
|
|
247
|
+
if self.log_file.exists():
|
|
248
|
+
with open(self.log_file, 'r', encoding='utf-8') as f:
|
|
249
|
+
for line in f:
|
|
250
|
+
events.append(json.loads(line))
|
|
251
|
+
|
|
252
|
+
return json.dumps({
|
|
253
|
+
'meta': meta,
|
|
254
|
+
'events': events,
|
|
255
|
+
'event_count': len(events)
|
|
256
|
+
}, indent=2, default=str)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def generate_session_id() -> str:
|
|
260
|
+
"""Generate a unique session ID."""
|
|
261
|
+
return datetime.utcnow().strftime('%Y%m%d_%H%M%S')
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def main():
|
|
265
|
+
parser = argparse.ArgumentParser(description='PaperFit Session Logger')
|
|
266
|
+
parser.add_argument('command', choices=['start', 'log', 'track', 'metrics', 'export', 'end'])
|
|
267
|
+
parser.add_argument('session_id', nargs='?', help='Session ID')
|
|
268
|
+
parser.add_argument('--user', default='anonymous', help='User ID')
|
|
269
|
+
parser.add_argument('--project', help='Project name')
|
|
270
|
+
parser.add_argument('--message', help='Log message')
|
|
271
|
+
parser.add_argument('--category', default='info', help='Log category')
|
|
272
|
+
parser.add_argument('--agent', help='Agent name')
|
|
273
|
+
parser.add_argument('--action', help='Agent action')
|
|
274
|
+
parser.add_argument('--details', help='Agent action details (JSON)')
|
|
275
|
+
parser.add_argument('--file', help='File path for file edit')
|
|
276
|
+
parser.add_argument('--reason', help='Reason for file edit')
|
|
277
|
+
parser.add_argument('--status', default='completed', help='End status')
|
|
278
|
+
|
|
279
|
+
args = parser.parse_args()
|
|
280
|
+
|
|
281
|
+
if args.command == 'start':
|
|
282
|
+
session_id = args.session_id or generate_session_id()
|
|
283
|
+
logger = SessionLogger(session_id)
|
|
284
|
+
logger.start(args.user, args.project)
|
|
285
|
+
|
|
286
|
+
elif args.command == 'log':
|
|
287
|
+
if not args.session_id:
|
|
288
|
+
print('Error: session_id required')
|
|
289
|
+
sys.exit(1)
|
|
290
|
+
logger = SessionLogger(args.session_id)
|
|
291
|
+
logger.log(args.message or 'No message', args.category)
|
|
292
|
+
|
|
293
|
+
elif args.command == 'track':
|
|
294
|
+
if not args.session_id:
|
|
295
|
+
print('Error: session_id required')
|
|
296
|
+
sys.exit(1)
|
|
297
|
+
logger = SessionLogger(args.session_id)
|
|
298
|
+
if args.agent and args.action:
|
|
299
|
+
details = json.loads(args.details) if args.details else None
|
|
300
|
+
logger.track_agent(args.agent, args.action, details)
|
|
301
|
+
else:
|
|
302
|
+
print('Error: --agent and --action required')
|
|
303
|
+
sys.exit(1)
|
|
304
|
+
|
|
305
|
+
elif args.command == 'metrics':
|
|
306
|
+
if not args.session_id:
|
|
307
|
+
print('Error: session_id required')
|
|
308
|
+
sys.exit(1)
|
|
309
|
+
logger = SessionLogger(args.session_id)
|
|
310
|
+
metrics = logger.get_metrics()
|
|
311
|
+
print(json.dumps(metrics, indent=2, default=str))
|
|
312
|
+
|
|
313
|
+
elif args.command == 'export':
|
|
314
|
+
if not args.session_id:
|
|
315
|
+
print('Error: session_id required')
|
|
316
|
+
sys.exit(1)
|
|
317
|
+
logger = SessionLogger(args.session_id)
|
|
318
|
+
print(logger.export())
|
|
319
|
+
|
|
320
|
+
elif args.command == 'end':
|
|
321
|
+
if not args.session_id:
|
|
322
|
+
print('Error: session_id required')
|
|
323
|
+
sys.exit(1)
|
|
324
|
+
logger = SessionLogger(args.session_id)
|
|
325
|
+
logger.end(args.status)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
if __name__ == '__main__':
|
|
329
|
+
main()
|