laporan-generator 2.6.2 → 2.8.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.
Files changed (36) hide show
  1. package/.agents/skills/laporan-generator/SKILL.md +109 -109
  2. package/.agents/skills/laporan-generator/resources/cover.md +18 -14
  3. package/.agents/skills/laporan-generator/resources/docx.lua +65 -0
  4. package/.agents/skills/laporan-generator/resources/laporan +37 -4
  5. package/.agents/skills/laporan-generator/resources/laporan.ps1 +74 -28
  6. package/.agents/skills/laporan-generator/resources/metadata.yml +13 -16
  7. package/.agents/skills/laporan-generator/resources/scripts/bundle.py +11 -2
  8. package/.agents/skills/laporan-generator/resources/scripts/report-doctor.py +121 -48
  9. package/.agents/skills/laporan-generator/resources/scripts/report-stats.py +53 -2
  10. package/.agents/skills/laporan-generator/resources/scripts/test_scripts.py +160 -2
  11. package/.agents/skills/laporan-generator/resources/template.typ +3 -0
  12. package/.agents/skills/laporan-generator/scripts/bundle.py +11 -2
  13. package/.agents/skills/laporan-generator/scripts/finalize-docx.py +140 -1
  14. package/.agents/skills/laporan-generator/scripts/make-reference-docx.py +42 -2
  15. package/.agents/skills/laporan-generator/scripts/report-doctor.py +121 -48
  16. package/.agents/skills/laporan-generator/scripts/report-stats.py +53 -2
  17. package/.agents/skills/laporan-generator/scripts/test_scripts.py +185 -2
  18. package/README.md +49 -0
  19. package/bin/laporan-generator.js +15 -3
  20. package/bin/mcp-server.js +4 -0
  21. package/cover.md +18 -14
  22. package/docs/metadata-schema.md +2 -0
  23. package/docx.lua +65 -0
  24. package/laporan +37 -4
  25. package/laporan.ps1 +50 -11
  26. package/lib/init.js +139 -45
  27. package/lib/mcp.js +579 -0
  28. package/lib/sync-hosts.js +196 -68
  29. package/lib/uninstall.js +21 -0
  30. package/metadata.yml +13 -16
  31. package/package.json +9 -3
  32. package/scripts/bundle.py +11 -2
  33. package/scripts/report-doctor.py +121 -48
  34. package/scripts/report-stats.py +46 -2
  35. package/scripts/test_scripts.py +147 -3
  36. package/template.typ +3 -0
