laporan-generator 2.6.2 → 2.7.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/.agents/skills/laporan-generator/SKILL.md +99 -109
- package/.agents/skills/laporan-generator/resources/laporan +37 -4
- package/.agents/skills/laporan-generator/resources/laporan.ps1 +74 -28
- package/.agents/skills/laporan-generator/resources/scripts/bundle.py +11 -2
- package/.agents/skills/laporan-generator/resources/scripts/report-doctor.py +121 -48
- package/.agents/skills/laporan-generator/resources/scripts/report-stats.py +53 -2
- package/.agents/skills/laporan-generator/resources/scripts/test_scripts.py +63 -2
- package/.agents/skills/laporan-generator/scripts/bundle.py +11 -2
- package/.agents/skills/laporan-generator/scripts/finalize-docx.py +140 -1
- package/.agents/skills/laporan-generator/scripts/make-reference-docx.py +42 -2
- package/.agents/skills/laporan-generator/scripts/report-doctor.py +121 -48
- package/.agents/skills/laporan-generator/scripts/report-stats.py +53 -2
- package/.agents/skills/laporan-generator/scripts/test_scripts.py +88 -2
- package/README.md +44 -0
- package/bin/laporan-generator.js +15 -3
- package/bin/mcp-server.js +4 -0
- package/laporan +37 -4
- package/laporan.ps1 +50 -11
- package/lib/init.js +115 -44
- package/lib/mcp.js +569 -0
- package/lib/sync-hosts.js +196 -68
- package/lib/uninstall.js +21 -0
- package/package.json +9 -3
- package/scripts/bundle.py +11 -2
- package/scripts/report-doctor.py +121 -48
- package/scripts/report-stats.py +46 -2
- package/scripts/test_scripts.py +50 -3
|
@@ -2,11 +2,19 @@
|
|
|
2
2
|
"""report-doctor.py: Audit komprehensif kesehatan dan integritas proyek laporan."""
|
|
3
3
|
|
|
4
4
|
import glob
|
|
5
|
+
import json
|
|
5
6
|
import os
|
|
6
7
|
import re
|
|
7
8
|
import shutil
|
|
8
9
|
import sys
|
|
9
10
|
|
|
11
|
+
if sys.platform == "win32":
|
|
12
|
+
try:
|
|
13
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
14
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
15
|
+
except Exception:
|
|
16
|
+
pass
|
|
17
|
+
|
|
10
18
|
GREEN = "\033[0;32m"
|
|
11
19
|
BLUE = "\033[0;34m"
|
|
12
20
|
CYAN = "\033[0;36m"
|
|
@@ -98,101 +106,166 @@ def check_style_anomalies(markdown_files):
|
|
|
98
106
|
|
|
99
107
|
|
|
100
108
|
def main():
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
json_mode = "--json" in sys.argv
|
|
110
|
+
pos_args = [a for a in sys.argv[1:] if not a.startswith("-")]
|
|
111
|
+
|
|
112
|
+
target_dir = os.getcwd()
|
|
113
|
+
if pos_args:
|
|
114
|
+
target_dir = os.path.abspath(pos_args[0])
|
|
115
|
+
elif not (os.path.isfile(os.path.join(target_dir, "metadata.yml")) or os.path.isdir(os.path.join(target_dir, "chapters"))):
|
|
116
|
+
pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
117
|
+
if os.path.isfile(os.path.join(pkg_root, "metadata.yml")):
|
|
118
|
+
target_dir = pkg_root
|
|
119
|
+
|
|
120
|
+
os.chdir(target_dir)
|
|
121
|
+
|
|
122
|
+
if not json_mode:
|
|
123
|
+
print(f"{CYAN}{BOLD}")
|
|
124
|
+
print(" ========================================================")
|
|
125
|
+
print(" LAPORAN GENERATOR - DOCTOR & AUDIT SUITE ")
|
|
126
|
+
print(" ========================================================")
|
|
127
|
+
print(f"{NC}")
|
|
109
128
|
|
|
110
129
|
errors = 0
|
|
111
130
|
warnings = 0
|
|
112
131
|
|
|
113
132
|
# 1. Dependensi
|
|
114
|
-
|
|
133
|
+
if not json_mode:
|
|
134
|
+
print(f" {BLUE}{BOLD}[1/4] Audit Toolchain & Dependensi Sistem{NC}")
|
|
115
135
|
deps, has_im = check_dependencies()
|
|
136
|
+
dep_list = []
|
|
116
137
|
for cmd, name, req, found in deps:
|
|
117
138
|
if "ImageMagick" in name:
|
|
118
139
|
continue
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
print(f" {RED}[FAIL]{NC} {name:<24}: TIDAK DITEMUKAN (Wajib)")
|
|
124
|
-
errors += 1
|
|
140
|
+
dep_list.append({"command": cmd, "name": name, "required": req, "found": found})
|
|
141
|
+
if not json_mode:
|
|
142
|
+
if found:
|
|
143
|
+
print(f" {GREEN}[OK]{NC} {name:<26}: Terpasang ({cmd})")
|
|
125
144
|
else:
|
|
126
|
-
|
|
145
|
+
if req:
|
|
146
|
+
print(f" {RED}[FAIL]{NC} {name:<24}: TIDAK DITEMUKAN (Wajib)")
|
|
147
|
+
else:
|
|
148
|
+
print(f" {YELLOW}[INFO]{NC} {name:<24}: Tidak ditemukan (Opsional)")
|
|
149
|
+
if not found and req:
|
|
150
|
+
errors += 1
|
|
127
151
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
152
|
+
dep_list.append({"command": "magick/convert", "name": "ImageMagick", "required": False, "found": has_im})
|
|
153
|
+
if not json_mode:
|
|
154
|
+
if has_im:
|
|
155
|
+
print(f" {GREEN}[OK]{NC} {'ImageMagick':<26}: Terpasang")
|
|
156
|
+
else:
|
|
157
|
+
print(f" {YELLOW}[INFO]{NC} {'ImageMagick':<24}: Tidak ditemukan (Opsional untuk kompresi gambar)")
|
|
158
|
+
print("")
|
|
133
159
|
|
|
134
160
|
# 2. Berkas & Struktur
|
|
135
|
-
|
|
161
|
+
if not json_mode:
|
|
162
|
+
print(f" {BLUE}{BOLD}[2/4] Audit Struktur Direktori & Berkas Wajib{NC}")
|
|
136
163
|
req_files = [
|
|
137
164
|
"metadata.yml", "template.typ", "reference.docx",
|
|
138
165
|
"references.bib", "docx.lua", "cover.md"
|
|
139
166
|
]
|
|
167
|
+
file_status = []
|
|
140
168
|
for rf in req_files:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
169
|
+
exists = os.path.isfile(rf)
|
|
170
|
+
file_status.append({"file": rf, "found": exists})
|
|
171
|
+
if not json_mode:
|
|
172
|
+
if exists:
|
|
173
|
+
print(f" {GREEN}[OK]{NC} Berkas {rf}")
|
|
174
|
+
else:
|
|
175
|
+
print(f" {RED}[FAIL]{NC} Berkas {rf} TIDAK DITEMUKAN")
|
|
176
|
+
if not exists:
|
|
145
177
|
errors += 1
|
|
146
178
|
|
|
147
179
|
chapter_files = sorted(glob.glob("chapters/*.md"))
|
|
148
|
-
if
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
180
|
+
if not json_mode:
|
|
181
|
+
if chapter_files:
|
|
182
|
+
print(f" {GREEN}[OK]{NC} Direktori chapters/ ({len(chapter_files)} bab ditemukan)")
|
|
183
|
+
else:
|
|
184
|
+
print(f" {RED}[FAIL]{NC} Tidak ada berkas markdown di chapters/")
|
|
185
|
+
if not chapter_files:
|
|
152
186
|
errors += 1
|
|
153
|
-
|
|
187
|
+
if not json_mode:
|
|
188
|
+
print("")
|
|
154
189
|
|
|
155
190
|
# 3. Sitasi & Referensi
|
|
156
|
-
|
|
191
|
+
if not json_mode:
|
|
192
|
+
print(f" {BLUE}{BOLD}[3/4] Audit Sitasi & Integritas Bibliografi{NC}")
|
|
157
193
|
bib_keys, cited_keys, missing_keys, orphan_keys = check_citations("references.bib", chapter_files)
|
|
158
|
-
|
|
159
|
-
|
|
194
|
+
if not json_mode:
|
|
195
|
+
print(f" * Total entri .bib : {len(bib_keys)}")
|
|
196
|
+
print(f" * Total sitasi di teks : {len(cited_keys)}")
|
|
160
197
|
|
|
161
198
|
if missing_keys:
|
|
162
199
|
for k, locs in missing_keys.items():
|
|
163
200
|
loc_str = ", ".join([f"{f}:{l}" for f, l in locs])
|
|
164
|
-
|
|
201
|
+
if not json_mode:
|
|
202
|
+
print(f" {RED}[ERROR]{NC} Sitasi '@{k}' TIDAK DITEMUKAN di references.bib ({loc_str})")
|
|
165
203
|
errors += 1
|
|
166
|
-
|
|
204
|
+
elif not json_mode:
|
|
167
205
|
print(f" {GREEN}[OK]{NC} Semua sitasi dalam teks valid terdaftar di references.bib")
|
|
168
206
|
|
|
169
207
|
if orphan_keys:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
208
|
+
if not json_mode:
|
|
209
|
+
print(f" {YELLOW}[WARN]{NC} {len(orphan_keys)} entri di references.bib belum disitir dalam teks:")
|
|
210
|
+
for ok in list(orphan_keys)[:5]:
|
|
211
|
+
print(f" - @{ok}")
|
|
212
|
+
if len(orphan_keys) > 5:
|
|
213
|
+
print(f" - ... dan {len(orphan_keys) - 5} lainnya")
|
|
175
214
|
warnings += 1
|
|
176
|
-
|
|
215
|
+
if not json_mode:
|
|
216
|
+
print("")
|
|
177
217
|
|
|
178
218
|
# 4. Gambar & Format Teks
|
|
179
|
-
|
|
219
|
+
if not json_mode:
|
|
220
|
+
print(f" {BLUE}{BOLD}[4/4] Audit Media Citra & Anomali Tipografi{NC}")
|
|
180
221
|
total_imgs, broken_imgs = check_images(chapter_files)
|
|
181
222
|
if broken_imgs:
|
|
182
223
|
for p, f, l in broken_imgs:
|
|
183
|
-
|
|
224
|
+
if not json_mode:
|
|
225
|
+
print(f" {RED}[ERROR]{NC} Berkas gambar tidak ditemukan: '{p}' (di {f}:{l})")
|
|
184
226
|
errors += 1
|
|
185
|
-
|
|
227
|
+
elif not json_mode:
|
|
186
228
|
print(f" {GREEN}[OK]{NC} Semua tautan gambar ({total_imgs} gambar) valid dan ada di disk")
|
|
187
229
|
|
|
188
230
|
anomalies = check_style_anomalies(chapter_files)
|
|
189
231
|
if anomalies:
|
|
190
232
|
for f, l, msg in anomalies:
|
|
191
|
-
|
|
233
|
+
if not json_mode:
|
|
234
|
+
print(f" {YELLOW}[WARN]{NC} {f}:{l} - {msg}")
|
|
192
235
|
warnings += 1
|
|
193
|
-
|
|
236
|
+
elif not json_mode:
|
|
194
237
|
print(f" {GREEN}[OK]{NC} Bebas dari manual heading numbering dan box-drawing characters")
|
|
195
|
-
|
|
238
|
+
if not json_mode:
|
|
239
|
+
print("")
|
|
240
|
+
|
|
241
|
+
score = 100 if (errors == 0 and warnings == 0) else (95 if errors == 0 else max(0, 90 - errors * 15 - warnings * 5))
|
|
242
|
+
status_label = "HEALTHY" if errors == 0 and warnings == 0 else ("WARNING" if errors == 0 else "ERROR")
|
|
243
|
+
|
|
244
|
+
if json_mode:
|
|
245
|
+
result = {
|
|
246
|
+
"status": status_label,
|
|
247
|
+
"score": score,
|
|
248
|
+
"errors_count": errors,
|
|
249
|
+
"warnings_count": warnings,
|
|
250
|
+
"target_dir": os.getcwd(),
|
|
251
|
+
"dependencies": dep_list,
|
|
252
|
+
"required_files": file_status,
|
|
253
|
+
"chapters_count": len(chapter_files),
|
|
254
|
+
"chapters": [os.path.basename(c) for c in chapter_files],
|
|
255
|
+
"citations": {
|
|
256
|
+
"bib_count": len(bib_keys),
|
|
257
|
+
"cited_count": len(cited_keys),
|
|
258
|
+
"missing_keys": {k: [f"{f}:{l}" for f, l in locs] for k, locs in missing_keys.items()},
|
|
259
|
+
"orphan_keys": list(orphan_keys),
|
|
260
|
+
},
|
|
261
|
+
"images": {
|
|
262
|
+
"total": total_imgs,
|
|
263
|
+
"broken": [{"path": p, "file": f, "line": l} for p, f, l in broken_imgs],
|
|
264
|
+
},
|
|
265
|
+
"anomalies": [{"file": f, "line": l, "message": msg} for f, l, msg in anomalies],
|
|
266
|
+
}
|
|
267
|
+
print(json.dumps(result, indent=2))
|
|
268
|
+
return 1 if errors > 0 else 0
|
|
196
269
|
|
|
197
270
|
# Skor Kesehatan
|
|
198
271
|
print(" ========================================================")
|
|
@@ -2,10 +2,18 @@
|
|
|
2
2
|
"""report-stats.py: Menghitung statistik komprehensif dokumen laporan akademik."""
|
|
3
3
|
|
|
4
4
|
import glob
|
|
5
|
+
import json
|
|
5
6
|
import os
|
|
6
7
|
import re
|
|
7
8
|
import sys
|
|
8
9
|
|
|
10
|
+
if sys.platform == "win32":
|
|
11
|
+
try:
|
|
12
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
13
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
14
|
+
except Exception:
|
|
15
|
+
pass
|
|
16
|
+
|
|
9
17
|
GREEN = "\033[0;32m"
|
|
10
18
|
BLUE = "\033[0;34m"
|
|
11
19
|
CYAN = "\033[0;36m"
|
|
@@ -48,8 +56,18 @@ def count_file_stats(filepath):
|
|
|
48
56
|
|
|
49
57
|
|
|
50
58
|
def main():
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
json_mode = "--json" in sys.argv
|
|
60
|
+
pos_args = [a for a in sys.argv[1:] if not a.startswith("-")]
|
|
61
|
+
|
|
62
|
+
target_dir = os.getcwd()
|
|
63
|
+
if pos_args:
|
|
64
|
+
target_dir = os.path.abspath(pos_args[0])
|
|
65
|
+
elif not (os.path.isfile(os.path.join(target_dir, "metadata.yml")) or os.path.isdir(os.path.join(target_dir, "chapters"))):
|
|
66
|
+
pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
67
|
+
if os.path.isfile(os.path.join(pkg_root, "metadata.yml")):
|
|
68
|
+
target_dir = pkg_root
|
|
69
|
+
|
|
70
|
+
os.chdir(target_dir)
|
|
53
71
|
|
|
54
72
|
title = "Laporan Akademik"
|
|
55
73
|
preset = "standard"
|
|
@@ -109,6 +127,39 @@ def main():
|
|
|
109
127
|
sz = os.path.getsize("Laporan.docx")
|
|
110
128
|
docx_size = f"{sz / 1024:.1f} KB"
|
|
111
129
|
|
|
130
|
+
if json_mode:
|
|
131
|
+
result = {
|
|
132
|
+
"title": title,
|
|
133
|
+
"preset": preset,
|
|
134
|
+
"target_dir": os.getcwd(),
|
|
135
|
+
"chapters_count": len(chapter_files),
|
|
136
|
+
"total_words": total_words,
|
|
137
|
+
"total_chars": total_chars,
|
|
138
|
+
"est_pages": est_pages,
|
|
139
|
+
"read_mins": read_mins,
|
|
140
|
+
"headings": {
|
|
141
|
+
"h1": total_h1,
|
|
142
|
+
"h2": total_h2,
|
|
143
|
+
"h3": total_h3,
|
|
144
|
+
},
|
|
145
|
+
"elements": {
|
|
146
|
+
"images": total_images,
|
|
147
|
+
"tables": total_tables,
|
|
148
|
+
"equations": total_equations,
|
|
149
|
+
},
|
|
150
|
+
"citations": {
|
|
151
|
+
"unique_cited": len(all_citations),
|
|
152
|
+
"bib_entries": bib_count,
|
|
153
|
+
},
|
|
154
|
+
"compiled_files": {
|
|
155
|
+
"pdf": pdf_size,
|
|
156
|
+
"docx": docx_size,
|
|
157
|
+
},
|
|
158
|
+
"file_breakdown": [{"filename": f, "words": c} for f, c in file_breakdown],
|
|
159
|
+
}
|
|
160
|
+
print(json.dumps(result, indent=2))
|
|
161
|
+
return
|
|
162
|
+
|
|
112
163
|
print(f"{CYAN}{BOLD}")
|
|
113
164
|
print(" ========================================================")
|
|
114
165
|
print(" LAPORAN GENERATOR - STATISTIK DOKUMEN ")
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""test_scripts.py: Automated unit tests for Laporan Generator Python helper scripts."""
|
|
3
3
|
|
|
4
|
+
import json
|
|
4
5
|
import os
|
|
6
|
+
import subprocess
|
|
5
7
|
import sys
|
|
6
8
|
import unittest
|
|
7
9
|
|
|
@@ -36,16 +38,39 @@ class TestScanPreset(unittest.TestCase):
|
|
|
36
38
|
self.assertIn("UNIVERSITAS INDONESIA", inst)
|
|
37
39
|
|
|
38
40
|
|
|
41
|
+
def get_presets_dir():
|
|
42
|
+
for c in [
|
|
43
|
+
os.path.join(ROOT_DIR, "presets"),
|
|
44
|
+
os.path.join(ROOT_DIR, "resources", "presets"),
|
|
45
|
+
os.path.join(ROOT_DIR, "..", "..", "..", "presets"),
|
|
46
|
+
]:
|
|
47
|
+
if os.path.isdir(c):
|
|
48
|
+
return os.path.abspath(c)
|
|
49
|
+
return os.path.join(ROOT_DIR, "presets")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_project_root():
|
|
53
|
+
for c in [
|
|
54
|
+
ROOT_DIR,
|
|
55
|
+
os.path.join(ROOT_DIR, "resources"),
|
|
56
|
+
os.path.join(ROOT_DIR, "..", "..", ".."),
|
|
57
|
+
]:
|
|
58
|
+
if os.path.isfile(os.path.join(c, "metadata.yml")):
|
|
59
|
+
return os.path.abspath(c)
|
|
60
|
+
return ROOT_DIR
|
|
61
|
+
|
|
62
|
+
|
|
39
63
|
class TestValidatePreset(unittest.TestCase):
|
|
40
64
|
def test_parse_simple_yaml(self):
|
|
41
|
-
|
|
65
|
+
presets_dir = get_presets_dir()
|
|
66
|
+
test_yaml = os.path.join(presets_dir, "standard.yml")
|
|
42
67
|
self.assertTrue(os.path.exists(test_yaml))
|
|
43
68
|
data = validate_preset.parse_simple_yaml(test_yaml)
|
|
44
69
|
self.assertEqual(data.get("preset_id"), "standard")
|
|
45
70
|
self.assertEqual(data.get("margin_top"), "2cm")
|
|
46
71
|
|
|
47
72
|
def test_validate_all_presets(self):
|
|
48
|
-
presets_dir =
|
|
73
|
+
presets_dir = get_presets_dir()
|
|
49
74
|
for f in os.listdir(presets_dir):
|
|
50
75
|
if f.endswith(".yml"):
|
|
51
76
|
path = os.path.join(presets_dir, f)
|
|
@@ -98,6 +123,67 @@ class TestFinalizeDocx(unittest.TestCase):
|
|
|
98
123
|
self.assertEqual(finalize_docx.classify("BAB 2 PEMBAHASAN"), "decimal-start")
|
|
99
124
|
self.assertEqual(finalize_docx.classify("BAB 3 PENUTUP"), "body")
|
|
100
125
|
|
|
126
|
+
def test_get_tab_pos(self):
|
|
127
|
+
sample_standard = '<w:sectPr><w:pgSz w:w="11906"/><w:pgMar w:left="1417" w:right="1417"/></w:sectPr>'
|
|
128
|
+
self.assertEqual(finalize_docx.get_tab_pos(sample_standard), 9072)
|
|
129
|
+
sample_4433 = '<w:sectPr><w:pgSz w:w="11906"/><w:pgMar w:left="2268" w:right="1701"/></w:sectPr>'
|
|
130
|
+
self.assertEqual(finalize_docx.get_tab_pos(sample_4433), 7937)
|
|
131
|
+
self.assertEqual(finalize_docx.get_tab_pos(""), 9072)
|
|
132
|
+
|
|
133
|
+
def test_fix_daftar_isi_title(self):
|
|
134
|
+
raw = '<w:p><w:pPr><w:pStyle w:val="Heading1"/><w:outlineLvl w:val="-1"/></w:pPr><w:r><w:t>DAFTAR ISI</w:t></w:r></w:p>'
|
|
135
|
+
fixed = finalize_docx.fix_daftar_isi_title(raw)
|
|
136
|
+
self.assertIn('w:val="TOCHeading"', fixed)
|
|
137
|
+
self.assertNotIn('w:val="Heading1"', fixed)
|
|
138
|
+
|
|
139
|
+
def test_fix_toc_styles(self):
|
|
140
|
+
styles = '<w:styles></w:styles>'
|
|
141
|
+
res = finalize_docx.fix_toc_styles(styles, 7937)
|
|
142
|
+
self.assertIn('w:styleId="TOCHeading"', res)
|
|
143
|
+
self.assertIn('w:styleId="TOC1"', res)
|
|
144
|
+
self.assertIn('w:styleId="TOC2"', res)
|
|
145
|
+
self.assertIn('w:styleId="TOC3"', res)
|
|
146
|
+
self.assertIn('w:pos="7937"', res)
|
|
147
|
+
self.assertIn('w:left="360"', res)
|
|
148
|
+
self.assertIn('w:left="720"', res)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
report_stats = __import__("report-stats")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class TestReportStats(unittest.TestCase):
|
|
155
|
+
def test_count_file_stats_structure(self):
|
|
156
|
+
sample_md = os.path.join(get_project_root(), "cover.md")
|
|
157
|
+
if os.path.exists(sample_md):
|
|
158
|
+
stats = report_stats.count_file_stats(sample_md)
|
|
159
|
+
self.assertIsInstance(stats, dict)
|
|
160
|
+
self.assertIsInstance(stats["words"], int)
|
|
161
|
+
self.assertIsInstance(stats["chars"], int)
|
|
162
|
+
self.assertGreaterEqual(stats["words"], 0)
|
|
163
|
+
|
|
164
|
+
def test_json_stats_execution(self):
|
|
165
|
+
proj_root = get_project_root()
|
|
166
|
+
cmd = [sys.executable, os.path.join(SCRIPTS_DIR, "report-stats.py"), proj_root, "--json"]
|
|
167
|
+
proc = subprocess.run(cmd, capture_output=True, text=True, cwd=proj_root)
|
|
168
|
+
self.assertEqual(proc.returncode, 0)
|
|
169
|
+
data = json.loads(proc.stdout)
|
|
170
|
+
self.assertIn("total_words", data)
|
|
171
|
+
self.assertIn("preset", data)
|
|
172
|
+
self.assertIn("target_dir", data)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class TestReportDoctor(unittest.TestCase):
|
|
176
|
+
def test_json_doctor_execution(self):
|
|
177
|
+
proj_root = get_project_root()
|
|
178
|
+
cmd = [sys.executable, os.path.join(SCRIPTS_DIR, "report-doctor.py"), proj_root, "--json"]
|
|
179
|
+
proc = subprocess.run(cmd, capture_output=True, text=True, cwd=proj_root)
|
|
180
|
+
data = json.loads(proc.stdout)
|
|
181
|
+
self.assertIn("status", data)
|
|
182
|
+
self.assertIn("dependencies", data)
|
|
183
|
+
self.assertIn("required_files", data)
|
|
184
|
+
self.assertIn("citations", data)
|
|
185
|
+
|
|
101
186
|
|
|
102
187
|
if __name__ == "__main__":
|
|
103
188
|
unittest.main(verbosity=2)
|
|
189
|
+
|
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
<p align="center">
|
|
14
14
|
<a href="https://www.npmjs.com/package/laporan-generator"><img src="https://img.shields.io/npm/v/laporan-generator?style=for-the-badge&color=cb3837&logo=npm" alt="npm version"></a>
|
|
15
|
+
<a href="#dukungan-model-context-protocol-mcp"><img src="https://img.shields.io/badge/MCP-Native%20Server-8A2BE2?style=for-the-badge&logo=anthropic" alt="MCP Server"></a>
|
|
15
16
|
<a href="https://github.com/muadzhdz/laporan-generator/actions/workflows/build.yml"><img src="https://github.com/muadzhdz/laporan-generator/actions/workflows/build.yml/badge.svg" alt="Status Build"></a>
|
|
16
17
|
<a href="#"><img src="https://img.shields.io/badge/Typst-0.13+-239DAD?style=for-the-badge&logo=typst&logoColor=white" alt="Typst"></a>
|
|
17
18
|
<a href="#"><img src="https://img.shields.io/badge/Pandoc-3.0+-blue?style=for-the-badge&logo=markdown" alt="Pandoc"></a>
|
|
@@ -110,6 +111,48 @@ npx laporan-generator sync-hosts
|
|
|
110
111
|
|
|
111
112
|
---
|
|
112
113
|
|
|
114
|
+
## Dukungan Model Context Protocol (MCP)
|
|
115
|
+
|
|
116
|
+
Laporan Generator v2.7.0 dilengkapi dengan **Native MCP Server** resmi yang berjalan melalui `stdio` berbasis protokol JSON-RPC 2.0 (spesifikasi 2024-11-05). Server ini memungkinkan AI Agent otonom (seperti Google Antigravity, Claude Desktop, Claude Code, dan OpenCode) untuk berinteraksi langsung dengan dokumen akademik melalui tools deterministik.
|
|
117
|
+
|
|
118
|
+
### Konfigurasi Otomatis (Direkomendasikan)
|
|
119
|
+
Cukup jalankan satu perintah berikut untuk mendaftarkan skill sekaligus konfigurasi MCP Server ke seluruh agent di mesin Anda:
|
|
120
|
+
```bash
|
|
121
|
+
npx laporan-generator sync-hosts
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Perintah di atas secara aman mendaftarkan server `laporan` tanpa merusak konfigurasi server lain yang sudah ada:
|
|
125
|
+
* **Antigravity CLI**: `~/.gemini/config/mcp_config.json`
|
|
126
|
+
* **Claude Desktop**: `~/.config/Claude/claude_desktop_config.json`
|
|
127
|
+
* **Claude Code**: `~/.claude.json`
|
|
128
|
+
* **OpenCode**: `~/.config/opencode/opencode.jsonc`
|
|
129
|
+
|
|
130
|
+
### Konfigurasi Manual
|
|
131
|
+
Jika ingin mendaftarkan server secara manual pada klien MCP apa pun:
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"mcpServers": {
|
|
135
|
+
"laporan": {
|
|
136
|
+
"command": "npx",
|
|
137
|
+
"args": ["-y", "laporan-generator", "mcp"]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 6 Native Tools MCP yang Disediakan
|
|
144
|
+
|
|
145
|
+
| Tool Name | Deskripsi Fungsi | Parameter Utama |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| `laporan_init` | Menginisialisasi dokumen baru (skripsi/makalah) secara terprogram | `target_dir`, `title`, `author`, `nim`, `institution`, `preset` |
|
|
148
|
+
| `laporan_doctor` | Mengaudit integritas dependensi, berkas wajib, sitasi, dan broken media | `target_dir`, `fix` (opsional) |
|
|
149
|
+
| `laporan_stats` | Menghitung statistik kata, bab, karakter, dan estimasi halaman | `target_dir` |
|
|
150
|
+
| `laporan_presets` | Memeriksa daftar dan detail konfigurasi preset kampus resmi | `action` (`list` / `get`), `preset_id` |
|
|
151
|
+
| `laporan_citations` | Memvalidasi integritas BibTeX dan pencarian sitasi langsung ke Crossref API | `action` (`validate` / `search`), `query`, `bib_path` |
|
|
152
|
+
| `laporan_build` | Mengompilasi dokumen ke PDF (Typst) dan/atau DOCX (Word) | `format` (`all` / `pdf` / `docx`), `preset` |
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
113
156
|
## Alur Penggunaan Cepat
|
|
114
157
|
|
|
115
158
|
### Opsi A: Alur Otonom AI Agent via NPX (Direkomendasikan)
|
|
@@ -192,6 +235,7 @@ Seluruh perintah didukung secara konsisten di NPX, Linux/macOS Bash (`./laporan`
|
|
|
192
235
|
|
|
193
236
|
| Perintah | NPX (`npx laporan-generator`) | Bash (`./laporan`) | PowerShell (`.\laporan.ps1`) | Keterangan Fungsi |
|
|
194
237
|
|---|---|---|---|---|
|
|
238
|
+
| `mcp` | Didukung | Didukung | Didukung | Menjalankan Model Context Protocol (MCP) Server untuk AI Agent |
|
|
195
239
|
| `init` | Didukung | Didukung | Didukung | Menyalin template berkas, metadata, dan draf bab awal |
|
|
196
240
|
| `sync-hosts` | Didukung | Didukung | Didukung | Memasang skill ke seluruh direktori agen AI yang terdeteksi |
|
|
197
241
|
| `setup` | Didukung | Didukung | Didukung | Memasang paket sistem Typst, Pandoc, ImageMagick, dan Python |
|
package/bin/laporan-generator.js
CHANGED
|
@@ -25,23 +25,29 @@ function showHelp() {
|
|
|
25
25
|
========================================================
|
|
26
26
|
LAPORAN GENERATOR CLI (Multi-Host AI Engine)
|
|
27
27
|
========================================================
|
|
28
|
-
Versi: 2.
|
|
28
|
+
Versi: 2.7.0
|
|
29
29
|
|
|
30
30
|
Penggunaan:
|
|
31
31
|
npx laporan-generator <perintah> [opsi]
|
|
32
32
|
|
|
33
33
|
Perintah Utama:
|
|
34
|
-
|
|
34
|
+
mcp Jalankan Model Context Protocol (MCP) Server untuk AI Agent
|
|
35
|
+
sync-hosts Pasang/sinkronkan AI agent skills & MCP ke Antigravity, Claude, OpenCode
|
|
35
36
|
uninstall Hapus/bersihkan skill dan perintah dari seluruh AI agent host
|
|
36
37
|
setup Jalankan installer dependensi otomatis multi-OS (Typst, Pandoc, ImageMagick)
|
|
37
38
|
init Inisialisasi template dokumen laporan/skripsi di direktori saat ini
|
|
38
39
|
doctor Audit kesehatan lingkungan dan dependensi dokumen
|
|
40
|
+
stats Hitung statistik komprehensif dokumen (kata, bab, estimasi halaman)
|
|
39
41
|
build Kompilasi dokumen ke PDF dan DOCX
|
|
40
42
|
help Tampilkan panduan ini
|
|
41
43
|
`);
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
switch (command) {
|
|
47
|
+
case 'mcp':
|
|
48
|
+
case 'server':
|
|
49
|
+
require('../lib/mcp').start();
|
|
50
|
+
break;
|
|
45
51
|
case 'uninstall':
|
|
46
52
|
case 'clean-skills':
|
|
47
53
|
require('../lib/uninstall').run(args.slice(1));
|
|
@@ -57,11 +63,17 @@ switch (command) {
|
|
|
57
63
|
case 'init':
|
|
58
64
|
require('../lib/init').run(args.slice(1));
|
|
59
65
|
break;
|
|
66
|
+
case 'stats':
|
|
67
|
+
const statsScript = path.join(__dirname, '../scripts/report-stats.py');
|
|
68
|
+
const pyStatsCmd = getPythonCommand();
|
|
69
|
+
const statsProc = spawnSync(pyStatsCmd, [statsScript, ...args.slice(1)], { stdio: 'inherit' });
|
|
70
|
+
process.exit(statsProc.status || 0);
|
|
71
|
+
break;
|
|
60
72
|
case 'doctor':
|
|
61
73
|
case 'check':
|
|
62
74
|
const doctorScript = path.join(__dirname, '../scripts/report-doctor.py');
|
|
63
75
|
const pyCmd = getPythonCommand();
|
|
64
|
-
const proc = spawnSync(pyCmd, [doctorScript], { stdio: 'inherit' });
|
|
76
|
+
const proc = spawnSync(pyCmd, [doctorScript, ...args.slice(1)], { stdio: 'inherit' });
|
|
65
77
|
process.exit(proc.status || 0);
|
|
66
78
|
break;
|
|
67
79
|
case 'build':
|
package/laporan
CHANGED
|
@@ -27,7 +27,7 @@ export PYTHONUTF8=1
|
|
|
27
27
|
show_banner() {
|
|
28
28
|
echo -e "${CYAN}${BOLD}"
|
|
29
29
|
echo " ========================================================"
|
|
30
|
-
echo " LAPORAN GENERATOR CLI v2.
|
|
30
|
+
echo " LAPORAN GENERATOR CLI v2.7.0 "
|
|
31
31
|
echo " Otomatisasi Dokumen Akademik (Typst + DOCX Engine) "
|
|
32
32
|
echo " ========================================================"
|
|
33
33
|
echo -e "${NC}"
|
|
@@ -38,6 +38,7 @@ show_help() {
|
|
|
38
38
|
echo -e "${BOLD}Penggunaan:${NC} ./laporan <perintah>"
|
|
39
39
|
echo ""
|
|
40
40
|
echo -e "${BOLD}Perintah yang Tersedia:${NC}"
|
|
41
|
+
echo -e " ${GREEN}mcp${NC} Jalankan Model Context Protocol (MCP) Server untuk AI Agent"
|
|
41
42
|
echo -e " ${GREEN}build${NC} Kompilasi PDF dan DOCX sekaligus"
|
|
42
43
|
echo -e " ${GREEN}pdf${NC} Kompilasi dokumen PDF (via Typst)"
|
|
43
44
|
echo -e " ${GREEN}docx${NC} Kompilasi dokumen Microsoft Word (.docx)"
|
|
@@ -47,7 +48,7 @@ show_help() {
|
|
|
47
48
|
echo -e " ${GREEN}doctor${NC} Audit kesehatan proyek (broken images, sitasi hilang, dll.)"
|
|
48
49
|
echo -e " ${GREEN}bundle${NC} Kemas seluruh laporan (PDF, DOCX, MD) menjadi arsip zip"
|
|
49
50
|
echo -e " ${GREEN}setup${NC} Pasang dependensi sistem otomatis (Linux, macOS, Nix, Docker)"
|
|
50
|
-
echo -e " ${GREEN}sync-skills${NC} Sinkronkan skill AI agent ke Antigravity, Claude,
|
|
51
|
+
echo -e " ${GREEN}sync-skills${NC} Sinkronkan skill AI agent & MCP ke Antigravity, Claude, OpenCode"
|
|
51
52
|
echo -e " ${GREEN}uninstall${NC} Hapus integrasi skill dan perintah AI agent dari seluruh sistem"
|
|
52
53
|
echo -e " ${GREEN}check${NC} Audit dependensi sistem dan struktur proyek"
|
|
53
54
|
echo -e " ${GREEN}test${NC} Jalankan suite pengujian otomatis (test.sh)"
|
|
@@ -424,16 +425,48 @@ case "${1:-help}" in
|
|
|
424
425
|
setup)
|
|
425
426
|
bash scripts/setup-deps.sh
|
|
426
427
|
;;
|
|
428
|
+
mcp|server)
|
|
429
|
+
shift
|
|
430
|
+
if command -v node >/dev/null 2>&1; then
|
|
431
|
+
if [ -f "bin/mcp-server.js" ]; then
|
|
432
|
+
node bin/mcp-server.js "$@"
|
|
433
|
+
elif command -v npx >/dev/null 2>&1; then
|
|
434
|
+
npx -y laporan-generator mcp "$@"
|
|
435
|
+
else
|
|
436
|
+
echo "Berkas bin/mcp-server.js tidak ditemukan dan npx tidak tersedia."
|
|
437
|
+
exit 1
|
|
438
|
+
fi
|
|
439
|
+
else
|
|
440
|
+
echo "Node.js diperlukan untuk menjalankan MCP Server."
|
|
441
|
+
exit 1
|
|
442
|
+
fi
|
|
443
|
+
;;
|
|
427
444
|
sync-skills|sync-hosts)
|
|
445
|
+
shift
|
|
428
446
|
if command -v node >/dev/null 2>&1; then
|
|
429
|
-
|
|
447
|
+
if [ -f "bin/laporan-generator.js" ]; then
|
|
448
|
+
node bin/laporan-generator.js sync-hosts "$@"
|
|
449
|
+
elif command -v npx >/dev/null 2>&1; then
|
|
450
|
+
npx -y laporan-generator sync-hosts "$@"
|
|
451
|
+
else
|
|
452
|
+
echo "Berkas bin/laporan-generator.js tidak ditemukan dan npx tidak tersedia."
|
|
453
|
+
exit 1
|
|
454
|
+
fi
|
|
430
455
|
else
|
|
431
456
|
echo "Node.js diperlukan untuk sinkronisasi multi-host AI agent."
|
|
432
457
|
fi
|
|
433
458
|
;;
|
|
434
459
|
uninstall|clean-skills)
|
|
460
|
+
shift
|
|
435
461
|
if command -v node >/dev/null 2>&1; then
|
|
436
|
-
|
|
462
|
+
if [ -f "bin/laporan-generator.js" ]; then
|
|
463
|
+
node bin/laporan-generator.js uninstall "$@"
|
|
464
|
+
elif command -v npx >/dev/null 2>&1; then
|
|
465
|
+
npx -y laporan-generator uninstall "$@"
|
|
466
|
+
else
|
|
467
|
+
echo "Berkas bin/laporan-generator.js tidak ditemukan dan npx tidak tersedia."
|
|
468
|
+
exit 1
|
|
469
|
+
fi
|
|
437
470
|
else
|
|
438
471
|
echo "Node.js diperlukan untuk membersihkan integrasi AI agent."
|
|
439
472
|
fi
|