@ulpi/cli 0.1.4 → 0.1.6

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 (112) hide show
  1. package/LICENSE +21 -0
  2. package/dist/{auth-PN7TMQHV-2W4ICG64.js → auth-FWM7MM4Q-VZC3U2XZ.js} +1 -1
  3. package/dist/{auth-ECQ3IB4E.js → auth-HDK7ECJL.js} +2 -1
  4. package/dist/{chunk-3SBPZRB5.js → chunk-3BCW6ABU.js} +402 -142
  5. package/dist/{chunk-JGBXM5NC.js → chunk-3WB5CXH4.js} +180 -5
  6. package/dist/{chunk-2HEE5OKX.js → chunk-4UCJIAOU.js} +2 -2
  7. package/dist/chunk-4XTHZVDS.js +109 -0
  8. package/dist/chunk-4ZPOZULQ.js +6522 -0
  9. package/dist/{chunk-SIAQVRKG.js → chunk-5MI5GIXM.js} +48 -2
  10. package/dist/{chunk-KLEASXUR.js → chunk-6ZL6NXMV.js} +1 -1
  11. package/dist/chunk-76D3BYJD.js +221 -0
  12. package/dist/{chunk-ZLYRPD7I.js → chunk-AWOSRA5F.js} +1 -1
  13. package/dist/{chunk-PDR55ZNW.js → chunk-BFEKZZHM.js} +274 -57
  14. package/dist/chunk-C7CLUQI6.js +1286 -0
  15. package/dist/{chunk-7AL4DOEJ.js → chunk-E3B5NROU.js} +7 -7
  16. package/dist/chunk-EJ7TW77N.js +1418 -0
  17. package/dist/{chunk-5J6NLQUN.js → chunk-IV6MWETF.js} +383 -168
  18. package/dist/chunk-IZPJHSPX.js +1478 -0
  19. package/dist/chunk-JLHNLM3C.js +228 -0
  20. package/dist/{chunk-BZL5H4YQ.js → chunk-KYYI23AQ.js} +2 -2
  21. package/dist/{chunk-2CLNOKPA.js → chunk-RSFJ6QSR.js} +18 -0
  22. package/dist/chunk-S6ANCSYO.js +1271 -0
  23. package/dist/chunk-SEU7WWNQ.js +1251 -0
  24. package/dist/chunk-SNQ7NAIS.js +453 -0
  25. package/dist/{ulpi-RMMCUAGP-JCJ273T6.js → chunk-TSLDGT5O.js} +73 -35
  26. package/dist/{chunk-SPOI23SB.js → chunk-UXHCHOWQ.js} +83 -62
  27. package/dist/chunk-V2H5D6Y3.js +146 -0
  28. package/dist/{chunk-QJ5GSMEC.js → chunk-VVEDXI7E.js} +2 -1
  29. package/dist/chunk-VXH5Y4FO.js +6761 -0
  30. package/dist/chunk-WED4LM5N.js +322 -0
  31. package/dist/{chunk-74WVVWJ4.js → chunk-YOKL7RB5.js} +184 -15
  32. package/dist/chunk-Z53CAR7G.js +298 -0
  33. package/dist/ci-X3U2W4HC.js +854 -0
  34. package/dist/cloud-2F3NLVHN.js +274 -0
  35. package/dist/{codemap-RKSD4MIE.js → codemap-XNGMAF3F.js} +37 -37
  36. package/dist/codex-MB5YTMRT.js +132 -0
  37. package/dist/{config-EGAXXCGL.js → config-OOELBYTH.js} +1 -1
  38. package/dist/dist-2BJYR5EI.js +59 -0
  39. package/dist/dist-2K7IEVTA.js +43 -0
  40. package/dist/dist-3EIQTZHT.js +1380 -0
  41. package/dist/{dist-YA2BWZB2.js → dist-4U5L2X2C.js} +2 -2
  42. package/dist/{dist-UKMCJBB2.js → dist-54KAMNLO.js} +16 -15
  43. package/dist/dist-6M4MZWZW.js +58 -0
  44. package/dist/dist-6X576SU2.js +27 -0
  45. package/dist/dist-7QOEYLFX.js +103 -0
  46. package/dist/dist-AYBGHEDY.js +2541 -0
  47. package/dist/dist-EK45QNEM.js +45 -0
  48. package/dist/{dist-CS2VKNYS.js → dist-FKFEJRPX.js} +16 -15
  49. package/dist/dist-GTEJUBBT.js +66 -0
  50. package/dist/dist-HA74OKJZ.js +40 -0
  51. package/dist/dist-HU5RZAON.js +48 -0
  52. package/dist/dist-IYE3OBRB.js +374 -0
  53. package/dist/{dist-GJYT2OQV.js → dist-JLU26AB6.js} +12 -9
  54. package/dist/{dist-6G7JC2RA.js → dist-KUCI6JFE.js} +49 -9
  55. package/dist/dist-NUEMFZFL.js +33 -0
  56. package/dist/{dist-RKOGLK7R.js → dist-NUXMDXZ3.js} +31 -3
  57. package/dist/{dist-QAU3LGJN.js → dist-YCNWHSLN.js} +15 -5
  58. package/dist/{dist-CB5D5LMO.js → dist-YFFG2ZD6.js} +9 -16
  59. package/dist/dist-ZG4OKCSR.js +15 -0
  60. package/dist/doctor-SI4LLLDZ.js +345 -0
  61. package/dist/{export-import-4A5MWLIA.js → export-import-JFQH4KSJ.js} +1 -1
  62. package/dist/{history-3MOBX4MA.js → history-5NE46ZAH.js} +7 -7
  63. package/dist/hooks-installer-UN5JZLDQ.js +19 -0
  64. package/dist/index.js +395 -619
  65. package/dist/{init-6CH4HV5T.js → init-5FK3VKRT.js} +79 -13
  66. package/dist/job-HIDMAFW2.js +376 -0
  67. package/dist/jobs.memory-PLMMSFHB-VBECCTHN.js +33 -0
  68. package/dist/kiro-VMUHDFGK.js +153 -0
  69. package/dist/{launchd-LF2QMSKZ.js → launchd-6AWT54HR.js} +9 -17
  70. package/dist/mcp-PDUD7SGP.js +249 -0
  71. package/dist/mcp-installer-PQU3XOGO.js +259 -0
  72. package/dist/mcp-setup-OA7IB3H3.js +263 -0
  73. package/dist/{memory-Y6OZTXJ2.js → memory-ZNAEAK3B.js} +17 -17
  74. package/dist/{ollama-3XCUZMZT-FYKHW4TZ.js → ollama-3XCUZMZT-4JMH6B7P.js} +1 -1
  75. package/dist/{openai-E7G2YAHU-UYY4ZWON.js → openai-E7G2YAHU-T3HMBPH7.js} +2 -2
  76. package/dist/portal-JYWVHXDU.js +210 -0
  77. package/dist/prd-Q4J5NVAR.js +408 -0
  78. package/dist/repos-WWZXNN3P.js +271 -0
  79. package/dist/review-integration-5WHEJU2A.js +14 -0
  80. package/dist/{rules-E427DKYJ.js → rules-Y4VSOY5Y.js} +3 -3
  81. package/dist/run-VPNXEIBY.js +687 -0
  82. package/dist/server-COL4AXKU-P7S7NNF6.js +11 -0
  83. package/dist/server-KKSETHDV-XSSLEENT.js +20 -0
  84. package/dist/{skills-CX73O3IV.js → skills-QEYU2N27.js} +4 -2
  85. package/dist/start-JYOEL7AJ.js +303 -0
  86. package/dist/{status-4DFHDJMN.js → status-BHQYYGAL.js} +2 -2
  87. package/dist/{templates-U7T6MARD.js → templates-CBRUJ66V.js} +4 -3
  88. package/dist/tui-DP7736EX.js +61 -0
  89. package/dist/ulpi-5EN6JCAS-LFE3WSL4.js +10 -0
  90. package/dist/{uninstall-6SW35IK4.js → uninstall-ICUV6DDV.js} +3 -3
  91. package/dist/{update-M6IBJNYP.js → update-7ZMAYRBH.js} +3 -3
  92. package/dist/{version-checker-Q6YTYAGP.js → version-checker-4ZFMZA7Y.js} +2 -2
  93. package/package.json +39 -31
  94. package/dist/chunk-2MZER6ND.js +0 -415
  95. package/dist/chunk-2VYFVYJL.js +0 -4273
  96. package/dist/chunk-6OCEY7JY.js +0 -422
  97. package/dist/chunk-7LXY5UVC.js +0 -330
  98. package/dist/chunk-B55DDP24.js +0 -136
  99. package/dist/chunk-JWUUVXIV.js +0 -13694
  100. package/dist/chunk-MIAQVCFW.js +0 -39
  101. package/dist/chunk-YM2HV4IA.js +0 -505
  102. package/dist/ci-STSL2LSP.js +0 -370
  103. package/dist/mcp-installer-NQCGKQ23.js +0 -124
  104. package/dist/projects-ATHDD3D6.js +0 -271
  105. package/dist/review-ADUPV3PN.js +0 -152
  106. package/dist/server-USLHY6GH-AEOJC5ST.js +0 -18
  107. package/dist/server-X5P6WH2M-7K2RY34N.js +0 -11
  108. package/dist/skills/ulpi-generate-guardian/SKILL.md +0 -750
  109. package/dist/skills/ulpi-generate-guardian/references/framework-rules.md +0 -849
  110. package/dist/skills/ulpi-generate-guardian/references/language-rules.md +0 -591
  111. package/dist/ui-OWXZ3YSR.js +0 -167
  112. package/dist/ui.html +0 -698
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2026 ULPI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -4,7 +4,7 @@ import {
4
4
  setApiSecret,
5
5
  validateAuth,
6
6
  validateLoopback
7
- } from "./chunk-MIAQVCFW.js";
7
+ } from "./chunk-Z53CAR7G.js";
8
8
  import "./chunk-4VNS5WPM.js";
