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.
@@ -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
- root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
102
- os.chdir(root)
103
-
104
- print(f"{CYAN}{BOLD}")
105
- print(" ========================================================")
106
- print(" LAPORAN GENERATOR - DOCTOR & AUDIT SUITE ")
107
- print(" ========================================================")
108
- print(f"{NC}")
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
- print(f" {BLUE}{BOLD}[1/4] Audit Toolchain & Dependensi Sistem{NC}")
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
- if found:
120
- print(f" {GREEN}[OK]{NC} {name:<26}: Terpasang ({cmd})")
121
- else:
122
- if req:
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
- print(f" {YELLOW}[INFO]{NC} {name:<24}: Tidak ditemukan (Opsional)")
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
- if has_im:
129
- print(f" {GREEN}[OK]{NC} {'ImageMagick':<26}: Terpasang")
130
- else:
131
- print(f" {YELLOW}[INFO]{NC} {'ImageMagick':<24}: Tidak ditemukan (Opsional untuk kompresi gambar)")
132
- print("")
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
- print(f" {BLUE}{BOLD}[2/4] Audit Struktur Direktori & Berkas Wajib{NC}")
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
- if os.path.isfile(rf):
142
- print(f" {GREEN}[OK]{NC} Berkas {rf}")
143
- else:
144
- print(f" {RED}[FAIL]{NC} Berkas {rf} TIDAK DITEMUKAN")
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 chapter_files:
149
- print(f" {GREEN}[OK]{NC} Direktori chapters/ ({len(chapter_files)} bab ditemukan)")
150
- else:
151
- print(f" {RED}[FAIL]{NC} Tidak ada berkas markdown di chapters/")
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
- print("")
187
+ if not json_mode:
188
+ print("")
154
189
 
155
190
  # 3. Sitasi & Referensi
156
- print(f" {BLUE}{BOLD}[3/4] Audit Sitasi & Integritas Bibliografi{NC}")
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
- print(f" * Total entri .bib : {len(bib_keys)}")
159
- print(f" * Total sitasi di teks : {len(cited_keys)}")
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
- print(f" {RED}[ERROR]{NC} Sitasi '@{k}' TIDAK DITEMUKAN di references.bib ({loc_str})")
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
- else:
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
- print(f" {YELLOW}[WARN]{NC} {len(orphan_keys)} entri di references.bib belum disitir dalam teks:")
171
- for ok in list(orphan_keys)[:5]:
172
- print(f" - @{ok}")
173
- if len(orphan_keys) > 5:
174
- print(f" - ... dan {len(orphan_keys) - 5} lainnya")
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
- print("")
215
+ if not json_mode:
216
+ print("")
177
217
 
178
218
  # 4. Gambar & Format Teks
179
- print(f" {BLUE}{BOLD}[4/4] Audit Media Citra & Anomali Tipografi{NC}")
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
- print(f" {RED}[ERROR]{NC} Berkas gambar tidak ditemukan: '{p}' (di {f}:{l})")
224
+ if not json_mode:
225
+ print(f" {RED}[ERROR]{NC} Berkas gambar tidak ditemukan: '{p}' (di {f}:{l})")
184
226
  errors += 1
185
- else:
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
- print(f" {YELLOW}[WARN]{NC} {f}:{l} - {msg}")
233
+ if not json_mode:
234
+ print(f" {YELLOW}[WARN]{NC} {f}:{l} - {msg}")
192
235
  warnings += 1
193
- else:
236
+ elif not json_mode:
194
237
  print(f" {GREEN}[OK]{NC} Bebas dari manual heading numbering dan box-drawing characters")
195
- print("")
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
- root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
52
- os.chdir(root)
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
- test_yaml = os.path.join(ROOT_DIR, "presets", "standard.yml")
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 = os.path.join(ROOT_DIR, "presets")
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 |
@@ -25,23 +25,29 @@ function showHelp() {
25
25
  ========================================================
26
26
  LAPORAN GENERATOR CLI (Multi-Host AI Engine)
27
27
  ========================================================
28
- Versi: 2.6.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
- sync-hosts Pasang/sinkronkan AI agent skills ke Antigravity, Claude, Grok
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':
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ require("../lib/mcp").start();
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.6.2 "
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, Grok"
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
- node bin/laporan-generator.js sync-hosts
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
- node bin/laporan-generator.js uninstall
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