@@ -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,164 @@ 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
+
186
+
187
+ class TestFrontMatterOptIn(unittest.TestCase):
188
+ def test_default_metadata_structure(self):
189
+ meta_path = os.path.join(get_project_root(), "metadata.yml")
190
+ with open(meta_path, "r", encoding="utf-8") as f:
191
+ content = f.read()
192
+ # Verify abstract is commented or empty by default
193
+ self.assertNotIn("\nabstract_id: |", content, "abstract_id should not be active by default in metadata.yml")
194
+ self.assertNotIn("\nabstract_en: |", content, "abstract_en should not be active by default in metadata.yml")
195
+ # Verify opt-in flags for daftar_gambar & daftar_tabel are explicitly false or commented
196
+ has_dg = "daftar_gambar: false" in content
197
+ has_dt = "daftar_tabel: false" in content
198
+ self.assertTrue(has_dg, "metadata.yml must explicitly specify daftar_gambar: false by default")
199
+ self.assertTrue(has_dt, "metadata.yml must explicitly specify daftar_tabel: false by default")
200
+
201
+ def test_cover_md_gated_outlines(self):
202
+ cover_path = os.path.join(get_project_root(), "cover.md")
203
+ with open(cover_path, "r", encoding="utf-8") as f:
204
+ content = f.read()
205
+ # Unconditional outline queries must not exist
206
+ self.assertNotIn("let imgs = query(figure.where(kind: image))\n if imgs.len() > 0", content,
207
+ "cover.md must not unconditionally emit DAFTAR GAMBAR outline")
208
+ self.assertNotIn("let tbls = query(figure.where(kind: table))\n if tbls.len() > 0", content,
209
+ "cover.md must not unconditionally emit DAFTAR TABEL outline")
210
+ # Must be gated by opt-in check
211
+ self.assertIn("opt-daftar-gambar", content, "cover.md must gate DAFTAR GAMBAR with opt-daftar-gambar")
212
+ self.assertIn("opt-daftar-tabel", content, "cover.md must gate DAFTAR TABEL with opt-daftar-tabel")
213
+
214
+ def test_lua_meta_bool_coercion(self):
215
+ """Verify docx.lua handles YAML boolean scalars and string equivalents safely."""
216
+ lua_code = """
217
+ local pandoc = {
218
+ utils = {
219
+ stringify = function(x)
220
+ if type(x) ~= 'string' and type(x) ~= 'table' then
221
+ error('pandoc.utils.stringify expects table or string, got ' .. type(x))
222
+ end
223
+ return tostring(x)
224
+ end
225
+ }
226
+ }
227
+ local function meta_str(meta, key)
228
+ local v = meta[key]
229
+ if v == nil then return "" end
230
+ if type(v) == "boolean" then return tostring(v) end
231
+ return pandoc.utils.stringify(v)
232
+ end
233
+ local function meta_bool(meta, key)
234
+ local v = meta[key]
235
+ if v == nil then return false end
236
+ if type(v) == "boolean" then return v end
237
+ local s = meta_str(meta, key):lower():gsub("%s+", "")
238
+ return s == "true" or s == "1" or s == "yes"
239
+ end
240
+
241
+ local meta = {
242
+ dg_bool_false = false,
243
+ dg_bool_true = true,
244
+ dg_str_false = "false",
245
+ dg_str_true = "true",
246
+ dg_nil = nil
247
+ }
248
+
249
+ assert(meta_str(meta, "dg_bool_false") == "false")
250
+ assert(meta_bool(meta, "dg_bool_false") == false)
251
+ assert(meta_str(meta, "dg_bool_true") == "true")
252
+ assert(meta_bool(meta, "dg_bool_true") == true)
253
+ assert(meta_bool(meta, "dg_str_false") == false)
254
+ assert(meta_bool(meta, "dg_str_true") == true)
255
+ assert(meta_bool(meta, "dg_nil") == false)
256
+ print("LUA_OK")
257
+ """
258
+ proc = subprocess.run(["lua", "-e", lua_code], capture_output=True, text=True)
259
+ self.assertEqual(proc.returncode, 0, f"Lua helper test failed: {proc.stderr}")
260
+ self.assertIn("LUA_OK", proc.stdout)
261
+
262
+ def test_init_project_boolean_coercion(self):
263
+ """Verify lib/init.js does not treat string 'false' as truthy."""
264
+ import tempfile
265
+ with tempfile.TemporaryDirectory() as tmpdir:
266
+ node_code = f"""
267
+ const path = require('path');
268
+ const {{ initProject }} = require('{os.path.join(ROOT_DIR, "lib/init.js")}');
269
+ initProject({{ targetDir: '{tmpdir}/test_f', daftar_gambar: 'false', daftar_tabel: false, silent: true }});
270
+ initProject({{ targetDir: '{tmpdir}/test_t', daftar_gambar: 'true', daftar_tabel: true, silent: true }});
271
+ """
272
+ proc = subprocess.run(["node", "-e", node_code], capture_output=True, text=True)
273
+ self.assertEqual(proc.returncode, 0, f"Node initProject failed: {proc.stderr}")
274
+ with open(os.path.join(tmpdir, "test_f", "metadata.yml"), "r", encoding="utf-8") as f:
275
+ meta_f = f.read()
276
+ with open(os.path.join(tmpdir, "test_t", "metadata.yml"), "r", encoding="utf-8") as f:
277
+ meta_t = f.read()
278
+ self.assertIn("daftar_gambar: false", meta_f)
279
+ self.assertIn("daftar_tabel: false", meta_f)
280
+ self.assertIn("daftar_gambar: true", meta_t)
281
+ self.assertIn("daftar_tabel: true", meta_t)
282
+
101
283
 
102
284
  if __name__ == "__main__":
103
285
  unittest.main(verbosity=2)
