@ulpi/cli 0.1.5 → 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.
- package/LICENSE +21 -0
- package/dist/{auth-PN7TMQHV-2W4ICG64.js → auth-FWM7MM4Q-VZC3U2XZ.js} +1 -1
- package/dist/{auth-BFFBUJUC.js → auth-HDK7ECJL.js} +2 -1
- package/dist/{chunk-RJIRWQJD.js → chunk-3BCW6ABU.js} +402 -142
- package/dist/{chunk-L3PWNHSA.js → chunk-3WB5CXH4.js} +180 -5
- package/dist/{chunk-K4OVPFY2.js → chunk-4UCJIAOU.js} +2 -2
- package/dist/chunk-4XTHZVDS.js +109 -0
- package/dist/chunk-4ZPOZULQ.js +6522 -0
- package/dist/{chunk-SIAQVRKG.js → chunk-5MI5GIXM.js} +48 -2
- package/dist/{chunk-KLEASXUR.js → chunk-6ZL6NXMV.js} +1 -1
- package/dist/{chunk-AV5RB3N2.js → chunk-76D3BYJD.js} +48 -0
- package/dist/{chunk-DOIKS6C5.js → chunk-AWOSRA5F.js} +1 -1
- package/dist/{chunk-UCMT5OKP.js → chunk-BFEKZZHM.js} +274 -57
- package/dist/chunk-C7CLUQI6.js +1286 -0
- package/dist/{chunk-ELTGWMDE.js → chunk-E3B5NROU.js} +7 -7
- package/dist/chunk-EJ7TW77N.js +1418 -0
- package/dist/{chunk-6OURRFP7.js → chunk-IV6MWETF.js} +383 -168
- package/dist/chunk-IZPJHSPX.js +1478 -0
- package/dist/chunk-JLHNLM3C.js +228 -0
- package/dist/{chunk-P2RESJRN.js → chunk-KYYI23AQ.js} +2 -2
- package/dist/chunk-S6ANCSYO.js +1271 -0
- package/dist/chunk-SEU7WWNQ.js +1251 -0
- package/dist/chunk-SNQ7NAIS.js +453 -0
- package/dist/{ulpi-RMMCUAGP-EWYUE7RU.js → chunk-TSLDGT5O.js} +73 -35
- package/dist/{chunk-EIWYSP3A.js → chunk-UXHCHOWQ.js} +83 -62
- package/dist/chunk-V2H5D6Y3.js +146 -0
- package/dist/{chunk-5SCG7UYM.js → chunk-VVEDXI7E.js} +1 -1
- package/dist/chunk-VXH5Y4FO.js +6761 -0
- package/dist/chunk-WED4LM5N.js +322 -0
- package/dist/{chunk-74WVVWJ4.js → chunk-YOKL7RB5.js} +184 -15
- package/dist/chunk-Z53CAR7G.js +298 -0
- package/dist/{ci-JQ56YIKC.js → ci-X3U2W4HC.js} +124 -26
- package/dist/cloud-2F3NLVHN.js +274 -0
- package/dist/{codemap-HMYBXJL2.js → codemap-XNGMAF3F.js} +37 -37
- package/dist/codex-MB5YTMRT.js +132 -0
- package/dist/{config-YYWEN7U2.js → config-OOELBYTH.js} +1 -1
- package/dist/dist-2BJYR5EI.js +59 -0
- package/dist/dist-3EIQTZHT.js +1380 -0
- package/dist/{dist-WAMAQVPK.js → dist-4U5L2X2C.js} +2 -2
- package/dist/{dist-4XTJ6HLM.js → dist-54KAMNLO.js} +16 -15
- package/dist/dist-6M4MZWZW.js +58 -0
- package/dist/dist-6X576SU2.js +27 -0
- package/dist/dist-7QOEYLFX.js +103 -0
- package/dist/dist-AYBGHEDY.js +2541 -0
- package/dist/dist-EK45QNEM.js +45 -0
- package/dist/{dist-U7ZIJMZD.js → dist-FKFEJRPX.js} +16 -15
- package/dist/dist-GTEJUBBT.js +66 -0
- package/dist/dist-HA74OKJZ.js +40 -0
- package/dist/{dist-XG2GG5SD.js → dist-HU5RZAON.js} +14 -2
- package/dist/dist-IYE3OBRB.js +374 -0
- package/dist/{dist-7WLLPWWB.js → dist-JLU26AB6.js} +12 -9
- package/dist/{dist-6G7JC2RA.js → dist-KUCI6JFE.js} +49 -9
- package/dist/dist-NUEMFZFL.js +33 -0
- package/dist/{dist-GWGTAHNM.js → dist-NUXMDXZ3.js} +31 -3
- package/dist/{dist-5R4RYNQO.js → dist-YCNWHSLN.js} +15 -5
- package/dist/{dist-6MFVWIFF.js → dist-YFFG2ZD6.js} +9 -16
- package/dist/dist-ZG4OKCSR.js +15 -0
- package/dist/doctor-SI4LLLDZ.js +345 -0
- package/dist/{export-import-4A5MWLIA.js → export-import-JFQH4KSJ.js} +1 -1
- package/dist/{history-RNUWO4JZ.js → history-5NE46ZAH.js} +7 -7
- package/dist/{hooks-installer-K2JXEBNN.js → hooks-installer-UN5JZLDQ.js} +2 -2
- package/dist/index.js +394 -618
- package/dist/{init-NQWFZPKO.js → init-5FK3VKRT.js} +76 -10
- package/dist/job-HIDMAFW2.js +376 -0
- package/dist/jobs.memory-PLMMSFHB-VBECCTHN.js +33 -0
- package/dist/kiro-VMUHDFGK.js +153 -0
- package/dist/{launchd-OYXUAVW6.js → launchd-6AWT54HR.js} +9 -17
- package/dist/mcp-PDUD7SGP.js +249 -0
- package/dist/mcp-installer-PQU3XOGO.js +259 -0
- package/dist/mcp-setup-OA7IB3H3.js +263 -0
- package/dist/{memory-D6ZFFCI2.js → memory-ZNAEAK3B.js} +17 -17
- package/dist/{ollama-3XCUZMZT-FYKHW4TZ.js → ollama-3XCUZMZT-4JMH6B7P.js} +1 -1
- package/dist/{openai-E7G2YAHU-IG33BFYF.js → openai-E7G2YAHU-T3HMBPH7.js} +2 -2
- package/dist/portal-JYWVHXDU.js +210 -0
- package/dist/prd-Q4J5NVAR.js +408 -0
- package/dist/repos-WWZXNN3P.js +271 -0
- package/dist/review-integration-5WHEJU2A.js +14 -0
- package/dist/{rules-3OFGWHP4.js → rules-Y4VSOY5Y.js} +3 -3
- package/dist/run-VPNXEIBY.js +687 -0
- package/dist/server-COL4AXKU-P7S7NNF6.js +11 -0
- package/dist/server-KKSETHDV-XSSLEENT.js +20 -0
- package/dist/{skills-GY2CTPWN.js → skills-QEYU2N27.js} +4 -2
- package/dist/start-JYOEL7AJ.js +303 -0
- package/dist/{status-SE43TIFJ.js → status-BHQYYGAL.js} +2 -2
- package/dist/{templates-O2XDKB5R.js → templates-CBRUJ66V.js} +6 -5
- package/dist/tui-DP7736EX.js +61 -0
- package/dist/ulpi-5EN6JCAS-LFE3WSL4.js +10 -0
- package/dist/{uninstall-KWGSGZTI.js → uninstall-ICUV6DDV.js} +3 -3
- package/dist/{update-QYZA4D23.js → update-7ZMAYRBH.js} +3 -3
- package/dist/{version-checker-MVB74DEX.js → version-checker-4ZFMZA7Y.js} +2 -2
- package/package.json +39 -31
- package/dist/chunk-26LLDX2T.js +0 -553
- package/dist/chunk-DDRLI6JU.js +0 -331
- package/dist/chunk-IFATANHR.js +0 -453
- package/dist/chunk-JWUUVXIV.js +0 -13694
- package/dist/chunk-LD52XG3X.js +0 -4273
- package/dist/chunk-MIAQVCFW.js +0 -39
- package/dist/chunk-YYZOFYS6.js +0 -415
- package/dist/dist-XD4YI27T.js +0 -26
- package/dist/mcp-installer-TOYDP77X.js +0 -124
- package/dist/projects-COUJP4ZC.js +0 -271
- package/dist/review-KMGP2S25.js +0 -152
- package/dist/server-USLHY6GH-F4JSXCWA.js +0 -18
- package/dist/server-X5P6WH2M-ULZF5WHZ.js +0 -11
- package/dist/skills/ulpi-generate-guardian/SKILL.md +0 -750
- package/dist/skills/ulpi-generate-guardian/references/framework-rules.md +0 -849
- package/dist/skills/ulpi-generate-guardian/references/language-rules.md +0 -591
- package/dist/ui-4SM2SUI6.js +0 -167
- 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.
|
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
} from "
|
|
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
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
626
|
-
|
|
899
|
+
var DISCOVERY_TIMEOUT_MS = 2e3;
|
|
900
|
+
var MAX_PORTS_TO_PROBE = 5;
|
|
901
|
+
function isProcessAlive(pid) {
|
|
627
902
|
try {
|
|
628
|
-
|
|
629
|
-
|
|
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
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
-
|
|
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
|
|
925
|
+
return null;
|
|
665
926
|
}
|
|
666
|
-
function
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
const
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
-
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
if (
|
|
736
|
-
|
|
737
|
-
|
|
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
|
-
|
|
740
|
-
|
|
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
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
|
|
1027
|
+
ReviewConfigSchema,
|
|
1028
|
+
ReviewHub,
|
|
1029
|
+
discoverUlpiServer,
|
|
1030
|
+
registerWithServer,
|
|
1031
|
+
waitForServerDecision
|
|
772
1032
|
};
|