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.
- package/.agents/skills/laporan-generator/SKILL.md +109 -109
- package/.agents/skills/laporan-generator/resources/cover.md +18 -14
- package/.agents/skills/laporan-generator/resources/docx.lua +65 -0
- 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/metadata.yml +13 -16
- 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 +160 -2
- package/.agents/skills/laporan-generator/resources/template.typ +3 -0
- 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 +185 -2
- package/README.md +49 -0
- package/bin/laporan-generator.js +15 -3
- package/bin/mcp-server.js +4 -0
- package/cover.md +18 -14
- package/docs/metadata-schema.md +2 -0
- package/docx.lua +65 -0
- package/laporan +37 -4
- package/laporan.ps1 +50 -11
- package/lib/init.js +139 -45
- package/lib/mcp.js +579 -0
- package/lib/sync-hosts.js +196 -68
- package/lib/uninstall.js +21 -0
- package/metadata.yml +13 -16
- 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 +147 -3
- 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
|
-
|
|
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,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"
|
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.8.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/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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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()
|
package/docs/metadata-schema.md
CHANGED
|
@@ -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.
|
|
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,
|
|
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
|