9
9
  export {
10
10
  generateApiSecret,
@@ -2,7 +2,8 @@ import {
2
2
  extractCredentials,
3
3
  getCredentialExpiry,
4
4
  validateCredentials
5
- } from "./chunk-B55DDP24.js";
5
+ } from "./chunk-76D3BYJD.js";
6
+ import "./chunk-KIKPIH6N.js";
6
7
  import "./chunk-4VNS5WPM.js";
7
8
 
8
9
  // src/commands/auth.ts
@@ -1,22 +1,20 @@
1
+ import {
2
+ API_LOCK_FILE,
3
+ REVIEWS_DIR,
4
+ getApiHost,
5
+ getApiPort
6
+ } from "./chunk-C7CLUQI6.js";
1
7
  import {
2
8
  external_exports
3
9
  } from "./chunk-KIKPIH6N.js";
4
- import {
5
- REVIEWS_DIR
6
- } from "./chunk-7LXY5UVC.js";
7
- import {
8
- __require
9
- } from "./chunk-4VNS5WPM.js";
10
+
11
+ // ../../packages/review-runtime/dist/index.js
12
+ import { basename } from "path";
13
+ import { randomUUID } from "crypto";
10
14
 
11
15
  // ../../packages/review-engine/dist/index.js
12
16
  import { promises as fs } from "fs";
