hanoman 0.1.16 → 0.1.18

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/dist/server.js CHANGED
@@ -5732,6 +5732,10 @@ var init_api = __esm({
5732
5732
  // SPEC-471 · ADR-0095 · tarik & triase issue GitHub (di belakang gate cookie, capability `support`).
5733
5733
  // hanoman TIDAK PERNAH menulis ke GitHub — tak ada path komentar/close di sini, dan itu disengaja.
5734
5734
  githubPull: (id) => `${API}/projects/${encodeURIComponent(id)}/github/pull`,
5735
+ // SPEC-516 · ADR-0105 · changelog per project
5736
+ changelog: (id) => `${API}/projects/${encodeURIComponent(id)}/changelog`,
5737
+ changelogSources: (id) => `${API}/projects/${encodeURIComponent(id)}/changelog/sources`,
5738
+ changelogItem: (id, cid) => `${API}/projects/${encodeURIComponent(id)}/changelog/${encodeURIComponent(cid)}`,
5735
5739
  githubIssues: (id) => `${API}/projects/${encodeURIComponent(id)}/github/issues`,
5736
5740
  githubIssuesAccept: `${API}/github-issues/accept`,
5737
5741
  githubIssueAccept: (id) => `${API}/github-issues/${encodeURIComponent(id)}/accept`,
@@ -7118,6 +7122,51 @@ var init_mcp = __esm({
7118
7122
  }
7119
7123
  });
7120
7124
 
7125
+ // ../shared/src/changelog.ts
7126
+ function dayString(d) {
7127
+ const p = (n2) => String(n2).padStart(2, "0");
7128
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
7129
+ }
7130
+ function defaultRange(today) {
7131
+ const from = new Date(today.getFullYear(), today.getMonth(), today.getDate() - (DEFAULT_RANGE_DAYS - 1));
7132
+ return { from: dayString(from), to: dayString(today) };
7133
+ }
7134
+ var CHANGELOG_MODES, zChangelogMode, zDay, zChangelogRequest, zChangelogView, DEFAULT_RANGE_DAYS;
7135
+ var init_changelog = __esm({
7136
+ "../shared/src/changelog.ts"() {
7137
+ "use strict";
7138
+ init_zod();
7139
+ CHANGELOG_MODES = ["backlog", "commit", "version"];
7140
+ zChangelogMode = external_exports.enum(CHANGELOG_MODES);
7141
+ zDay = external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/, "format tanggal harus YYYY-MM-DD");
7142
+ zChangelogRequest = external_exports.discriminatedUnion("mode", [
7143
+ external_exports.object({ mode: external_exports.literal("backlog"), from: zDay.optional(), to: zDay.optional() }),
7144
+ external_exports.object({ mode: external_exports.literal("commit"), fromSha: external_exports.string().trim().min(4), toSha: external_exports.string().trim().min(4) }),
7145
+ external_exports.object({ mode: external_exports.literal("version"), fromTag: external_exports.string().trim().min(1).optional(), toTag: external_exports.string().trim().min(1) })
7146
+ ]).superRefine((v, ctx) => {
7147
+ if (v.mode === "backlog" && v.from && v.to && v.from > v.to)
7148
+ ctx.addIssue({
7149
+ code: external_exports.ZodIssueCode.custom,
7150
+ path: ["from"],
7151
+ message: "rentang tanggal terbalik \u2014 `from` harus lebih awal atau sama dengan `to`"
7152
+ });
7153
+ });
7154
+ zChangelogView = external_exports.object({
7155
+ id: external_exports.string(),
7156
+ projectId: external_exports.string(),
7157
+ mode: zChangelogMode,
7158
+ title: external_exports.string(),
7159
+ params: external_exports.unknown(),
7160
+ body: external_exports.string(),
7161
+ generator: external_exports.enum(["agent", "fallback"]),
7162
+ warning: external_exports.string().nullable(),
7163
+ itemCount: external_exports.number().int(),
7164
+ createdAt: external_exports.string()
7165
+ });
7166
+ DEFAULT_RANGE_DAYS = 30;
7167
+ }
7168
+ });
7169
+
7121
7170
  // ../shared/src/index.ts