286
+
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.8.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 |
@@ -249,6 +293,11 @@ title: "Analisis dan Implementasi Sistem Deteksi Intrusi Jaringan Berbasis eBPF"
249
293
  document_type: "Skripsi"
250
294
  margin_preset: "itb-ta"
251
295
 
296
+ # Kontrol Struktur Halaman Depan (Opt-In)
297
+ # Default struktur bersih: Cover -> Kata Pengantar -> Daftar Isi -> Batang Tubuh -> Daftar Pustaka
298
+ daftar_gambar: false # Ubah true jika dokumen memuat gambar dan membutuhkan DAFTAR GAMBAR
299
+ daftar_tabel: false # Ubah true jika dokumen memuat tabel dan membutuhkan DAFTAR TABEL
300
+
252
301
  author:
253
302
  - name: "Mu'adz Hudzaifah"
254
303
  nim: "13522001"
@@ -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.8.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/cover.md CHANGED
@@ -22,21 +22,25 @@ Penulis menyadari bahwa laporan ini masih jauh dari sempurna. Oleh karena itu, k
22
22
  depth: 3,
23
23
  )
24
24
  #context {
25
- let imgs = query(figure.where(kind: image))
26
- if imgs.len() > 0 {
27
- pagebreak()
28
- outline(
29
- title: align(center)[#text(size: 14pt, weight: "bold")[DAFTAR GAMBAR]],
30
- target: figure.where(kind: image),
31
- )
25
+ if opt-daftar-gambar {
26
+ let imgs = query(figure.where(kind: image))
27
+ if imgs.len() > 0 {
28
+ pagebreak()
29
+ outline(
30
+ title: align(center)[#text(size: 14pt, weight: "bold")[DAFTAR GAMBAR]],
31
+ target: figure.where(kind: image),
32
+ )
33
+ }
32
34
  }
33
- let tbls = query(figure.where(kind: table))
34
- if tbls.len() > 0 {
35
- pagebreak()
36
- outline(
37
- title: align(center)[#text(size: 14pt, weight: "bold")[DAFTAR TABEL]],
38
- target: figure.where(kind: table),
39
- )
35
+ if opt-daftar-tabel {
36
+ let tbls = query(figure.where(kind: table))
37
+ if tbls.len() > 0 {
38
+ pagebreak()
39
+ outline(
40
+ title: align(center)[#text(size: 14pt, weight: "bold")[DAFTAR TABEL]],
41
+ target: figure.where(kind: table),
42
+ )
43
+ }
40
44
  }
41
45
  }
42
46
  #pagebreak()
@@ -49,6 +49,8 @@ abstract: |
49
49
  | `faculty` | String | **Ya** | Nama Fakultas, Departemen, atau Program Studi. |
50
50
  | `year` | String | **Ya** | Tahun Ajaran (contoh: `2025/2026`). |
51
51
  | `date` | String | Opsional | Bulan dan Tahun pembuatan (contoh: `Juli 2026`). |
52
+ | `daftar_gambar` | Boolean | Opsional | Aktifkan halaman DAFTAR GAMBAR (`true`/`false`, default: `false`). |
53
+ | `daftar_tabel` | Boolean | Opsional | Aktifkan halaman DAFTAR TABEL (`true`/`false`, default: `false`). |
52
54
  | `abstract` | Multiline String | Opsional | Ringkasan/Abstrak laporan dalam Bahasa Indonesia. |
53
55
 
54
56
  ---
package/docx.lua CHANGED
@@ -138,9 +138,24 @@ local function meta_str(meta, key)
138
138
  if v == nil then
139
139
  return ""
140
140
  end
141
+ if type(v) == "boolean" then
142
+ return tostring(v)
143
+ end
141
144
  return pandoc.utils.stringify(v)
142
145
  end
143
146
 
147
+ local function meta_bool(meta, key)
148
+ local v = meta[key]
149
+ if v == nil then
150
+ return false
151
+ end
152
+ if type(v) == "boolean" then
153
+ return v
154
+ end
155
+ local s = meta_str(meta, key):lower():gsub("%s+", "")
156
+ return s == "true" or s == "1" or s == "yes"
157
+ end
158
+
144
159
  local function format_chapter_number(n)
145
160
  if chapter_num_fmt == "arabic" then
146
161
  return tostring(n)
@@ -280,6 +295,44 @@ local function daftar_isi()
280
295
  return out
281
296
  end
282
297
 
298
+ local function daftar_gambar()
299
+ local out = pandoc.List{}
300
+ out[#out + 1] = pagebreak()
301
+ out[#out + 1] = pandoc.RawBlock(
302
+ "openxml",
303
+ '<w:p><w:pPr><w:pStyle w:val="TOCHeading"/><w:jc w:val="center"/><w:outlineLvl w:val="9"/></w:pPr>' ..
304
+ '<w:r><w:rPr><w:b/><w:sz w:val="28"/><w:szCs w:val="28"/></w:rPr><w:t>DAFTAR GAMBAR</w:t></w:r></w:p>'
305
+ )
306
+ out[#out + 1] = pandoc.RawBlock(
307
+ "openxml",
308
+ '<w:p><w:pPr><w:pStyle w:val="TOC1"/></w:pPr>' ..
309
+ '<w:r><w:fldChar w:fldCharType="begin" w:dirty="true"/></w:r>' ..
310
+ '<w:r><w:instrText xml:space="preserve"> TOC \\c "Gambar" \\h \\z </w:instrText></w:r>' ..
311
+ '<w:r><w:fldChar w:fldCharType="separate"/></w:r>' ..
312
+ '<w:r><w:fldChar w:fldCharType="end"/></w:r></w:p>'
313
+ )
314
+ return out
315
+ end
316
+
317
+ local function daftar_tabel()
318
+ local out = pandoc.List{}
319
+ out[#out + 1] = pagebreak()
320
+ out[#out + 1] = pandoc.RawBlock(
321
+ "openxml",
322
+ '<w:p><w:pPr><w:pStyle w:val="TOCHeading"/><w:jc w:val="center"/><w:outlineLvl w:val="9"/></w:pPr>' ..
323
+ '<w:r><w:rPr><w:b/><w:sz w:val="28"/><w:szCs w:val="28"/></w:rPr><w:t>DAFTAR TABEL</w:t></w:r></w:p>'
324
+ )
325
+ out[#out + 1] = pandoc.RawBlock(
326
+ "openxml",
327
+ '<w:p><w:pPr><w:pStyle w:val="TOC1"/></w:pPr>' ..
328
+ '<w:r><w:fldChar w:fldCharType="begin" w:dirty="true"/></w:r>' ..
329
+ '<w:r><w:instrText xml:space="preserve"> TOC \\c "Tabel" \\h \\z </w:instrText></w:r>' ..
330
+ '<w:r><w:fldChar w:fldCharType="separate"/></w:r>' ..
331
+ '<w:r><w:fldChar w:fldCharType="end"/></w:r></w:p>'
332
+ )
333
+ return out
334
+ end
335
+
283
336
  function Pandoc(doc)
284
337
  if FORMAT ~= "docx" then
285
338
  return doc
@@ -292,6 +345,8 @@ function Pandoc(doc)
292
345
  local faculty = meta_str(meta, "faculty")
293
346
  local institution = meta_str(meta, "institution")
294
347
  local year = meta_str(meta, "year")
348
+ local show_dg = (meta_bool(meta, "daftar_gambar") or meta_bool(meta, "list_of_figures"))
349
+ local show_dt = (meta_bool(meta, "daftar_tabel") or meta_bool(meta, "list_of_tables"))
295
350
 
296
351
  local blocks = pandoc.List{}
297
352
 
@@ -401,6 +456,16 @@ function Pandoc(doc)
401
456
  for _, b in ipairs(daftar_isi()) do
402
457
  body[#body + 1] = b
403
458
  end
459
+ if show_dg then
460
+ for _, b in ipairs(daftar_gambar()) do
461
+ body[#body + 1] = b
462
+ end
463
+ end
464
+ if show_dt then
465
+ for _, b in ipairs(daftar_tabel()) do
466
+ body[#body + 1] = b
467
+ end
468
+ end
404
469
  inserted = true
405
470
  end
406
471
  body[#body + 1] = blk
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.8.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