13
17
  import { join } from "path";
14
- var __require2 = /* @__PURE__ */ ((x) => typeof __require !== "undefined" ? __require : typeof Proxy !== "undefined" ? new Proxy(x, {
15
- get: (a, b) => (typeof __require !== "undefined" ? __require : a)[b]
16
- }) : x)(function(x) {
17
- if (typeof __require !== "undefined") return __require.apply(this, arguments);
18
- throw Error('Dynamic require of "' + x + '" is not supported');
19
- });
20
18
  function parseMarkdownToBlocks(markdown) {
21
19
  if (!markdown) return [];
22
20
  try {
@@ -251,9 +249,6 @@ function extractSections(blocks) {
251
249
  }
252
250
  return sections;
253
251
  }
254
- function getSectionFullText(section, blocks) {
255
- return section.blockIds.map((id) => blocks.find((b) => b.id === id)).filter((b) => b != null).map((b) => b.content).join("\n\n");
256
- }
257
252
  var FILE_PATH_PATTERN = /(?:src\/|\.\/|\/)[a-zA-Z0-9_\-/.]+\.[a-zA-Z]{1,5}/g;
258
253
  var DIMENSION_CHECKERS = [
259
254
  {
@@ -617,156 +612,421 @@ async function collectPlansFromDir(baseDir, summaries) {
617
612
  } catch {
618
613
  }
619
614
  }
620
- function isGitCommitCommand(command) {
621
- if (!command) return false;
622
- const subcommands = command.split(/\s*(?:&&|\|\||[;|])\s*/);
623
- return subcommands.some((sub) => /(?:^|\b)(?:\w+=\S*\s+)*git(?:\s+-c\s+\S+)*\s+commit\b/.test(sub));
615
+ var ReviewConfigSchema = external_exports.object({
616
+ enabled: external_exports.boolean().default(true),
617
+ plan_review: external_exports.boolean().default(true),
618
+ code_review: external_exports.boolean().default(true),
619
+ auto_open_browser: external_exports.boolean().default(true),
620
+ require_server: external_exports.boolean().default(false),
621
+ review_timeout_seconds: external_exports.number().int().min(0).max(600).default(0),
622
+ timeout_behavior: external_exports.enum(["allow", "deny"]).default("allow"),
623
+ default_export_format: external_exports.enum(["markdown", "github", "jira", "json"]).default("markdown"),
624
+ webhook_url: external_exports.string().default("")
625
+ });
626
+
627
+ // ../../packages/review-runtime/dist/index.js
628
+ import * as fs2 from "fs";
629
+ var ReviewHub = class _ReviewHub {
630
+ sessions = /* @__PURE__ */ new Map();
631
+ awaitWaiters = /* @__PURE__ */ new Map();
632
+ decidedEvictionMs = 5 * 60 * 1e3;
633
+ longPollTimeoutMs = 30 * 1e3;
634
+ /** Maximum number of concurrent sessions to prevent unbounded memory growth. */
635
+ static MAX_SESSIONS = 100;
636
+ /** Sessions older than this TTL are automatically evicted. */
637
+ static SESSION_TTL_MS = 30 * 60 * 1e3;
638
+ // 30 minutes
639
+ /**
640
+ * Register a new review session.
641
+ */
642
+ async register(payload) {
643
+ this.cleanupExpired();
644
+ if (this.sessions.size >= _ReviewHub.MAX_SESSIONS) {
645
+ const oldest = [...this.sessions.entries()].sort(([, a], [, b]) => a.registeredAt - b.registeredAt)[0];
646
+ if (oldest) {
647
+ this.sessions.delete(oldest[0]);
648
+ this.awaitWaiters.delete(oldest[0]);
649
+ }
650
+ }
651
+ const sessionId = randomUUID();
652
+ const token = randomUUID();
653
+ const projectSlug = basename(payload.projectPath);
654
+ let resolveDecision;
655
+ const decisionPromise = new Promise((resolve) => {
656
+ resolveDecision = resolve;
657
+ });
658
+ const session = {
659
+ id: sessionId,
660
+ type: payload.type,
661
+ projectPath: payload.projectPath,
662
+ projectSlug,
663
+ title: "",
664
+ status: "pending",
665
+ registeredAt: Date.now(),
666
+ resolveDecision,
667
+ token
668
+ };
669
+ if (payload.type === "plan" && payload.plan) {
670
+ const blocks = parseMarkdownToBlocks(payload.plan);
671
+ const sections = extractSections(blocks);
672
+ const quality = scorePlanQuality(blocks, sections);
673
+ const title = extractTitle(blocks);
674
+ const slug = generateSlug(title);
675
+ const versionNumber = await getNextVersionNumber(slug, payload.projectPath);
676
+ const version = {
677
+ id: randomUUID(),
678
+ versionNumber,
679
+ markdown: payload.plan,
680
+ annotations: [],
681
+ inlineEdits: [],
682
+ instructions: [],
683
+ priorities: [],
684
+ risks: [],
685
+ createdAt: Date.now()
686
+ };
687
+ try {
688
+ await savePlan(payload.plan, version, slug, payload.projectPath);
689
+ } catch (err) {
690
+ console.error(`[review-hub] Failed to persist plan: ${err instanceof Error ? err.message : String(err)}`);
691
+ }
692
+ session.plan = payload.plan;
693
+ session.blocks = blocks;
694
+ session.sections = sections;
695
+ session.quality = quality;
696
+ session.version = version;
697
+ session.slug = slug;
698
+ session.title = title;
699
+ } else if (payload.type === "code") {
700
+ session.diff = payload.diff;
701
+ session.commitMessage = payload.commitMessage;
702
+ session.title = payload.commitMessage || "Code Review";
703
+ }
704
+ this.sessions.set(sessionId, session);
705
+ decisionPromise.then((decision) => {
706
+ const waiters = this.awaitWaiters.get(sessionId) || [];
707
+ for (const resolve of waiters) {
708
+ resolve(decision);
709
+ }
710
+ this.awaitWaiters.delete(sessionId);
711
+ });
712
+ return { session: this.toPublicSession(session), sessionId, token };
713
+ }
714
+ /**
715
+ * Get a session by ID.
716
+ */
717
+ getSession(id) {
718
+ const session = this.sessions.get(id);
719
+ return session ? this.toPublicSession(session) : null;
720
+ }
721
+ /**
722
+ * List all sessions, optionally filtered by type/status.
723
+ */
724
+ listSessions(opts) {
725
+ let sessions = [...this.sessions.values()];
726
+ if (opts?.type) sessions = sessions.filter((s) => s.type === opts.type);
727
+ if (opts?.status) sessions = sessions.filter((s) => s.status === opts.status);
728
+ sessions.sort((a, b) => a.registeredAt - b.registeredAt);
729
+ return sessions.map((s) => this.toPublicSession(s));
730
+ }
731
+ /**
732
+ * Verify a session token.
733
+ */
734
+ verifyToken(sessionId, token) {
735
+ const session = this.sessions.get(sessionId);
736
+ if (!session) return false;
737
+ return session.token === token;
738
+ }
739
+ /**
740
+ * Submit a decision for a session.
741
+ * When token is provided, it must match the session token.
742
+ */
743
+ async submitDecision(sessionId, decision, token) {
744
+ const session = this.sessions.get(sessionId);
745
+ if (!session) return false;
746
+ if (token !== void 0 && session.token !== token) return false;
747
+ if (!isValidDecision(decision)) return false;
748
+ try {
749
+ if (session.type === "plan" && session.version && session.slug) {
750
+ const planDecision = decision;
751
+ const reviewedVersion = {
752
+ ...session.version,
753
+ annotations: planDecision.annotations || [],
754
+ inlineEdits: planDecision.inlineEdits || [],
755
+ instructions: planDecision.instructions || [],
756
+ priorities: planDecision.priorities || [],
757
+ risks: planDecision.risks || [],
758
+ decision: {
759
+ behavior: planDecision.behavior,
760
+ message: planDecision.message,
761
+ decidedAt: Date.now()
762
+ }
763
+ };
764
+ await savePlan(session.plan, reviewedVersion, session.slug, session.projectPath);
765
+ }
766
+ } catch {
767
+ }
768
+ session.status = "decided";
769
+ session.decidedAt = Date.now();
770
+ session.decisionPayload = decision;
771
+ session.resolveDecision(decision);
772
+ const waiters = this.awaitWaiters.get(sessionId);
773
+ if (waiters && waiters.length > 0) {
774
+ for (const waiter of waiters) {
775
+ waiter(decision);
776
+ }
777
+ this.awaitWaiters.delete(sessionId);
778
+ }
779
+ setTimeout(() => {
780
+ this.sessions.delete(sessionId);
781
+ this.awaitWaiters.delete(sessionId);
782
+ }, this.decidedEvictionMs);
783
+ return true;
784
+ }
785
+ /**
786
+ * Long-poll waiting for a decision on a session.
787
+ * Returns the decision or null on timeout.
788
+ */
789
+ awaitDecision(sessionId, timeoutMs) {
790
+ const session = this.sessions.get(sessionId);
791
+ if (!session) return Promise.resolve(null);
792
+ if (session.status === "decided" && session.decisionPayload) {
793
+ return Promise.resolve(session.decisionPayload);
794
+ }
795
+ const timeout = timeoutMs ?? this.longPollTimeoutMs;
796
+ return new Promise((resolve) => {
797
+ const waiters = this.awaitWaiters.get(sessionId) || [];
798
+ const timer = setTimeout(() => {
799
+ const idx = waiters.indexOf(onDecision);
800
+ if (idx !== -1) waiters.splice(idx, 1);
801
+ resolve(null);
802
+ }, timeout);
803
+ function onDecision(decision) {
804
+ clearTimeout(timer);
805
+ resolve(decision);
806
+ }
807
+ waiters.push(onDecision);
808
+ this.awaitWaiters.set(sessionId, waiters);
809
+ });
810
+ }
811
+ /**
812
+ * Save annotations/version for a plan session.
813
+ */
814
+ async saveVersion(sessionId, data) {
815
+ const session = this.sessions.get(sessionId);
816
+ if (!session || session.type !== "plan" || !session.version || !session.slug) {
817
+ return false;
818
+ }
819
+ const updatedVersion = {
820
+ ...session.version,
821
+ annotations: data.annotations || [],
822
+ inlineEdits: data.inlineEdits || [],
823
+ instructions: data.instructions || [],
824
+ priorities: data.priorities || [],
825
+ risks: data.risks || []
826
+ };
827
+ await savePlan(session.plan, updatedVersion, session.slug, session.projectPath);
828
+ Object.assign(session.version, updatedVersion);
829
+ return true;
830
+ }
831
+ /**
832
+ * Get the session token (for loopback clients that need to pass it to the UI).
833
+ */
834
+ getSessionToken(id) {
835
+ const session = this.sessions.get(id);
836
+ return session?.token ?? null;
837
+ }
838
+ /**
839
+ * Get the raw decision payload for a session (used by await endpoint).
840
+ */
841
+ getDecisionPayload(id) {
842
+ const session = this.sessions.get(id);
843
+ return session?.decisionPayload ?? null;
844
+ }
845
+ /**
846
+ * Cleanup orphaned sessions (pending for over 1 hour).
847
+ */
848
+ cleanup() {
849
+ const now = Date.now();
850
+ for (const [id, session] of this.sessions) {
851
+ if (session.status === "pending" && now - session.registeredAt > 60 * 60 * 1e3) {
852
+ this.sessions.delete(id);
853
+ this.awaitWaiters.delete(id);
854
+ }
855
+ }
856
+ }
857
+ /**
858
+ * Remove sessions that have exceeded the TTL, regardless of status.
859
+ */
860
+ cleanupExpired() {
861
+ const now = Date.now();
862
+ for (const [id, session] of this.sessions) {
863
+ if (now - session.registeredAt > _ReviewHub.SESSION_TTL_MS) {
864
+ this.sessions.delete(id);
865
+ this.awaitWaiters.delete(id);
866
+ }
867
+ }
868
+ }
869
+ toPublicSession(session) {
870
+ const { resolveDecision, decisionPayload, token: _token, ...pub } = session;
871
+ if (decisionPayload && !pub.decision) {
872
+ if ("behavior" in decisionPayload) {
873
+ pub.decision = {
874
+ behavior: decisionPayload.behavior,
875
+ message: decisionPayload.message,
876
+ decidedAt: session.decidedAt ?? Date.now()
877
+ };
878
+ } else if ("approved" in decisionPayload) {
879
+ pub.decision = {
880
+ behavior: decisionPayload.approved ? "allow" : "deny",
881
+ message: decisionPayload.message ?? decisionPayload.feedback,
882
+ decidedAt: session.decidedAt ?? Date.now()
883
+ };
884
+ }
885
+ }
886
+ return pub;
887
+ }
888
+ };
889
+ function isValidDecision(d) {
890
+ if (!d || typeof d !== "object") return false;
891
+ if ("behavior" in d) {
892
+ return d.behavior === "allow" || d.behavior === "deny";
893
+ }
894
+ if ("approved" in d) {
895
+ return typeof d.approved === "boolean";
896
+ }
897
+ return false;
624
898
  }
625
- function gitOutputSync(args, cwd) {
626
- const { execFileSync } = __require2("child_process");
899
+ var DISCOVERY_TIMEOUT_MS = 2e3;
900
+ var MAX_PORTS_TO_PROBE = 5;
901
+ function isProcessAlive(pid) {
627
902
  try {
628
- return execFileSync(args[0], args.slice(1), {
629
- cwd,
630
- encoding: "utf-8",
631
- timeout: 1e4,
632
- maxBuffer: 10 * 1024 * 1024
633
- });
903
+ process.kill(pid, 0);
904
+ return true;
634
905
  } catch {
635
- return "";
906
+ return false;
636
907
  }
637
908
  }
638
- function captureUntrackedDiff(files, cwd) {
639
- const { existsSync, readFileSync } = __require2("fs");
640
- const { join: join2 } = __require2("path");
641
- const parts = [];
642
- for (const file of files) {
643
- const fullPath = join2(cwd, file);
644
- if (!existsSync(fullPath)) continue;
645
- const noIndexDiff = gitOutputSync(["git", "diff", "--no-index", "--", "/dev/null", file], cwd);
646
- if (noIndexDiff.trim()) {
647
- parts.push(noIndexDiff);
648
- } else {
909
+ function readLockFile() {
910
+ try {
911
+ const raw = fs2.readFileSync(API_LOCK_FILE, "utf-8");
912
+ const data = JSON.parse(raw);
913
+ if (typeof data === "object" && data !== null && typeof data.port === "number" && typeof data.pid === "number") {
914
+ const { port, pid, secret } = data;
915
+ if (isProcessAlive(pid)) {
916
+ return { port, secret };
917
+ }
649
918
  try {
650
- const content = readFileSync(fullPath, "utf-8");
651
- const lines = content.split("\n");
652
- const header = `diff --git a/${file} b/${file}
653
- new file mode 100644
654
- --- /dev/null
655
- +++ b/${file}
656
- @@ -0,0 +1,${lines.length} @@`;
657
- const body = lines.map((l) => `+${l}`).join("\n");
658
- parts.push(`${header}
659
- ${body}`);
919
+ fs2.unlinkSync(API_LOCK_FILE);
660
920
  } catch {
661
921
  }
662
922
  }
923
+ } catch {
663
924
  }
664
- return parts.join("\n");
925
+ return null;
665
926
  }
666
- function captureCommitDiff(opts) {
667
- const { command, cwd } = opts;
668
- let commitMessage = "";
669
- const heredocMatch = command.match(/<<'?EOF'?\s*\n([\s\S]*?)\n\s*EOF/);
670
- if (heredocMatch) {
671
- commitMessage = heredocMatch[1].trim();
672
- } else {
673
- const mMatch = command.match(/-m\s+(?:"([^"]*(?:\\.[^"]*)*)"|'([^']*)'|(\S+))/);
674
- if (mMatch) {
675
- commitMessage = mMatch[1] ?? mMatch[2] ?? mMatch[3] ?? "";
676
- }
677
- }
678
- const hasAllFlag = /\bcommit\b[^|&;]*(?:-[a-z]*a[a-z]*\b|--all\b)/.test(command);
679
- const isAmend = /\bcommit\b[^|&;\n]*--amend\b/.test(command);
680
- const allowEmpty = /\bcommit\b[^|&;\n]*--allow-empty\b/.test(command);
681
- const scopedFiles = [];
682
- let hasChainedAdd = false;
683
- let isBroadAdd = false;
684
- const chainedAddMatch = command.match(/\bgit\s+add\s+([\s\S]*?)(?:&&|;|\n)\s*git\b.*\bcommit\b/);
685
- if (chainedAddMatch) {
686
- hasChainedAdd = true;
687
- const addArgs = chainedAddMatch[1].trim();
688
- isBroadAdd = addArgs === "." || /(?:^|\s)(?:-A|--all|-u|--update)(?:\s|$)/.test(addArgs);
689
- const files = addArgs.split(/\s+/).filter((arg) => !arg.startsWith("-") && arg !== ".");
690
- scopedFiles.push(...files);
691
- }
692
- const commitPart = command.match(/\bgit\b.*\bcommit\b(.*)/s);
693
- if (commitPart) {
694
- let rest = commitPart[1];
695
- rest = rest.replace(/-m\s+(?:"[^"]*(?:\\.[^"]*)*"|'[^']*'|\S+)/, "");
696
- rest = rest.replace(/<<'?EOF'?\s*\n[\s\S]*?\n\s*EOF/, "");
697
- rest = rest.replace(/-m\s*"?\$\(cat\s*<<'?EOF'?[\s\S]*?EOF\s*\)"?/, "");
698
- rest = rest.replace(/\s--(?:amend|no-edit|no-verify|allow-empty|signoff|gpg-sign|no-gpg-sign|fixup|squash|reset-author|short|branch|long|porcelain|dry-run|verbose|quiet|all)\b/g, "");
699
- rest = rest.replace(/\s-[aSsnvq]\b/g, "");
700
- rest = rest.replace(/\s--\s/, " ");
701
- const commitFiles = rest.trim().split(/\s+/).filter((f) => f && !f.startsWith("-"));
702
- if (commitFiles.length > 0) {
703
- scopedFiles.push(...commitFiles);
704
- }
705
- }
706
- let diff = "";
707
- if (isAmend) {
708
- const baseArgs = scopedFiles.length > 0 ? ["git", "diff", "HEAD~1", "--", ...scopedFiles] : ["git", "diff", "HEAD~1"];
709
- diff = gitOutputSync(baseArgs, cwd);
710
- if (!diff.trim()) {
711
- const fallbackArgs = scopedFiles.length > 0 ? ["git", "diff", "HEAD", "--", ...scopedFiles] : ["git", "diff", "HEAD"];
712
- diff = gitOutputSync(fallbackArgs, cwd);
713
- }
714
- } else if (hasChainedAdd && !isBroadAdd && scopedFiles.length > 0) {
715
- const stagedDiff = gitOutputSync(["git", "diff", "--cached", "--", ...scopedFiles], cwd);
716
- const unstagedDiff = gitOutputSync(["git", "diff", "--", ...scopedFiles], cwd);
717
- diff = [stagedDiff, unstagedDiff].filter((d) => d.trim()).join("\n");
718
- if (!diff.trim()) {
719
- diff = captureUntrackedDiff(scopedFiles, cwd);
720
- }
721
- } else if (hasChainedAdd && isBroadAdd) {
722
- const stagedDiff = gitOutputSync(["git", "diff", "--cached"], cwd);
723
- const unstagedDiff = gitOutputSync(["git", "diff"], cwd);
724
- diff = [stagedDiff, unstagedDiff].filter((d) => d.trim()).join("\n");
725
- if (!diff.trim()) {
726
- const untrackedFiles = gitOutputSync(["git", "ls-files", "--others", "--exclude-standard"], cwd).split("\n").filter((f) => f.trim());
727
- if (untrackedFiles.length > 0) {
728
- diff = captureUntrackedDiff(untrackedFiles, cwd);
729
- }
927
+ async function healthCheck(host, port) {
928
+ try {
929
+ const controller = new AbortController();
930
+ const timer = setTimeout(() => controller.abort(), DISCOVERY_TIMEOUT_MS);
931
+ const res = await fetch(`http://${host}:${port}/api/health`, {
932
+ signal: controller.signal
933
+ });
934
+ clearTimeout(timer);
935
+ return res.ok;
936
+ } catch {
937
+ return false;
938
+ }
939
+ }
940
+ async function discoverUlpiServer() {
941
+ const host = getApiHost();
942
+ const lockData = readLockFile();
943
+ if (lockData !== null) {
944
+ if (await healthCheck(host, lockData.port)) {
945
+ return lockData;
730
946
  }
731
- } else {
732
- const baseArgs = hasAllFlag ? ["git", "diff", "HEAD"] : ["git", "diff", "--cached"];
733
- const diffArgs = scopedFiles.length > 0 ? [...baseArgs, "--", ...scopedFiles] : baseArgs;
734
- diff = gitOutputSync(diffArgs, cwd);
735
- if (!diff.trim() && hasAllFlag) {
736
- const fallbackArgs = scopedFiles.length > 0 ? ["git", "diff", "--cached", "--", ...scopedFiles] : ["git", "diff", "--cached"];
737
- diff = gitOutputSync(fallbackArgs, cwd);
947
+ }
948
+ const configPort = getApiPort();
949
+ const ports = [configPort, ...configPort !== 9800 ? [9800] : []].slice(0, MAX_PORTS_TO_PROBE);
950
+ for (const port of ports) {
951
+ if (port === lockData?.port) continue;
952
+ if (await healthCheck(host, port)) {
953
+ return { port };
738
954
  }
739
- if (!diff.trim() && scopedFiles.length > 0) {
740
- diff = captureUntrackedDiff(scopedFiles, cwd);
955
+ }
956
+ return null;
957
+ }
958
+ function buildHeaders(secret) {
959
+ const headers = { "Content-Type": "application/json" };
960
+ if (secret) headers["X-Ulpi-Secret"] = secret;
961
+ return headers;
962
+ }
963
+ async function registerWithServer(port, payload, secret) {
964
+ try {
965
+ const host = getApiHost();
966
+ const res = await fetch(`http://${host}:${port}/api/review/hub/register`, {
967
+ method: "POST",
968
+ headers: buildHeaders(secret),
969
+ body: JSON.stringify(payload)
970
+ });
971
+ if (!res.ok) return null;
972
+ const data = await res.json();
973
+ return data;
974
+ } catch {
975
+ return null;
976
+ }
977
+ }
978
+ async function waitForServerDecision(port, sessionId, timeoutMs = 3e4, maxWaitMs, token, secret) {
979
+ const deadline = Date.now() + (maxWaitMs && maxWaitMs > 0 ? maxWaitMs : 10 * 60 * 1e3);
980
+ const host = getApiHost();
981
+ const tokenParam = token ? `?token=${encodeURIComponent(token)}` : "";
982
+ const secretHeaders = secret ? { "X-Ulpi-Secret": secret } : {};
983
+ while (Date.now() < deadline) {
984
+ try {
985
+ const controller = new AbortController();
986
+ const timer = setTimeout(() => controller.abort(), timeoutMs + 5e3);
987
+ const res = await fetch(
988
+ `http://${host}:${port}/api/review/hub/session/${sessionId}/await${tokenParam}`,
989
+ { signal: controller.signal, headers: secretHeaders }
990
+ );
991
+ clearTimeout(timer);
992
+ if (res.status === 204) {
993
+ continue;
994
+ }
995
+ if (res.ok) {
996
+ const data = await res.json();
997
+ return data.decision;
998
+ }
999
+ if (res.status === 410) {
1000
+ try {
1001
+ const data = await res.json();
1002
+ if (data.decision) return data.decision;
1003
+ } catch {
1004
+ }
1005
+ return null;
1006
+ }
1007
+ if (res.status >= 500) {
1008
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
1009
+ continue;
1010
+ }
1011
+ return null;
1012
+ } catch {
1013
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
741
1014
  }
742
1015
  }
743
- return { diff, commitMessage, allowEmpty, isAmend };
1016
+ return null;
744
1017
  }
745
- var ReviewConfigSchema = external_exports.object({
746
- enabled: external_exports.boolean().default(true),
747
- plan_review: external_exports.boolean().default(true),
748
- code_review: external_exports.boolean().default(true),
749
- auto_open_browser: external_exports.boolean().default(true),
750
- require_server: external_exports.boolean().default(false),
751
- review_timeout_seconds: external_exports.number().int().min(0).max(600).default(0),
752
- timeout_behavior: external_exports.enum(["allow", "deny"]).default("allow"),
753
- default_export_format: external_exports.enum(["markdown", "github", "jira", "json"]).default("markdown"),
754
- webhook_url: external_exports.string().default("")
755
- });
756
1018
 
757
1019
  export {
758
1020
  parseMarkdownToBlocks,
759
- extractTitle,
760
- generateSlug,
761
1021
  extractSections,
762
- getSectionFullText,
763
1022
  scorePlanQuality,
764
1023
  buildAiScoringPrompt,
765
1024
  savePlan,
766
1025
  loadPlan,
767
- getNextVersionNumber,
768
1026
  listPlansWithMeta,
769
- isGitCommitCommand,
770
- captureCommitDiff,
771
- ReviewConfigSchema
1027
+ ReviewConfigSchema,
1028
+ ReviewHub,
1029
+ discoverUlpiServer,
1030
+ registerWithServer,
1031
+ waitForServerDecision
772
1032
  };