7122
7171
  var init_src = __esm({
7123
7172
  "../shared/src/index.ts"() {
@@ -7142,6 +7191,7 @@ var init_src = __esm({
7142
7191
  init_webhook();
7143
7192
  init_mcp();
7144
7193
  init_auto_merge();
7194
+ init_changelog();
7145
7195
  }
7146
7196
  });
7147
7197
 
@@ -9584,7 +9634,10 @@ var init_sync = __esm({
9584
9634
  // ini spec asal-hub mendapat createdAt lokal palsu di tiap client (kolom NOT NULL ber-default).
9585
9635
  // SPEC-447 · ADR-0093 · dependsOn ikut juga: tanpa itu client tak tahu urutannya dan akan
9586
9636
  // meluncurkan pekerjaan yang di hub terblokir. Bukan DATE_FIELDS — nilainya array string.
9587
- spec: ["projectId", "title", "source", "stage", "priority", "author", "objective", "payload", "branchFrom", "baseSha", "headSha", "dependsOn", "createdAt", "startedAt", "updatedAt"],
9637
+ // SPEC-516 · ADR-0105 · doneAt ikut menyeberang — cermin createdAt/startedAt. Tanpa ini spec
9638
+ // asal-hub mendarat di tiap client dengan doneAt null tanpa satu pun error, dan changelog
9639
+ // mode backlog di client itu selamanya kosong.
9640
+ spec: ["projectId", "title", "source", "stage", "priority", "author", "objective", "payload", "branchFrom", "baseSha", "headSha", "dependsOn", "createdAt", "startedAt", "doneAt", "updatedAt"],
9588
9641
  vps: ["name", "host", "port", "user", "health", "audit", "hardened", "lastSeenAt", "lastAuditAt", "updatedAt"],
9589
9642
  sessionResult: ["projectId", "specId", "oldStage", "newStage", "commitSha", "branch", "prUrl", "status", "deviceId", "author", "createdAt", "updatedAt"],
9590
9643
  // SPEC-268 · ADR-0066 · metadata tiket (lampiran biner tak disync). accessKeyHash wajib
@@ -9624,7 +9677,7 @@ var init_sync = __esm({
9624
9677
  };
9625
9678
  DATE_FIELDS = {
9626
9679
  project: ["updatedAt"],
9627
- spec: ["createdAt", "startedAt", "updatedAt"],
9680
+ spec: ["createdAt", "startedAt", "doneAt", "updatedAt"],
9628
9681
  vps: ["lastSeenAt", "lastAuditAt", "updatedAt"],
9629
9682
  sessionResult: ["createdAt", "updatedAt"],
9630
9683
  ticket: ["createdAt", "updatedAt"],
@@ -9940,8 +9993,8 @@ async function telegramEngineContext() {
9940
9993
  };
9941
9994
  }
9942
9995
  function telegramReloadNeeded(before, after) {
9943
- const strip = ({ engine: _engine, ...rest }) => rest;
9944
- return JSON.stringify(strip(before)) !== JSON.stringify(strip(after));
9996
+ const strip2 = ({ engine: _engine, ...rest }) => rest;
9997
+ return JSON.stringify(strip2(before)) !== JSON.stringify(strip2(after));
9945
9998
  }
9946
9999
  var init_config3 = __esm({
9947
10000
  "src/services/telegram/config.ts"() {
@@ -11311,7 +11364,7 @@ var require_permessage_deflate = __commonJS({
11311
11364
  var Limiter = require_limiter();
11312
11365
  var { kStatusCode } = require_constants();
11313
11366
  var FastBuffer = Buffer[Symbol.species];
11314
- var TRAILER = Buffer.from([0, 0, 255, 255]);
11367
+ var TRAILER2 = Buffer.from([0, 0, 255, 255]);
11315
11368
  var kPerMessageDeflate = Symbol("permessage-deflate");
11316
11369
  var kTotalLength = Symbol("total-length");
11317
11370
  var kCallback = Symbol("callback");
@@ -11586,7 +11639,7 @@ var require_permessage_deflate = __commonJS({
11586
11639
  }
11587
11640
  this._inflate[kCallback] = callback;
11588
11641
  this._inflate.write(data);
11589
- if (fin) this._inflate.write(TRAILER);
11642
+ if (fin) this._inflate.write(TRAILER2);
11590
11643
  this._inflate.flush(() => {
11591
11644
  const err = this._inflate[kError];
11592
11645
  if (err) {
@@ -16697,7 +16750,7 @@ var require_Decoder = __commonJS({
16697
16750
  "../node_modules/.pnpm/@fastify+busboy@3.2.0/node_modules/@fastify/busboy/lib/utils/Decoder.js"(exports, module) {
16698
16751
  "use strict";
16699
16752
  var RE_PLUS = /\+/g;
16700
- var HEX = [
16753
+ var HEX2 = [
16701
16754
  0,
16702
16755
  0,
16703
16756
  0,
@@ -16838,7 +16891,7 @@ var require_Decoder = __commonJS({
16838
16891
  const len = str3.length;
16839
16892
  for (; i < len; ++i) {
16840
16893
  if (this.buffer !== void 0) {
16841
- if (!HEX[str3.charCodeAt(i)]) {
16894
+ if (!HEX2[str3.charCodeAt(i)]) {
16842
16895
  res += "%" + this.buffer;
16843
16896
  this.buffer = void 0;
16844
16897
  --i;
@@ -17260,7 +17313,7 @@ var require_plugin = __commonJS({
17260
17313
  var require_generateId = __commonJS({
17261
17314
  "../node_modules/.pnpm/@fastify+multipart@8.3.1/node_modules/@fastify/multipart/lib/generateId.js"(exports, module) {
17262
17315
  "use strict";
17263
- var HEX = [
17316
+ var HEX2 = [
17264
17317
  "00",
17265
17318
  "01",
17266
17319
  "02",
@@ -17520,13 +17573,13 @@ var require_generateId = __commonJS({
17520
17573
  ];
17521
17574
  var random = Math.random;
17522
17575
  function seed() {
17523
- return HEX[255 * random() | 0] + HEX[255 * random() | 0] + HEX[255 * random() | 0] + HEX[255 * random() | 0] + HEX[255 * random() | 0] + HEX[255 * random() | 0] + HEX[255 * random() | 0];
17576
+ return HEX2[255 * random() | 0] + HEX2[255 * random() | 0] + HEX2[255 * random() | 0] + HEX2[255 * random() | 0] + HEX2[255 * random() | 0] + HEX2[255 * random() | 0] + HEX2[255 * random() | 0];
17524
17577
  }
17525
17578
  module.exports.generateId = function generateIdFn() {
17526
17579
  let num = 0;
17527
17580
  let str3 = seed();
17528
17581
  return function generateId() {
17529
- return num === 255 ? (str3 = seed()) + HEX[num = 0] : str3 + HEX[++num];
17582
+ return num === 255 ? (str3 = seed()) + HEX2[num = 0] : str3 + HEX2[++num];
17530
17583
  };
17531
17584
  }();
17532
17585
  }
@@ -18359,7 +18412,7 @@ import websocket from "@fastify/websocket";
18359
18412
  import cookie from "@fastify/cookie";
18360
18413
  import { fileURLToPath as fileURLToPath4 } from "node:url";
18361
18414
  import { dirname as dirname13 } from "node:path";
18362
- import { existsSync as existsSync12 } from "node:fs";
18415
+ import { existsSync as existsSync13 } from "node:fs";
18363
18416
 
18364
18417
  // src/web-dir.ts
18365
18418
  import { resolve } from "node:path";
@@ -21817,6 +21870,8 @@ async function recordDrift(vpsId, vpsName, drift, snapshotId) {
21817
21870
  });
21818
21871
  }
21819
21872
  async function recordCompletion(specId, title, projectId) {
21873
+ await prisma.spec.updateMany({ where: { id: specId, doneAt: null }, data: { doneAt: /* @__PURE__ */ new Date() } }).catch(() => {
21874
+ });
21820
21875
  const sessionId2 = specId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
21821
21876
  await prisma.notification.create({
21822
21877
  data: { type: "done", key: `done:${specId}`, specId, sessionId: sessionId2, title, projectId }
@@ -27763,6 +27818,323 @@ async function lead_default(app2) {
27763
27818
  });
27764
27819
  }
27765
27820
 
27821
+ // src/routes/changelog.ts
27822
+ init_src();
27823
+ init_db();
27824
+
27825
+ // src/services/changelog/collect.ts
27826
+ init_db();
27827
+ import { execFile as execFile10 } from "node:child_process";
27828
+ import { promisify as promisify9 } from "node:util";
27829
+ import { existsSync as existsSync12 } from "node:fs";
27830
+
27831
+ // src/services/changelog/scrub.ts
27832
+ var MERGE = /^merge\s+(branch|pull request|remote-tracking branch|tag)\b/i;
27833
+ var CONVENTIONAL = /^(feat|fix|chore|docs|refactor|test|perf|build|ci|style|revert)(\([^)]*\))?!?:\s*/i;
27834
+ var PATH_LIKE = /(?:[\w.@-]+\/)+[\w.-]+\.\w{1,6}/g;
27835
+ var HEX = /\b(?=[0-9a-f]{7,40}\b)(?=[0-9a-f]*\d)(?=[0-9a-f]*[a-f])[0-9a-f]{7,40}\b/gi;
27836
+ var INTERNAL_REF = /\b(?:SPEC|ADR|PR|ISSUE)[-\s]?\d+\b/gi;
27837
+ var CAMEL = /\b[a-z]{2,}(?:[A-Z][a-z]{2,})+\b/g;
27838
+ var SNAKE = /\b[a-z][a-z0-9]*(?:_[a-z0-9]+)+\b/gi;
27839
+ var CALL = /\b[A-Za-z_][\w.]*\(\s*\)/g;
27840
+ var INLINE_CODE = /`[^`\n]*`/g;
27841
+ var FENCE = /^```[\s\S]*?^```$/gm;
27842
+ var TRAILER = /^[A-Za-z-]+:\s.*$/gm;
27843
+ var tidy = (s2) => s2.replace(/\(\s*\)/g, " ").replace(/[ \t]{2,}/g, " ").replace(/\s+([,.;:!?])/g, "$1").replace(/^[\s:;,·—–-]+/, "").replace(/[\s:;,·—–-]+$/, "").trim();
27844
+ function strip(s2) {
27845
+ return s2.replace(INLINE_CODE, " ").replace(PATH_LIKE, " ").replace(CALL, " ").replace(HEX, " ").replace(INTERNAL_REF, " ").replace(CAMEL, " ").replace(SNAKE, " ");
27846
+ }
27847
+ function scrubSubject(s2) {
27848
+ const one = (s2 ?? "").replace(/\s+/g, " ").trim();
27849
+ if (!one || MERGE.test(one)) return "";
27850
+ return tidy(strip(one.replace(CONVENTIONAL, "")));
27851
+ }
27852
+ function scrubBody(s2) {
27853
+ const first = (s2 ?? "").replace(TRAILER, "").split(/\n\s*\n/)[0] ?? "";
27854
+ return tidy(strip(first.replace(/\s+/g, " ").trim()));
27855
+ }
27856
+ function scrubOutput(md) {
27857
+ const noFence = (md ?? "").replace(FENCE, "");
27858
+ const lines2 = noFence.split("\n").map((line) => {
27859
+ const m = /^(\s*(?:[-*+]|\d+\.)\s+|\s*#{1,6}\s+|\s*>\s*)?(.*)$/.exec(line);
27860
+ const lead = m?.[1] ?? "";
27861
+ const rest = m?.[2] ?? line;
27862
+ const cleaned = tidy(strip(rest));
27863
+ return cleaned ? `${lead}${cleaned}` : "";
27864
+ });
27865
+ return lines2.join("\n").replace(/\n{3,}/g, "\n\n").trim() + "\n";
27866
+ }
27867
+
27868
+ // src/services/changelog/collect.ts
27869
+ var toItem = (label, detail) => {
27870
+ const l = scrubSubject(label);
27871
+ return l ? { label: l, detail: scrubBody(detail) } : null;
27872
+ };
27873
+ async function collectBacklog(projectId, from, to) {
27874
+ const f = dayStart(from), t = dayEnd(to);
27875
+ const rows = await prisma.spec.findMany({
27876
+ where: { projectId, stage: "done" },
27877
+ select: { title: true, objective: true, doneAt: true },
27878
+ orderBy: [{ doneAt: "asc" }, { id: "asc" }]
27879
+ });
27880
+ const stampless = rows.filter((r) => r.doneAt === null).length;
27881
+ const hit = rows.filter((r) => inDayRange(r.doneAt, f, t));
27882
+ const items = hit.map((r) => toItem(r.title, r.objective ?? "")).filter((x) => x !== null);
27883
+ if (items.length === 0)
27884
+ return { ok: false, reason: `tak ada backlog yang selesai antara ${from} dan ${to}` };
27885
+ const notes = [];
27886
+ if (stampless > 0)
27887
+ notes.push(`${stampless} item selesai tanpa stempel waktu (selesai sebelum stempel ini ada) dan tak ikut dihitung.`);
27888
+ return { ok: true, input: { mode: "backlog", title: `${from} \u2013 ${to}`, items, notes } };
27889
+ }
27890
+ var exec8 = promisify9(execFile10);
27891
+ var GIT7 = { maxBuffer: 1 << 24, encoding: "utf8" };
27892
+ var US2 = "";
27893
+ var RS = "";
27894
+ var NO_REPO = "project ini belum ditautkan ke repo di mesin ini";
27895
+ var NO_TAG = "repo project ini belum punya tag rilis";
27896
+ var usable = (repoDir) => !!repoDir && existsSync12(repoDir);
27897
+ async function listTags(repoDir) {
27898
+ if (!usable(repoDir)) return { tags: [], head: null, reason: NO_REPO };
27899
+ try {
27900
+ const { stdout } = await exec8("git", ["tag", "--list", "--sort=-creatordate"], { cwd: repoDir, ...GIT7 });
27901
+ const tags = stdout.split("\n").map((s2) => s2.trim()).filter(Boolean);
27902
+ const head = await exec8("git", ["rev-parse", "--short", "HEAD"], { cwd: repoDir, ...GIT7 }).then((r) => r.stdout.trim()).catch(() => null);
27903
+ return { tags, head, reason: tags.length ? null : NO_TAG };
27904
+ } catch {
27905
+ return { tags: [], head: null, reason: NO_TAG };
27906
+ }
27907
+ }
27908
+ async function previousTag(repoDir, tag2) {
27909
+ return exec8("git", ["describe", "--tags", "--abbrev=0", "--end-of-options", `${tag2}^`], { cwd: repoDir, ...GIT7 }).then((r) => r.stdout.trim() || null).catch(() => null);
27910
+ }
27911
+ async function revExists(repoDir, rev) {
27912
+ return exec8("git", ["rev-parse", "--verify", "--quiet", "--end-of-options", `${rev}^{commit}`], { cwd: repoDir, ...GIT7 }).then(() => true).catch(() => false);
27913
+ }
27914
+ async function logRange(repoDir, range) {
27915
+ const { stdout } = await exec8(
27916
+ "git",
27917
+ ["log", "--no-merges", `--pretty=format:%s${US2}%b${RS}`, ...range],
27918
+ { cwd: repoDir, ...GIT7 }
27919
+ );
27920
+ return stdout.split(RS).map((rec) => rec.replace(/^\n+/, "")).filter((rec) => rec.trim()).map((rec) => {
27921
+ const [subject, body] = rec.split(US2);
27922
+ return toItem(subject ?? "", body ?? "");
27923
+ }).filter((x) => x !== null);
27924
+ }
27925
+ async function collectCommits(repoDir, fromSha, toSha) {
27926
+ if (!usable(repoDir)) return { ok: false, reason: NO_REPO };
27927
+ for (const rev of [fromSha, toSha])
27928
+ if (!await revExists(repoDir, rev)) return { ok: false, reason: `revisi "${rev}" tak dikenal di repo project` };
27929
+ try {
27930
+ const items = await logRange(repoDir, ["--end-of-options", `${fromSha}..${toSha}`]);
27931
+ if (!items.length) return { ok: false, reason: `tak ada perubahan antara "${fromSha}" dan "${toSha}"` };
27932
+ const short = (s2) => s2.slice(0, 7);
27933
+ return { ok: true, input: { mode: "commit", title: `${short(fromSha)} \u2192 ${short(toSha)}`, items, notes: [] } };
27934
+ } catch (e) {
27935
+ return { ok: false, reason: `git menolak rentang itu: ${e.message.split("\n")[0]}` };
27936
+ }
27937
+ }
27938
+ async function collectVersions(repoDir, fromTag, toTag) {
27939
+ const { tags, reason } = await listTags(repoDir);
27940
+ if (reason) return { ok: false, reason };
27941
+ if (!usable(repoDir)) return { ok: false, reason: NO_REPO };
27942
+ if (!tags.includes(toTag)) return { ok: false, reason: `tag "${toTag}" tak ada di repo project` };
27943
+ if (fromTag && !tags.includes(fromTag)) return { ok: false, reason: `tag "${fromTag}" tak ada di repo project` };
27944
+ const prev = fromTag ?? await previousTag(repoDir, toTag);
27945
+ try {
27946
+ const items = await logRange(repoDir, ["--end-of-options", prev ? `${prev}..${toTag}` : toTag]);
27947
+ if (!items.length) return { ok: false, reason: `tak ada perubahan yang masuk ke "${toTag}"` };
27948
+ const title = fromTag ? `${fromTag} \u2192 ${toTag}` : toTag;
27949
+ return { ok: true, input: { mode: "version", title, items, notes: [] } };
27950
+ } catch (e) {
27951
+ return { ok: false, reason: `git menolak rentang itu: ${e.message.split("\n")[0]}` };
27952
+ }
27953
+ }
27954
+
27955
+ // src/services/changelog/generate.ts
27956
+ init_src();
27957
+ init_db();
27958
+ init_settings2();
27959
+
27960
+ // src/services/changelog/render.ts
27961
+ var MODE_LABEL = {
27962
+ backlog: "backlog yang selesai dalam rentang tanggal",
27963
+ commit: "riwayat perubahan repo dalam rentang yang dipilih",
27964
+ version: "perubahan yang masuk ke versi/rilis itu"
27965
+ };
27966
+ var DETAIL_MAX = 240;
27967
+ var clip2 = (s2) => s2.length > DETAIL_MAX ? `${s2.slice(0, DETAIL_MAX - 1).trimEnd()}\u2026` : s2;
27968
+ function fallbackMarkdown(input) {
27969
+ const lines2 = [`# Changelog \u2014 ${input.title}`, ""];
27970
+ if (input.items.length === 0) {
27971
+ lines2.push("_Tak ada perubahan yang bisa diringkas untuk rentang ini._", "");
27972
+ } else {
27973
+ lines2.push(`Ringkasan ${input.items.length} perubahan untuk pemakai.`, "");
27974
+ for (const it of input.items) {
27975
+ const d = clip2(it.detail.trim());
27976
+ lines2.push(d ? `- **${it.label}** \u2014 ${d}` : `- **${it.label}**`);
27977
+ }
27978
+ lines2.push("");
27979
+ }
27980
+ if (input.notes.length) {
27981
+ lines2.push("---", "");
27982
+ for (const n2 of input.notes) lines2.push(`> ${n2}`);
27983
+ lines2.push("");
27984
+ }
27985
+ return lines2.join("\n");
27986
+ }
27987
+ function changelogPrompt(input, budgetMs) {
27988
+ const budget = Math.max(1, Math.round(budgetMs / 1e3));
27989
+ const items = input.items.map((it, i) => `${i + 1}. ${it.label}${it.detail ? ` \u2014 ${it.detail}` : ""}`).join("\n");
27990
+ return [
27991
+ "Kamu menulis changelog untuk PEMAKAI sebuah produk, bukan untuk developer.",
27992
+ "",
27993
+ `Anggaran waktumu ${budget} detik. Jawab langsung \u2014 jangan membaca berkas, jangan memakai tool,`,
27994
+ "jangan menyelidiki apa pun. Seluruh bahan sudah ada di bawah.",
27995
+ "",
27996
+ `Judul rentang: ${input.title}`,
27997
+ `Bahan berasal dari ${MODE_LABEL[input.mode]}.`,
27998
+ "",
27999
+ "Bahan:",
28000
+ items || "(tidak ada)",
28001
+ "",
28002
+ "Aturan:",
28003
+ "- Bahasa Indonesia, gaya editorial: tenang, ringkas, kalimat penuh.",
28004
+ "- Tulis apa yang berubah BAGI PEMAKAI, bukan apa yang disentuh di dalam kode.",
28005
+ "- DILARANG menyebut nama berkas, nama fungsi, nama variabel, hash commit, nomor SPEC/ADR,",
28006
+ " nama branch, atau istilah internal apa pun.",
28007
+ "- Gabungkan bahan yang bicara hal yang sama jadi satu butir. 3\u201310 butir; kurangi bila memang sedikit.",
28008
+ "- Jangan mengarang perubahan yang tak ada di bahan.",
28009
+ "",
28010
+ "Bentuk keluaran \u2014 HANYA markdown ini, tanpa kalimat pembuka atau penutup di luarnya,",
28011
+ "tanpa blok kode:",
28012
+ "",
28013
+ `# Changelog \u2014 ${input.title}`,
28014
+ "",
28015
+ "<satu paragraf pembuka, maksimal dua kalimat>",
28016
+ "",
28017
+ "- **<judul singkat perubahan>** \u2014 <satu kalimat manfaatnya bagi pemakai>"
28018
+ ].join("\n");
28019
+ }
28020
+
28021
+ // src/services/changelog/generate.ts
28022
+ var CHANGELOG_TIMEOUT_MS = 18e4;
28023
+ async function collect(projectId, req) {
28024
+ if (req.mode === "backlog") {
28025
+ const d = defaultRange(/* @__PURE__ */ new Date());
28026
+ return collectBacklog(projectId, req.from ?? d.from, req.to ?? d.to);
28027
+ }
28028
+ const repoDir = await resolveRepoDir(projectId);
28029
+ return req.mode === "commit" ? collectCommits(repoDir, req.fromSha, req.toSha) : collectVersions(repoDir, req.fromTag, req.toTag);
28030
+ }
28031
+ async function generateChangelog(projectId, req, deps = {}) {
28032
+ const got = await collect(projectId, req);
28033
+ if (!got.ok) return got;
28034
+ const input = got.input;
28035
+ const prompt = changelogPrompt(input, CHANGELOG_TIMEOUT_MS);
28036
+ const { agent, model, effort } = await sessionAgentDefaults();
28037
+ const run2 = deps.think ?? think;
28038
+ let body = "";
28039
+ let generator = "agent";
28040
+ const warnings = [...input.notes];
28041
+ try {
28042
+ const raw = await run2(prompt, { agent, model, effort, timeoutMs: CHANGELOG_TIMEOUT_MS });
28043
+ body = scrubOutput(raw ?? "");
28044
+ if (!body.trim()) throw new Error("agen tak memulangkan teks apa pun");
28045
+ } catch (e) {
28046
+ generator = "fallback";
28047
+ body = fallbackMarkdown(input);
28048
+ warnings.push(`Narasi otomatis tak tersedia \u2014 ${e.message}. Yang tampil adalah draf ringkas.`);
28049
+ }
28050
+ const row = await prisma.changelog.create({
28051
+ data: {
28052
+ projectId,
28053
+ mode: input.mode,
28054
+ title: input.title,
28055
+ params: req,
28056
+ body,
28057
+ generator,
28058
+ itemCount: input.items.length,
28059
+ warning: warnings.length ? warnings.join(" ") : null
28060
+ }
28061
+ });
28062
+ return { ok: true, row };
28063
+ }
28064
+
28065
+ // src/routes/changelog.ts
28066
+ var view6 = (c) => ({
28067
+ id: c.id,
28068
+ projectId: c.projectId,
28069
+ mode: c.mode,
28070
+ title: c.title,
28071
+ params: c.params,
28072
+ body: c.body,
28073
+ generator: c.generator,
28074
+ warning: c.warning,
28075
+ itemCount: c.itemCount,
28076
+ createdAt: c.createdAt.toISOString()
28077
+ });
28078
+ async function changelog_default(app2) {
28079
+ app2.get("/projects/:id/changelog/sources", async (req, reply) => {
28080
+ const { id } = req.params;
28081
+ if (!await prisma.project.findUnique({ where: { id } })) return reply.code(404).send({ error: "not found" });
28082
+ const { tags, head, reason } = await listTags(await resolveRepoDir(id));
28083
+ const done = await prisma.spec.findMany({
28084
+ where: { projectId: id, stage: "done", doneAt: { not: null } },
28085
+ select: { doneAt: true },
28086
+ orderBy: { doneAt: "asc" }
28087
+ });
28088
+ return {
28089
+ hasRepo: reason === null || tags.length > 0,
28090
+ tags,
28091
+ head,
28092
+ reason,
28093
+ backlog: {
28094
+ doneCount: done.length,
28095
+ earliest: done[0]?.doneAt?.toISOString() ?? null,
28096
+ latest: done[done.length - 1]?.doneAt?.toISOString() ?? null
28097
+ },
28098
+ defaultRange: defaultRange(/* @__PURE__ */ new Date())
28099
+ };
28100
+ });
28101
+ app2.get("/projects/:id/changelog", async (req, reply) => {
28102
+ const { id } = req.params;
28103
+ if (!await prisma.project.findUnique({ where: { id } })) return reply.code(404).send({ error: "not found" });
28104
+ const { page, limit } = req.query;
28105
+ const rows = await prisma.changelog.findMany({ where: { projectId: id }, orderBy: { createdAt: "desc" } });
28106
+ return paginate(rows.map(view6), page, limit);
28107
+ });
28108
+ app2.get("/projects/:id/changelog/:cid", async (req, reply) => {
28109
+ const { id, cid } = req.params;
28110
+ const row = await prisma.changelog.findFirst({ where: { id: cid, projectId: id } });
28111
+ if (!row) return reply.code(404).send({ error: "not found" });
28112
+ const fmt = downloadFormat(req.query);
28113
+ if (fmt) return sendDocDownload(reply, fmt, {
28114
+ content: row.body,
28115
+ name: `${row.title}.md`,
28116
+ prefix: `${id}-changelog`,
28117
+ eyebrow: `hanoman \xB7 ${id} \xB7 changelog`,
28118
+ path: row.title
28119
+ });
28120
+ return view6(row);
28121
+ });
28122
+ app2.post("/projects/:id/changelog", async (req, reply) => {
28123
+ const { id } = req.params;
28124
+ if (!await prisma.project.findUnique({ where: { id } })) return reply.code(404).send({ error: "not found" });
28125
+ const parsed = zChangelogRequest.safeParse(req.body);
28126
+ if (!parsed.success) return reply.code(400).send({ error: parsed.error.flatten() });
28127
+ const r = await generateChangelog(id, parsed.data);
28128
+ if (!r.ok) return reply.code(422).send({ error: r.reason });
28129
+ return reply.code(201).send(view6(r.row));
28130
+ });
28131
+ app2.delete("/projects/:id/changelog/:cid", async (req, reply) => {
28132
+ const { id, cid } = req.params;
28133
+ const { count } = await prisma.changelog.deleteMany({ where: { id: cid, projectId: id } });
28134
+ return count ? reply.code(204).send() : reply.code(404).send({ error: "not found" });
28135
+ });
28136
+ }
28137
+
27766
28138
  // src/routes/custom-agents.ts
27767
28139
  init_src();
27768
28140
  init_db();
@@ -27893,7 +28265,7 @@ async function installCustomAgents() {
27893
28265
 
27894
28266
  // src/routes/custom-agents.ts
27895
28267
  var rowsOf = async () => await prisma.customAgent.findMany();
27896
- var view6 = (r, projectId) => ({
28268
+ var view7 = (r, projectId) => ({
27897
28269
  id: r.id,
27898
28270
  projectId: r.projectId,
27899
28271
  name: r.name,
@@ -27938,7 +28310,7 @@ async function custom_agents_default(app2) {
27938
28310
  const byName = /* @__PURE__ */ new Map();
27939
28311
  for (const r of rows) if (r.projectId === null) byName.set(r.name, r);
27940
28312
  for (const r of rows) if (r.projectId !== null) byName.set(r.name, r);
27941
- return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)).map((r) => view6(r, projectId));
28313
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name)).map((r) => view7(r, projectId));
27942
28314
  });
27943
28315
  app2.post("/custom-agents", async (req, reply) => {
27944
28316
  const parsed = zCreateCustomAgent.safeParse(req.body);
@@ -27985,7 +28357,7 @@ async function custom_agents_default(app2) {
27985
28357
  } });
27986
28358
  await loadCustomAgents();
27987
28359
  await notifySynced("customAgent", id);
27988
- return reply.code(201).send(view6(row));
28360
+ return reply.code(201).send(view7(row));
27989
28361
  });
27990
28362
  app2.patch("/custom-agents/:id", async (req, reply) => {
27991
28363
  const { id } = req.params;
@@ -28032,7 +28404,7 @@ async function custom_agents_default(app2) {
28032
28404
  } });
28033
28405
  await loadCustomAgents();
28034
28406
  await notifySynced("customAgent", id);
28035
- return view6(row);
28407
+ return view7(row);
28036
28408
  });
28037
28409
  app2.delete("/custom-agents/:id", async (req, reply) => {
28038
28410
  const { id } = req.params;
@@ -28105,7 +28477,7 @@ async function resolveGithubRepo(projectId) {
28105
28477
 
28106
28478
  // src/services/github-fetch.ts
28107
28479
  init_config2();
28108
- import { execFile as execFile10 } from "node:child_process";
28480
+ import { execFile as execFile11 } from "node:child_process";
28109
28481
  var labelNames = (l) => (l ?? []).map((x) => typeof x === "string" ? x : x.name ?? "").filter(Boolean);
28110
28482
  var norm2 = (s2) => s2.toLowerCase() === "closed" ? "closed" : "open";
28111
28483
  function issueFromGh(raw) {
@@ -28146,7 +28518,7 @@ function issuesFromRest(raw) {
28146
28518
  var GH_FIELDS = "number,title,body,author,labels,url,state,createdAt,updatedAt";
28147
28519
  var API2 = "https://api.github.com";
28148
28520
  var defaultRunGh = (args, env) => new Promise((resolve15, reject2) => {
28149
- execFile10(
28521
+ execFile11(
28150
28522
  args[0],
28151
28523
  args.slice(1),
28152
28524
  { env, maxBuffer: 1 << 26, encoding: "utf8", timeout: 6e4 },
@@ -28373,7 +28745,7 @@ var STATUS = {
28373
28745
  unauthorized: 401,
28374
28746
  other: 502
28375
28747
  };
28376
- var view7 = (i) => ({
28748
+ var view8 = (i) => ({
28377
28749
  id: i.id,
28378
28750
  projectId: i.projectId,
28379
28751
  repoSlug: i.repoSlug,
@@ -28408,7 +28780,7 @@ async function githubIssues(app2) {
28408
28780
  where: { projectId: id, ...status ? { status } : {} },
28409
28781
  orderBy: [{ number: "desc" }]
28410
28782
  });
28411
- return reply.send({ items: items.map(view7) });
28783
+ return reply.send({ items: items.map(view8) });
28412
28784
  });
28413
28785
  app2.post("/github-issues/accept", async (req, reply) => {
28414
28786
  const parsed = zAcceptMany.safeParse(req.body ?? {});
@@ -29120,8 +29492,8 @@ init_db();
29120
29492
  // src/services/auth.ts
29121
29493
  init_db();
29122
29494
  import { randomBytes as randomBytes7, scrypt as scryptCb, timingSafeEqual as timingSafeEqual2, createHash as createHash7 } from "node:crypto";
29123
- import { promisify as promisify9 } from "node:util";
29124
- var scrypt = promisify9(scryptCb);
29495
+ import { promisify as promisify10 } from "node:util";
29496
+ var scrypt = promisify10(scryptCb);
29125
29497
  var COOKIE_NAME = "hn_session";
29126
29498
  var SESSION_TTL_MS = 7 * 24 * 3600 * 1e3;
29127
29499
  async function hashPassword(pw) {
@@ -29191,7 +29563,7 @@ function cookieOpts() {
29191
29563
  }
29192
29564
 
29193
29565
  // src/routes/auth.ts
29194
- var view8 = (u) => ({ id: u.id, email: u.email, createdAt: u.createdAt.toISOString() });
29566
+ var view9 = (u) => ({ id: u.id, email: u.email, createdAt: u.createdAt.toISOString() });
29195
29567
  async function issue(reply, userId) {
29196
29568
  const token = await createSession2(userId);
29197
29569
  reply.setCookie(COOKIE_NAME, token, cookieOpts());
@@ -29209,7 +29581,7 @@ async function auth_default(app2) {
29209
29581
  data: { email: p.data.email, passwordHash: await hashPassword(p.data.password) }
29210
29582
  });
29211
29583
  await issue(reply, user.id);
29212
- return { user: view8(user) };
29584
+ return { user: view9(user) };
29213
29585
  });
29214
29586
  app2.post("/auth/login", async (req, reply) => {
29215
29587
  if (loginThrottle(req.ip).blocked) return reply.code(429).send({ error: "too many attempts" });
@@ -29222,7 +29594,7 @@ async function auth_default(app2) {
29222
29594
  }
29223
29595
  clearLoginFails(req.ip);
29224
29596
  await issue(reply, user.id);
29225
- return { user: view8(user) };
29597
+ return { user: view9(user) };
29226
29598
  });
29227
29599
  app2.post("/auth/logout", async (req, reply) => {
29228
29600
  const token = req.cookies?.[COOKIE_NAME];
@@ -29230,7 +29602,7 @@ async function auth_default(app2) {
29230
29602
  reply.clearCookie(COOKIE_NAME, { path: "/" });
29231
29603
  return reply.code(204).send();
29232
29604
  });
29233
- app2.get("/auth/users", async () => (await prisma.user.findMany({ orderBy: { createdAt: "asc" } })).map(view8));
29605
+ app2.get("/auth/users", async () => (await prisma.user.findMany({ orderBy: { createdAt: "asc" } })).map(view9));
29234
29606
  app2.post("/auth/users", async (req, reply) => {
29235
29607
  const p = zSignup.safeParse(req.body);
29236
29608
  if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
@@ -29239,7 +29611,7 @@ async function auth_default(app2) {
29239
29611
  const user = await prisma.user.create({
29240
29612
  data: { email: p.data.email, passwordHash: await hashPassword(p.data.password) }
29241
29613
  });
29242
- return view8(user);
29614
+ return view9(user);
29243
29615
  });
29244
29616
  app2.delete("/auth/users/:id", async (req, reply) => {
29245
29617
  if (await prisma.user.count() <= 1) return reply.code(400).send({ error: "tak bisa hapus user terakhir" });
@@ -29259,7 +29631,7 @@ async function auth_default(app2) {
29259
29631
  });
29260
29632
  await deleteUserSessions(user.id);
29261
29633
  await issue(reply, user.id);
29262
- return { user: view8(user) };
29634
+ return { user: view9(user) };
29263
29635
  });
29264
29636
  }
29265
29637
 
@@ -29272,15 +29644,15 @@ async function agent_tokens_default(app2) {
29272
29644
  app2.post("/agent-tokens", async (req, reply) => {
29273
29645
  const p = zAgentTokenCreate.safeParse(req.body);
29274
29646
  if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
29275
- const { view: view9, token } = await issueAgentToken({ ...p.data, createdBy: req.user?.id });
29276
- return reply.code(201).send({ ...view9, token });
29647
+ const { view: view10, token } = await issueAgentToken({ ...p.data, createdBy: req.user?.id });
29648
+ return reply.code(201).send({ ...view10, token });
29277
29649
  });
29278
29650
  app2.patch("/agent-tokens/:id", async (req, reply) => {
29279
29651
  const p = zAgentTokenPatch.safeParse(req.body);
29280
29652
  if (!p.success) return reply.code(400).send({ error: p.error.flatten() });
29281
29653
  const { id } = req.params;
29282
- const view9 = await patchAgentToken(id, p.data);
29283
- return view9 ? reply.send(view9) : reply.code(404).send({ error: "not found" });
29654
+ const view10 = await patchAgentToken(id, p.data);
29655
+ return view10 ? reply.send(view10) : reply.code(404).send({ error: "not found" });
29284
29656
  });
29285
29657
  app2.delete("/agent-tokens/:id", async (req, reply) => {
29286
29658
  const { id } = req.params;
@@ -29350,7 +29722,7 @@ function capabilityForRoute(method, path) {
29350
29722
  }
29351
29723
  if (top === "projects") {
29352
29724
  const sub = seg[2];
29353
- if (sub === "docs" || sub === "prds") return rw("docs");
29725
+ if (sub === "docs" || sub === "prds" || sub === "changelog") return rw("docs");
29354
29726
  if (sub === "github") return rw("support");
29355
29727
  if (sub && IDE_SUBS.has(sub)) return rw("ide");
29356
29728
  return rw("projects");
@@ -29495,7 +29867,7 @@ var PUBLIC = /* @__PURE__ */ new Set([
29495
29867
  "GET /api/agent-integration.md"
29496
29868
  ]);
29497
29869
  function buildApp({ requireAuth = true, agentDocFile } = {}) {
29498
- const docFile = agentDocFile !== void 0 ? agentDocFile : pickGuideFile(dirname13(fileURLToPath4(import.meta.url)), process.env, existsSync12);
29870
+ const docFile = agentDocFile !== void 0 ? agentDocFile : pickGuideFile(dirname13(fileURLToPath4(import.meta.url)), process.env, existsSync13);
29499
29871
  const app2 = Fastify({ logger: false, trustProxy: true });
29500
29872
  app2.addContentTypeParser("application/json", { parseAs: "string" }, (_req, body, done) => {
29501
29873
  if (!body) return done(null, void 0);
@@ -29578,9 +29950,10 @@ function buildApp({ requireAuth = true, agentDocFile } = {}) {
29578
29950
  await api.register(githubIssues);
29579
29951
  await api.register(telegramRoutes);
29580
29952
  await api.register(webhooks_default);
29953
+ await api.register(changelog_default);
29581
29954
  }, { prefix: "/api" });
29582
29955
  if (process.env.NODE_ENV === "production") {
29583
- const dist = pickWebDir(dirname13(fileURLToPath4(import.meta.url)), process.env, existsSync12);
29956
+ const dist = pickWebDir(dirname13(fileURLToPath4(import.meta.url)), process.env, existsSync13);
29584
29957
  if (dist) {
29585
29958
  app2.register(fastifyStatic, { root: dist });
29586
29959
  app2.setNotFoundHandler((req, reply) => req.url.startsWith("/api") ? reply.code(404).send({ error: "not found" }) : reply.sendFile("index.html"));
@@ -30116,16 +30489,16 @@ function startWebhookEngine() {
30116
30489
  // src/services/auto-merge.ts
30117
30490
  init_src();
30118
30491
  init_db();
30119
- import { execFile as execFile11 } from "node:child_process";
30120
- import { promisify as promisify10 } from "node:util";
30492
+ import { execFile as execFile12 } from "node:child_process";
30493
+ import { promisify as promisify11 } from "node:util";
30121
30494
  var AUTO_MERGE_WINDOW_MS = 24 * 60 * 60 * 1e3;
30122
30495
  var AUTO_MERGE_GRACE_MS = 15 * 60 * 1e3;
30123
30496
  var TICK_MS4 = 6e4;
30124
- var exec8 = promisify10(execFile11);
30125
- var GIT7 = { timeout: 3e4, maxBuffer: 1 << 24, encoding: "utf8" };
30497
+ var exec9 = promisify11(execFile12);
30498
+ var GIT8 = { timeout: 3e4, maxBuffer: 1 << 24, encoding: "utf8" };
30126
30499
  var gitOut = async (cwd, args) => {
30127
30500
  try {
30128
- return (await exec8("git", args, { cwd, ...GIT7 })).stdout.trim();
30501
+ return (await exec9("git", args, { cwd, ...GIT8 })).stdout.trim();
30129
30502
  } catch {
30130
30503
  return null;
30131
30504
  }