@rogatio/cli 5.15.1 → 5.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/editor/index.js +162 -16
- package/dist/node/index.js +496 -54
- package/package.json +1 -1
package/dist/editor/index.js
CHANGED
|
@@ -231,7 +231,7 @@ function hasLoneSurrogate(value) {
|
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
// packages/editor/src/ai-assist-panel.ts
|
|
234
|
-
function createAIAssistPanel(_container, _editor, onApply, onClose) {
|
|
234
|
+
function createAIAssistPanel(_container, _editor, onApply, onClose, onSend) {
|
|
235
235
|
let visible = false;
|
|
236
236
|
let streamingContent = "";
|
|
237
237
|
let streamingMessageIndex = -1;
|
|
@@ -351,6 +351,7 @@ function createAIAssistPanel(_container, _editor, onApply, onClose) {
|
|
|
351
351
|
panel.appendChild(messagesContainer);
|
|
352
352
|
panel.appendChild(inputContainer);
|
|
353
353
|
const closeBtn = header.querySelector(".ai-close-btn");
|
|
354
|
+
let sending = false;
|
|
354
355
|
function show() {
|
|
355
356
|
if (!visible) {
|
|
356
357
|
visible = true;
|
|
@@ -373,10 +374,17 @@ function createAIAssistPanel(_container, _editor, onApply, onClose) {
|
|
|
373
374
|
onClose();
|
|
374
375
|
}
|
|
375
376
|
}
|
|
377
|
+
function destroy() {
|
|
378
|
+
visible = false;
|
|
379
|
+
if (panel.parentNode) {
|
|
380
|
+
panel.parentNode.removeChild(panel);
|
|
381
|
+
}
|
|
382
|
+
onClose();
|
|
383
|
+
}
|
|
376
384
|
function isVisible() {
|
|
377
385
|
return visible;
|
|
378
386
|
}
|
|
379
|
-
function
|
|
387
|
+
function renderMessage(message) {
|
|
380
388
|
const messageEl = document.createElement("div");
|
|
381
389
|
messageEl.style.cssText = `
|
|
382
390
|
display: flex;
|
|
@@ -402,6 +410,10 @@ function createAIAssistPanel(_container, _editor, onApply, onClose) {
|
|
|
402
410
|
messagesContainer.appendChild(messageEl);
|
|
403
411
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
404
412
|
}
|
|
413
|
+
function addMessage(message) {
|
|
414
|
+
messages.push(message);
|
|
415
|
+
renderMessage(message);
|
|
416
|
+
}
|
|
405
417
|
function createProposalElement(proposal) {
|
|
406
418
|
const container = document.createElement("div");
|
|
407
419
|
container.style.cssText = `
|
|
@@ -513,22 +525,22 @@ function createAIAssistPanel(_container, _editor, onApply, onClose) {
|
|
|
513
525
|
}
|
|
514
526
|
function updateStreamingContent(content) {
|
|
515
527
|
if (streamingMessageIndex >= 0 && streamingMessageIndex < messages.length) {
|
|
528
|
+
streamingContent += content;
|
|
516
529
|
const message = messages[streamingMessageIndex];
|
|
517
|
-
message.content = streamingContent
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
)
|
|
521
|
-
|
|
522
|
-
bubbles[streamingMessageIndex].textContent = message.content;
|
|
530
|
+
message.content = streamingContent;
|
|
531
|
+
const messageEl = messagesContainer.children[streamingMessageIndex];
|
|
532
|
+
const bubble = messageEl?.firstElementChild;
|
|
533
|
+
if (bubble instanceof HTMLElement) {
|
|
534
|
+
bubble.textContent = message.content;
|
|
523
535
|
}
|
|
536
|
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
524
537
|
}
|
|
525
538
|
}
|
|
526
539
|
function startStreaming(role) {
|
|
527
540
|
streamingContent = "";
|
|
528
541
|
const message = { role, content: "", isStreaming: true };
|
|
529
|
-
messages.push(message);
|
|
530
|
-
streamingMessageIndex = messages.length - 1;
|
|
531
542
|
addMessage(message);
|
|
543
|
+
streamingMessageIndex = messages.length - 1;
|
|
532
544
|
}
|
|
533
545
|
function finishStreaming(proposal) {
|
|
534
546
|
if (streamingMessageIndex >= 0 && streamingMessageIndex < messages.length) {
|
|
@@ -552,9 +564,16 @@ function createAIAssistPanel(_container, _editor, onApply, onClose) {
|
|
|
552
564
|
}
|
|
553
565
|
function handleSend() {
|
|
554
566
|
const prompt = textarea.value.trim();
|
|
555
|
-
if (!prompt) return;
|
|
567
|
+
if (!prompt || sending) return;
|
|
568
|
+
sending = true;
|
|
569
|
+
sendButton.disabled = true;
|
|
556
570
|
textarea.value = "";
|
|
557
571
|
textarea.style.height = "auto";
|
|
572
|
+
addMessage({ role: "user", content: prompt });
|
|
573
|
+
void Promise.resolve(onSend(prompt)).finally(() => {
|
|
574
|
+
sending = false;
|
|
575
|
+
sendButton.disabled = false;
|
|
576
|
+
});
|
|
558
577
|
}
|
|
559
578
|
closeBtn.addEventListener("click", hide);
|
|
560
579
|
sendButton.addEventListener("click", handleSend);
|
|
@@ -567,6 +586,7 @@ function createAIAssistPanel(_container, _editor, onApply, onClose) {
|
|
|
567
586
|
return {
|
|
568
587
|
show,
|
|
569
588
|
hide,
|
|
589
|
+
destroy,
|
|
570
590
|
isVisible,
|
|
571
591
|
addMessage,
|
|
572
592
|
clearMessages,
|
|
@@ -1966,6 +1986,69 @@ function displayName(value, fallback) {
|
|
|
1966
1986
|
function isHTMLElement(value) {
|
|
1967
1987
|
return value !== null && typeof value === "object" && value.nodeType === 1 && typeof value.appendChild === "function";
|
|
1968
1988
|
}
|
|
1989
|
+
function isAsyncIterable(value) {
|
|
1990
|
+
return value !== null && typeof value === "object" && Symbol.asyncIterator in value && typeof value[Symbol.asyncIterator] === "function";
|
|
1991
|
+
}
|
|
1992
|
+
function aiAssistErrorMessage(error) {
|
|
1993
|
+
if (error instanceof Error && error.message.length > 0) return error.message;
|
|
1994
|
+
if (typeof error === "string" && error.length > 0) return error;
|
|
1995
|
+
return "AI Assist failed.";
|
|
1996
|
+
}
|
|
1997
|
+
function ownString(record, ...keys) {
|
|
1998
|
+
for (const key of keys) {
|
|
1999
|
+
if (!Object.hasOwn(record, key)) continue;
|
|
2000
|
+
if (FORBIDDEN_EXTENSION_FIELDS.has(key)) continue;
|
|
2001
|
+
const value = record[key];
|
|
2002
|
+
if (typeof value === "string") return value;
|
|
2003
|
+
}
|
|
2004
|
+
return void 0;
|
|
2005
|
+
}
|
|
2006
|
+
function applyRuleProposalAction(rule, ruleProposal) {
|
|
2007
|
+
const action = ruleProposal.action;
|
|
2008
|
+
switch (ruleProposal.kind) {
|
|
2009
|
+
case "redirect": {
|
|
2010
|
+
rule.type = "redirect";
|
|
2011
|
+
const snap = snapshotOwnData(action);
|
|
2012
|
+
if (snap.valid) rule.redirect = snap.value;
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
case "query": {
|
|
2016
|
+
rule.type = "query";
|
|
2017
|
+
const snap = snapshotOwnData(action);
|
|
2018
|
+
if (snap.valid) rule.action = snap.value;
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
case "header": {
|
|
2022
|
+
rule.type = "header";
|
|
2023
|
+
if (!isRecord4(action)) return;
|
|
2024
|
+
const direction = ownString(action, "headerDirection", "direction");
|
|
2025
|
+
const operation = ownString(action, "headerOperation", "operation");
|
|
2026
|
+
const name = ownString(action, "headerName", "name");
|
|
2027
|
+
const value = ownString(action, "headerValue", "value");
|
|
2028
|
+
if (direction !== void 0) rule.headerDirection = direction;
|
|
2029
|
+
if (operation !== void 0) rule.headerOperation = operation;
|
|
2030
|
+
if (name !== void 0) rule.headerName = name;
|
|
2031
|
+
if (value !== void 0) rule.headerValue = value;
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
case "response-body": {
|
|
2035
|
+
rule.type = "response-body";
|
|
2036
|
+
const snap = snapshotOwnData(action);
|
|
2037
|
+
if (snap.valid) rule.responseBody = snap.value;
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
case "request-body": {
|
|
2041
|
+
rule.type = "request-body";
|
|
2042
|
+
const snap = snapshotOwnData(action);
|
|
2043
|
+
if (snap.valid) rule.requestBody = snap.value;
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
default: {
|
|
2047
|
+
const _exhaustive = ruleProposal.kind;
|
|
2048
|
+
void _exhaustive;
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
1969
2052
|
function normalizeExtensions(value) {
|
|
1970
2053
|
const ids = /* @__PURE__ */ new Set();
|
|
1971
2054
|
const merged = [...builtInRuleTypes];
|
|
@@ -2042,6 +2125,7 @@ var EditorControllerImpl = class {
|
|
|
2042
2125
|
testRunning = false;
|
|
2043
2126
|
testRequestId = 0;
|
|
2044
2127
|
aiAssistPanel = null;
|
|
2128
|
+
aiAssistInFlight = false;
|
|
2045
2129
|
constructor(options, initial, extensions) {
|
|
2046
2130
|
this.root = options.root;
|
|
2047
2131
|
this.document = options.root.ownerDocument;
|
|
@@ -2064,8 +2148,8 @@ var EditorControllerImpl = class {
|
|
|
2064
2148
|
},
|
|
2065
2149
|
(proposal) => this.applyAIProposal(proposal),
|
|
2066
2150
|
() => {
|
|
2067
|
-
}
|
|
2068
|
-
|
|
2151
|
+
},
|
|
2152
|
+
(prompt) => this.runAIAssist(prompt)
|
|
2069
2153
|
);
|
|
2070
2154
|
}
|
|
2071
2155
|
this.host = this.document.createElement("div");
|
|
@@ -2142,7 +2226,7 @@ var EditorControllerImpl = class {
|
|
|
2142
2226
|
if (this.destroyed) return;
|
|
2143
2227
|
this.destroyed = true;
|
|
2144
2228
|
if (this.aiAssistPanel) {
|
|
2145
|
-
this.aiAssistPanel.
|
|
2229
|
+
this.aiAssistPanel.destroy();
|
|
2146
2230
|
this.aiAssistPanel = null;
|
|
2147
2231
|
}
|
|
2148
2232
|
this.cleanupExtensions();
|
|
@@ -2916,8 +3000,7 @@ var EditorControllerImpl = class {
|
|
|
2916
3000
|
if (ruleProposal.method) {
|
|
2917
3001
|
rule.method = ruleProposal.method;
|
|
2918
3002
|
}
|
|
2919
|
-
|
|
2920
|
-
rule[actionField] = ruleProposal.action;
|
|
3003
|
+
applyRuleProposalAction(rule, ruleProposal);
|
|
2921
3004
|
group.rules.push(rule);
|
|
2922
3005
|
this.markChanged();
|
|
2923
3006
|
const groupIdStr = String(group.id);
|
|
@@ -2932,6 +3015,69 @@ var EditorControllerImpl = class {
|
|
|
2932
3015
|
this.statusMessage = `Applied ${proposal.rules.length} rule${proposal.rules.length === 1 ? "" : "s"} from AI.`;
|
|
2933
3016
|
this.render();
|
|
2934
3017
|
}
|
|
3018
|
+
async runAIAssist(prompt) {
|
|
3019
|
+
const handler = this.options.aiAssist;
|
|
3020
|
+
const panel = this.aiAssistPanel;
|
|
3021
|
+
if (!handler || !panel || this.aiAssistInFlight || this.destroyed) return;
|
|
3022
|
+
this.aiAssistInFlight = true;
|
|
3023
|
+
const diagnostics = this.validateCurrent();
|
|
3024
|
+
const hasErrors = diagnostics.some(
|
|
3025
|
+
(diagnostic2) => diagnostic2.severity === "error"
|
|
3026
|
+
);
|
|
3027
|
+
const request = {
|
|
3028
|
+
kind: hasErrors ? "fix" : "generate",
|
|
3029
|
+
prompt,
|
|
3030
|
+
context: {
|
|
3031
|
+
project: this.getDraft(),
|
|
3032
|
+
activeGroupId: this.currentGroupId(),
|
|
3033
|
+
diagnostics: diagnostics.length > 0 ? diagnostics : void 0
|
|
3034
|
+
}
|
|
3035
|
+
};
|
|
3036
|
+
panel.startStreaming("assistant");
|
|
3037
|
+
try {
|
|
3038
|
+
const result = handler(request);
|
|
3039
|
+
if (isAsyncIterable(result)) {
|
|
3040
|
+
let proposal;
|
|
3041
|
+
for await (const chunk of result) {
|
|
3042
|
+
if (this.destroyed) return;
|
|
3043
|
+
const handled = this.consumeAIAssistChunk(panel, chunk);
|
|
3044
|
+
if (handled === "error") return;
|
|
3045
|
+
if (handled.proposal) proposal = handled.proposal;
|
|
3046
|
+
}
|
|
3047
|
+
panel.finishStreaming(proposal);
|
|
3048
|
+
return;
|
|
3049
|
+
}
|
|
3050
|
+
const response = await result;
|
|
3051
|
+
if (this.destroyed) return;
|
|
3052
|
+
panel.finishStreaming(response.proposal);
|
|
3053
|
+
} catch (error) {
|
|
3054
|
+
if (this.destroyed) return;
|
|
3055
|
+
panel.finishStreaming();
|
|
3056
|
+
panel.addMessage({
|
|
3057
|
+
role: "system",
|
|
3058
|
+
content: aiAssistErrorMessage(error)
|
|
3059
|
+
});
|
|
3060
|
+
} finally {
|
|
3061
|
+
this.aiAssistInFlight = false;
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
consumeAIAssistChunk(panel, chunk) {
|
|
3065
|
+
if (chunk.type === "token") {
|
|
3066
|
+
if (typeof chunk.content === "string" && chunk.content.length > 0) {
|
|
3067
|
+
panel.updateStreamingContent(chunk.content);
|
|
3068
|
+
}
|
|
3069
|
+
return {};
|
|
3070
|
+
}
|
|
3071
|
+
if (chunk.type === "error") {
|
|
3072
|
+
panel.finishStreaming();
|
|
3073
|
+
panel.addMessage({
|
|
3074
|
+
role: "system",
|
|
3075
|
+
content: chunk.error?.message ?? "AI Assist failed."
|
|
3076
|
+
});
|
|
3077
|
+
return "error";
|
|
3078
|
+
}
|
|
3079
|
+
return { proposal: chunk.proposal };
|
|
3080
|
+
}
|
|
2935
3081
|
addGroupOrigin(groupId) {
|
|
2936
3082
|
const group = this.groupById(groupId);
|
|
2937
3083
|
if (!group || this.saving) return;
|
package/dist/node/index.js
CHANGED
|
@@ -20608,6 +20608,404 @@ function createAIClient(config) {
|
|
|
20608
20608
|
}
|
|
20609
20609
|
};
|
|
20610
20610
|
}
|
|
20611
|
+
var RESOURCE_TYPES2 = [
|
|
20612
|
+
"main_frame",
|
|
20613
|
+
"sub_frame",
|
|
20614
|
+
"stylesheet",
|
|
20615
|
+
"script",
|
|
20616
|
+
"image",
|
|
20617
|
+
"font",
|
|
20618
|
+
"object",
|
|
20619
|
+
"xmlhttprequest",
|
|
20620
|
+
"fetch",
|
|
20621
|
+
"media",
|
|
20622
|
+
"websocket",
|
|
20623
|
+
"manifest",
|
|
20624
|
+
"other"
|
|
20625
|
+
];
|
|
20626
|
+
var HTTP_METHODS2 = [
|
|
20627
|
+
"GET",
|
|
20628
|
+
"POST",
|
|
20629
|
+
"PUT",
|
|
20630
|
+
"PATCH",
|
|
20631
|
+
"DELETE",
|
|
20632
|
+
"HEAD",
|
|
20633
|
+
"OPTIONS",
|
|
20634
|
+
"CONNECT",
|
|
20635
|
+
"TRACE"
|
|
20636
|
+
];
|
|
20637
|
+
var FORBIDDEN_REQUEST_HEADERS2 = [
|
|
20638
|
+
"accept-charset",
|
|
20639
|
+
"accept-encoding",
|
|
20640
|
+
"access-control-request-headers",
|
|
20641
|
+
"access-control-request-method",
|
|
20642
|
+
"connection",
|
|
20643
|
+
"content-length",
|
|
20644
|
+
"cookie",
|
|
20645
|
+
"cookie2",
|
|
20646
|
+
"date",
|
|
20647
|
+
"dnt",
|
|
20648
|
+
"expect",
|
|
20649
|
+
"host",
|
|
20650
|
+
"keep-alive",
|
|
20651
|
+
"origin",
|
|
20652
|
+
"referer",
|
|
20653
|
+
"te",
|
|
20654
|
+
"trailer",
|
|
20655
|
+
"transfer-encoding",
|
|
20656
|
+
"upgrade",
|
|
20657
|
+
"via"
|
|
20658
|
+
];
|
|
20659
|
+
var FORBIDDEN_RESPONSE_HEADERS2 = ["set-cookie", "set-cookie2"];
|
|
20660
|
+
function formatResourceTypes() {
|
|
20661
|
+
return RESOURCE_TYPES2.join(", ");
|
|
20662
|
+
}
|
|
20663
|
+
function formatHttpMethods() {
|
|
20664
|
+
return HTTP_METHODS2.join(", ");
|
|
20665
|
+
}
|
|
20666
|
+
function formatForbiddenHeaders() {
|
|
20667
|
+
return [
|
|
20668
|
+
...FORBIDDEN_REQUEST_HEADERS2.map((h) => `request: ${h}`),
|
|
20669
|
+
...FORBIDDEN_RESPONSE_HEADERS2.map((h) => `response: ${h}`)
|
|
20670
|
+
].join(", ");
|
|
20671
|
+
}
|
|
20672
|
+
function formatProjectContext(project) {
|
|
20673
|
+
try {
|
|
20674
|
+
return JSON.stringify(project, null, 2);
|
|
20675
|
+
} catch {
|
|
20676
|
+
return "{}";
|
|
20677
|
+
}
|
|
20678
|
+
}
|
|
20679
|
+
function buildSystemPrompt(project) {
|
|
20680
|
+
const projectJson = formatProjectContext(project);
|
|
20681
|
+
return `You are an expert Rogatio rule author. Rogatio is a local-first browser request/response rule engine.
|
|
20682
|
+
|
|
20683
|
+
## Schema Constraants
|
|
20684
|
+
|
|
20685
|
+
### URL Regular Expression
|
|
20686
|
+
- Maximum 2048 characters
|
|
20687
|
+
- Case-sensitive, no flags
|
|
20688
|
+
- Must be valid ECMAScript regex
|
|
20689
|
+
- Used to match request URLs
|
|
20690
|
+
|
|
20691
|
+
### Origins
|
|
20692
|
+
- Only explicit http:// and https:// origins with hostname
|
|
20693
|
+
- Optional port (e.g., https://example.com:8080)
|
|
20694
|
+
- No wildcards, credentials, paths, query strings, or fragments
|
|
20695
|
+
- Effective origins are union of project, group, and rule origins
|
|
20696
|
+
|
|
20697
|
+
### Forbidden Headers
|
|
20698
|
+
Cannot set/remove these headers:
|
|
20699
|
+
${formatForbiddenHeaders()}
|
|
20700
|
+
|
|
20701
|
+
### Resource Types
|
|
20702
|
+
${formatResourceTypes()}
|
|
20703
|
+
|
|
20704
|
+
### HTTP Methods
|
|
20705
|
+
${formatHttpMethods()}
|
|
20706
|
+
|
|
20707
|
+
### Bounds
|
|
20708
|
+
- Max 64 projects per profile
|
|
20709
|
+
- Max 100 groups per project
|
|
20710
|
+
- Max 500 rules per project
|
|
20711
|
+
- Max 10 origins per group/rule
|
|
20712
|
+
|
|
20713
|
+
## Rule Types
|
|
20714
|
+
|
|
20715
|
+
Each rule has: id, name, urlRegex, origins[], resourceTypes[], priority, method?, action
|
|
20716
|
+
|
|
20717
|
+
### redirect
|
|
20718
|
+
Action: destination string. Use $1 through $9 for URL regex captures. $$ is a literal dollar sign. Existing redirect \\1 references remain compatible.
|
|
20719
|
+
|
|
20720
|
+
### query
|
|
20721
|
+
Action: query parameters with name, operation, and value. Set values may use URL captures.
|
|
20722
|
+
|
|
20723
|
+
### header
|
|
20724
|
+
Action: header direction, operation, name, and value. Set and append values may use URL captures.
|
|
20725
|
+
|
|
20726
|
+
### response-body
|
|
20727
|
+
Replace-mode body may use URL captures. Regex-mode replacement strings use captures from the body pattern, not the URL pattern.
|
|
20728
|
+
|
|
20729
|
+
### request-body
|
|
20730
|
+
Replace-mode body may use URL captures. Regex-mode replacement strings use captures from the body pattern, not the URL pattern.
|
|
20731
|
+
|
|
20732
|
+
## Compiler Diagnostic Codes (stable)
|
|
20733
|
+
- schema.invalid-regex - Invalid regex syntax
|
|
20734
|
+
- schema.duplicate-id - Duplicate group/rule ID
|
|
20735
|
+
- schema.invalid-origin - Invalid origin format
|
|
20736
|
+
- schema.invalid-resource-type - Unknown resource type
|
|
20737
|
+
- schema.invalid-method - Unknown HTTP method
|
|
20738
|
+
- schema.regex-too-long - Regex exceeds 2048 chars
|
|
20739
|
+
- compiler.unsupported-operation - Operation kind not supported
|
|
20740
|
+
|
|
20741
|
+
## Current Project
|
|
20742
|
+
${projectJson}
|
|
20743
|
+
|
|
20744
|
+
## Output Format
|
|
20745
|
+
|
|
20746
|
+
Return ONLY valid JSON matching this TypeScript interface:
|
|
20747
|
+
|
|
20748
|
+
\`\`\`typescript
|
|
20749
|
+
interface RuleProposal {
|
|
20750
|
+
kind: "redirect" | "query" | "header" | "response-body" | "request-body";
|
|
20751
|
+
groupId: string;
|
|
20752
|
+
name: string;
|
|
20753
|
+
urlRegex: string;
|
|
20754
|
+
origins?: string[];
|
|
20755
|
+
resourceTypes?: string[];
|
|
20756
|
+
priority?: number;
|
|
20757
|
+
method?: string;
|
|
20758
|
+
action: unknown;
|
|
20759
|
+
}
|
|
20760
|
+
|
|
20761
|
+
interface AIProposal {
|
|
20762
|
+
rules: RuleProposal[];
|
|
20763
|
+
explanation: string;
|
|
20764
|
+
}
|
|
20765
|
+
\`\`\`
|
|
20766
|
+
|
|
20767
|
+
## Instructions
|
|
20768
|
+
|
|
20769
|
+
1. Propose MINIMAL changes - only what the user asked for
|
|
20770
|
+
2. Use existing groups when possible; create new only if needed
|
|
20771
|
+
3. All regex must be valid ECMAScript
|
|
20772
|
+
4. All origins must be valid http/https with hostname
|
|
20773
|
+
5. Resource types must be from the allowed list
|
|
20774
|
+
6. Methods must be from the allowed list
|
|
20775
|
+
7. Priority: higher = more specific (100 default)
|
|
20776
|
+
8. Action object must match the rule kind schema exactly
|
|
20777
|
+
9. Use $1\u2013$9 only when urlRegex defines those capture groups; use $$ for a literal dollar sign
|
|
20778
|
+
10. Before proposing, mentally validate against dry-run: would this rule match the intended URLs?
|
|
20779
|
+
11. Explanation should be 1-2 sentences describing what the rules do
|
|
20780
|
+
`;
|
|
20781
|
+
}
|
|
20782
|
+
var MAX_FIX_ATTEMPTS = 3;
|
|
20783
|
+
function isRecord(value) {
|
|
20784
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20785
|
+
}
|
|
20786
|
+
function ownString(record, ...keys) {
|
|
20787
|
+
for (const key of keys) {
|
|
20788
|
+
if (!Object.hasOwn(record, key)) continue;
|
|
20789
|
+
const value = record[key];
|
|
20790
|
+
if (typeof value === "string") return value;
|
|
20791
|
+
}
|
|
20792
|
+
return void 0;
|
|
20793
|
+
}
|
|
20794
|
+
function ruleFromProposal(proposal, ruleId) {
|
|
20795
|
+
const rule = {
|
|
20796
|
+
id: ruleId,
|
|
20797
|
+
name: proposal.name,
|
|
20798
|
+
urlRegex: proposal.urlRegex,
|
|
20799
|
+
origins: proposal.origins ? [...proposal.origins] : [],
|
|
20800
|
+
resourceTypes: proposal.resourceTypes ? [...proposal.resourceTypes] : ["main_frame"],
|
|
20801
|
+
priority: proposal.priority ?? 100
|
|
20802
|
+
};
|
|
20803
|
+
if (proposal.method !== void 0) {
|
|
20804
|
+
rule.method = proposal.method;
|
|
20805
|
+
}
|
|
20806
|
+
switch (proposal.kind) {
|
|
20807
|
+
case "redirect":
|
|
20808
|
+
rule.type = "redirect";
|
|
20809
|
+
rule.redirect = proposal.action;
|
|
20810
|
+
break;
|
|
20811
|
+
case "query":
|
|
20812
|
+
rule.type = "query";
|
|
20813
|
+
rule.action = proposal.action;
|
|
20814
|
+
break;
|
|
20815
|
+
case "header": {
|
|
20816
|
+
rule.type = "header";
|
|
20817
|
+
if (isRecord(proposal.action)) {
|
|
20818
|
+
const direction = ownString(
|
|
20819
|
+
proposal.action,
|
|
20820
|
+
"headerDirection",
|
|
20821
|
+
"direction"
|
|
20822
|
+
);
|
|
20823
|
+
const operation = ownString(
|
|
20824
|
+
proposal.action,
|
|
20825
|
+
"headerOperation",
|
|
20826
|
+
"operation"
|
|
20827
|
+
);
|
|
20828
|
+
const name = ownString(proposal.action, "headerName", "name");
|
|
20829
|
+
const value = ownString(proposal.action, "headerValue", "value");
|
|
20830
|
+
if (direction !== void 0) rule.headerDirection = direction;
|
|
20831
|
+
if (operation !== void 0) rule.headerOperation = operation;
|
|
20832
|
+
if (name !== void 0) rule.headerName = name;
|
|
20833
|
+
if (value !== void 0) rule.headerValue = value;
|
|
20834
|
+
}
|
|
20835
|
+
break;
|
|
20836
|
+
}
|
|
20837
|
+
case "response-body":
|
|
20838
|
+
rule.type = "response-body";
|
|
20839
|
+
rule.responseBody = proposal.action;
|
|
20840
|
+
break;
|
|
20841
|
+
case "request-body":
|
|
20842
|
+
rule.type = "request-body";
|
|
20843
|
+
rule.requestBody = proposal.action;
|
|
20844
|
+
break;
|
|
20845
|
+
default: {
|
|
20846
|
+
const _exhaustive = proposal.kind;
|
|
20847
|
+
void _exhaustive;
|
|
20848
|
+
}
|
|
20849
|
+
}
|
|
20850
|
+
return rule;
|
|
20851
|
+
}
|
|
20852
|
+
function mergeProposalIntoProject(project, proposal) {
|
|
20853
|
+
const groups = Array.isArray(project.groups) ? project.groups.map((group) => {
|
|
20854
|
+
if (!isRecord(group)) {
|
|
20855
|
+
return { id: "invalid", name: "invalid", origins: [], rules: [] };
|
|
20856
|
+
}
|
|
20857
|
+
return {
|
|
20858
|
+
...group,
|
|
20859
|
+
rules: Array.isArray(group.rules) ? [...group.rules] : []
|
|
20860
|
+
};
|
|
20861
|
+
}) : [];
|
|
20862
|
+
let seq = 0;
|
|
20863
|
+
for (const ruleProposal of proposal.rules) {
|
|
20864
|
+
let group = groups.find(
|
|
20865
|
+
(candidate) => candidate.id === ruleProposal.groupId
|
|
20866
|
+
);
|
|
20867
|
+
if (!group) {
|
|
20868
|
+
group = {
|
|
20869
|
+
id: ruleProposal.groupId,
|
|
20870
|
+
name: "AI Group",
|
|
20871
|
+
origins: [],
|
|
20872
|
+
rules: []
|
|
20873
|
+
};
|
|
20874
|
+
groups.push(group);
|
|
20875
|
+
}
|
|
20876
|
+
const rules = group.rules;
|
|
20877
|
+
rules.push(ruleFromProposal(ruleProposal, `ai-${Date.now()}-${seq++}`));
|
|
20878
|
+
}
|
|
20879
|
+
return { ...project, groups };
|
|
20880
|
+
}
|
|
20881
|
+
function parseProposal(content) {
|
|
20882
|
+
try {
|
|
20883
|
+
const parsed = JSON.parse(content);
|
|
20884
|
+
if (parsed.rules && parsed.explanation) {
|
|
20885
|
+
return parsed;
|
|
20886
|
+
}
|
|
20887
|
+
} catch {
|
|
20888
|
+
}
|
|
20889
|
+
return null;
|
|
20890
|
+
}
|
|
20891
|
+
function buildMessages(systemPrompt, request, previousProposal, validationErrors) {
|
|
20892
|
+
const messages = [{ role: "system", content: systemPrompt }];
|
|
20893
|
+
if (previousProposal) {
|
|
20894
|
+
messages.push({
|
|
20895
|
+
role: "assistant",
|
|
20896
|
+
content: JSON.stringify(previousProposal)
|
|
20897
|
+
});
|
|
20898
|
+
}
|
|
20899
|
+
let userContent = "";
|
|
20900
|
+
if (request.kind === "fix" && validationErrors) {
|
|
20901
|
+
userContent = `The previous proposal failed validation. Fix the following errors:
|
|
20902
|
+
|
|
20903
|
+
`;
|
|
20904
|
+
for (const err of validationErrors) {
|
|
20905
|
+
userContent += `- ${err.path}: ${err.message} (${err.code})
|
|
20906
|
+
`;
|
|
20907
|
+
}
|
|
20908
|
+
userContent += `
|
|
20909
|
+
Original request: ${request.prompt}`;
|
|
20910
|
+
} else if (request.kind === "explain") {
|
|
20911
|
+
userContent = `Explain this rule/project: ${request.prompt}`;
|
|
20912
|
+
} else {
|
|
20913
|
+
userContent = request.prompt;
|
|
20914
|
+
}
|
|
20915
|
+
if (request.context.dryRunCases && request.context.dryRunCases.length > 0) {
|
|
20916
|
+
userContent += `
|
|
20917
|
+
|
|
20918
|
+
Dry-run test cases:`;
|
|
20919
|
+
for (const tc of request.context.dryRunCases) {
|
|
20920
|
+
userContent += `
|
|
20921
|
+
- ${tc.url}${tc.method ? ` (${tc.method})` : ""}${tc.resourceType ? ` [${tc.resourceType}]` : ""}`;
|
|
20922
|
+
}
|
|
20923
|
+
}
|
|
20924
|
+
messages.push({ role: "user", content: userContent });
|
|
20925
|
+
return messages;
|
|
20926
|
+
}
|
|
20927
|
+
async function runComplete(client, options) {
|
|
20928
|
+
const result = await client.complete(options);
|
|
20929
|
+
return parseProposal(result.content);
|
|
20930
|
+
}
|
|
20931
|
+
async function runStream(client, options) {
|
|
20932
|
+
let fullContent = "";
|
|
20933
|
+
for await (const chunk of client.stream(options)) {
|
|
20934
|
+
if (chunk.delta) {
|
|
20935
|
+
fullContent += chunk.delta;
|
|
20936
|
+
}
|
|
20937
|
+
if (chunk.done) {
|
|
20938
|
+
return parseProposal(fullContent);
|
|
20939
|
+
}
|
|
20940
|
+
}
|
|
20941
|
+
return parseProposal(fullContent);
|
|
20942
|
+
}
|
|
20943
|
+
async function runAIAssist(request, config, validate, dryRun, client) {
|
|
20944
|
+
const aiClient = client ?? createAIClient(config);
|
|
20945
|
+
const systemPrompt = buildSystemPrompt(request.context.project);
|
|
20946
|
+
let currentProposal = null;
|
|
20947
|
+
let attempts = 0;
|
|
20948
|
+
while (attempts < MAX_FIX_ATTEMPTS) {
|
|
20949
|
+
attempts++;
|
|
20950
|
+
const isFixAttempt = attempts > 1 || request.kind === "fix";
|
|
20951
|
+
const validationErrors = (() => {
|
|
20952
|
+
if (!isFixAttempt || !currentProposal) return [];
|
|
20953
|
+
return validate(
|
|
20954
|
+
mergeProposalIntoProject(request.context.project, currentProposal)
|
|
20955
|
+
);
|
|
20956
|
+
})();
|
|
20957
|
+
const messages = buildMessages(
|
|
20958
|
+
systemPrompt,
|
|
20959
|
+
request,
|
|
20960
|
+
currentProposal ?? void 0,
|
|
20961
|
+
validationErrors
|
|
20962
|
+
);
|
|
20963
|
+
const completionOptions = {
|
|
20964
|
+
messages,
|
|
20965
|
+
model: config.model,
|
|
20966
|
+
temperature: 0.2,
|
|
20967
|
+
responseFormat: { type: "json_object" }
|
|
20968
|
+
};
|
|
20969
|
+
let proposal = null;
|
|
20970
|
+
try {
|
|
20971
|
+
proposal = await runStream(aiClient, {
|
|
20972
|
+
...completionOptions,
|
|
20973
|
+
stream: true
|
|
20974
|
+
});
|
|
20975
|
+
} catch {
|
|
20976
|
+
try {
|
|
20977
|
+
proposal = await runComplete(aiClient, completionOptions);
|
|
20978
|
+
} catch (e) {
|
|
20979
|
+
throw new Error(`AI request failed: ${e}`);
|
|
20980
|
+
}
|
|
20981
|
+
}
|
|
20982
|
+
if (!proposal) {
|
|
20983
|
+
throw new Error("AI returned invalid proposal format");
|
|
20984
|
+
}
|
|
20985
|
+
currentProposal = proposal;
|
|
20986
|
+
const testProject = mergeProposalIntoProject(
|
|
20987
|
+
request.context.project,
|
|
20988
|
+
proposal
|
|
20989
|
+
);
|
|
20990
|
+
const diagnostics = validate(testProject);
|
|
20991
|
+
if (diagnostics.length === 0) {
|
|
20992
|
+
if (dryRun && request.context.dryRunCases && request.context.dryRunCases.length > 0) {
|
|
20993
|
+
dryRun(testProject, request.context.dryRunCases);
|
|
20994
|
+
}
|
|
20995
|
+
return proposal;
|
|
20996
|
+
}
|
|
20997
|
+
if (request.kind === "fix") {
|
|
20998
|
+
continue;
|
|
20999
|
+
}
|
|
21000
|
+
if (attempts < MAX_FIX_ATTEMPTS) {
|
|
21001
|
+
continue;
|
|
21002
|
+
}
|
|
21003
|
+
throw new Error(
|
|
21004
|
+
`Max fix attempts (${MAX_FIX_ATTEMPTS}) reached. Remaining errors: ${diagnostics.map((d) => d.message).join(", ")}`
|
|
21005
|
+
);
|
|
21006
|
+
}
|
|
21007
|
+
throw new Error(`Max fix attempts (${MAX_FIX_ATTEMPTS}) reached`);
|
|
21008
|
+
}
|
|
20611
21009
|
function getConfigDir() {
|
|
20612
21010
|
const platform = process.platform;
|
|
20613
21011
|
if (platform === "win32") {
|
|
@@ -23780,7 +24178,7 @@ function toMatcherOperations(operations) {
|
|
|
23780
24178
|
})
|
|
23781
24179
|
);
|
|
23782
24180
|
}
|
|
23783
|
-
function
|
|
24181
|
+
function isRecord2(value) {
|
|
23784
24182
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23785
24183
|
}
|
|
23786
24184
|
function parseUrl(req) {
|
|
@@ -23802,6 +24200,25 @@ function parseDiagnostics(result) {
|
|
|
23802
24200
|
}
|
|
23803
24201
|
return [];
|
|
23804
24202
|
}
|
|
24203
|
+
function schemaDiagnosticsForAssist(value) {
|
|
24204
|
+
const result = validateProjectDetailed(value);
|
|
24205
|
+
if (result.valid) return [];
|
|
24206
|
+
return result.errors.map((error) => ({
|
|
24207
|
+
code: `schema.${error.keyword}`,
|
|
24208
|
+
severity: "error",
|
|
24209
|
+
path: error.instancePath || "/",
|
|
24210
|
+
message: error.message ?? "The project contains invalid data."
|
|
24211
|
+
}));
|
|
24212
|
+
}
|
|
24213
|
+
function isAIAssistRequestBody(body) {
|
|
24214
|
+
if (!isRecord2(body)) return false;
|
|
24215
|
+
if (body.kind !== "generate" && body.kind !== "fix" && body.kind !== "explain") {
|
|
24216
|
+
return false;
|
|
24217
|
+
}
|
|
24218
|
+
if (typeof body.prompt !== "string") return false;
|
|
24219
|
+
if (!isRecord2(body.context) || !isRecord2(body.context.project)) return false;
|
|
24220
|
+
return true;
|
|
24221
|
+
}
|
|
23805
24222
|
function parseCompilerDiagnostics(result) {
|
|
23806
24223
|
if (!result.ok) {
|
|
23807
24224
|
return result.diagnostics.map((d) => ({
|
|
@@ -24029,7 +24446,7 @@ function createRoutes(context) {
|
|
|
24029
24446
|
res.end(JSON.stringify(failure2.body));
|
|
24030
24447
|
return;
|
|
24031
24448
|
}
|
|
24032
|
-
if (!
|
|
24449
|
+
if (!isRecord2(body) || !isRecord2(body.project)) {
|
|
24033
24450
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
24034
24451
|
res.end(
|
|
24035
24452
|
JSON.stringify({
|
|
@@ -24113,7 +24530,7 @@ function createRoutes(context) {
|
|
|
24113
24530
|
res.end(JSON.stringify(failure2.body));
|
|
24114
24531
|
return;
|
|
24115
24532
|
}
|
|
24116
|
-
if (!
|
|
24533
|
+
if (!isRecord2(body) || !Array.isArray(body.messages)) {
|
|
24117
24534
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
24118
24535
|
res.end(
|
|
24119
24536
|
JSON.stringify({
|
|
@@ -24176,7 +24593,7 @@ function createRoutes(context) {
|
|
|
24176
24593
|
res.end(JSON.stringify(failure2.body));
|
|
24177
24594
|
return;
|
|
24178
24595
|
}
|
|
24179
|
-
if (!
|
|
24596
|
+
if (!isRecord2(body) || !Array.isArray(body.messages)) {
|
|
24180
24597
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
24181
24598
|
res.end(
|
|
24182
24599
|
JSON.stringify({
|
|
@@ -24220,6 +24637,68 @@ function createRoutes(context) {
|
|
|
24220
24637
|
}
|
|
24221
24638
|
return;
|
|
24222
24639
|
}
|
|
24640
|
+
if (pathname === "/api/ai/assist" && method === "POST") {
|
|
24641
|
+
if (!validateCsrf(req, context.csrfToken)) {
|
|
24642
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
24643
|
+
res.end(
|
|
24644
|
+
JSON.stringify({
|
|
24645
|
+
code: "csrf-invalid",
|
|
24646
|
+
message: "Invalid CSRF token"
|
|
24647
|
+
})
|
|
24648
|
+
);
|
|
24649
|
+
return;
|
|
24650
|
+
}
|
|
24651
|
+
if (!context.aiClient || !context.aiProviderConfig) {
|
|
24652
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
24653
|
+
res.end(
|
|
24654
|
+
JSON.stringify({
|
|
24655
|
+
code: "ai-not-configured",
|
|
24656
|
+
message: "AI provider not configured"
|
|
24657
|
+
})
|
|
24658
|
+
);
|
|
24659
|
+
return;
|
|
24660
|
+
}
|
|
24661
|
+
let body;
|
|
24662
|
+
try {
|
|
24663
|
+
const bodyText = await getRequestBody(req);
|
|
24664
|
+
body = JSON.parse(bodyText);
|
|
24665
|
+
} catch (error) {
|
|
24666
|
+
const failure2 = bodyErrorResponse(error);
|
|
24667
|
+
res.writeHead(failure2.status, { "Content-Type": "application/json" });
|
|
24668
|
+
res.end(JSON.stringify(failure2.body));
|
|
24669
|
+
return;
|
|
24670
|
+
}
|
|
24671
|
+
if (!isAIAssistRequestBody(body)) {
|
|
24672
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
24673
|
+
res.end(
|
|
24674
|
+
JSON.stringify({
|
|
24675
|
+
code: "invalid-request",
|
|
24676
|
+
message: "Missing or invalid AI Assist request"
|
|
24677
|
+
})
|
|
24678
|
+
);
|
|
24679
|
+
return;
|
|
24680
|
+
}
|
|
24681
|
+
try {
|
|
24682
|
+
const proposal = await runAIAssist(
|
|
24683
|
+
body,
|
|
24684
|
+
context.aiProviderConfig,
|
|
24685
|
+
schemaDiagnosticsForAssist,
|
|
24686
|
+
void 0,
|
|
24687
|
+
context.aiClient
|
|
24688
|
+
);
|
|
24689
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
24690
|
+
res.end(JSON.stringify({ proposal }));
|
|
24691
|
+
} catch (e) {
|
|
24692
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
24693
|
+
res.end(
|
|
24694
|
+
JSON.stringify({
|
|
24695
|
+
code: "ai-error",
|
|
24696
|
+
message: e instanceof Error ? e.message : "AI Assist failed"
|
|
24697
|
+
})
|
|
24698
|
+
);
|
|
24699
|
+
}
|
|
24700
|
+
return;
|
|
24701
|
+
}
|
|
24223
24702
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
24224
24703
|
res.end(JSON.stringify({ code: "not-found", message: "Not found" }));
|
|
24225
24704
|
};
|
|
@@ -24713,7 +25192,8 @@ async function editCommand(args, options = {}) {
|
|
|
24713
25192
|
editorBundlePath: "",
|
|
24714
25193
|
editorCssPath: "",
|
|
24715
25194
|
editorFontsPath: "",
|
|
24716
|
-
aiClient
|
|
25195
|
+
aiClient,
|
|
25196
|
+
aiProviderConfig: providerConfig ?? void 0
|
|
24717
25197
|
};
|
|
24718
25198
|
let server;
|
|
24719
25199
|
try {
|
|
@@ -24806,64 +25286,26 @@ async function ensureCivilizationProjectName(storage, filePath, data) {
|
|
|
24806
25286
|
}
|
|
24807
25287
|
function generateEditorHtml(apiBase, csrfToken, filePath, aiConfigured) {
|
|
24808
25288
|
const aiAssistHandler = aiConfigured ? `
|
|
24809
|
-
aiAssist: async function
|
|
24810
|
-
const res = await fetch(apiBase + '/api/ai/
|
|
25289
|
+
aiAssist: async function(request) {
|
|
25290
|
+
const res = await fetch(apiBase + '/api/ai/assist', {
|
|
24811
25291
|
method: 'POST',
|
|
24812
25292
|
headers: {
|
|
24813
25293
|
'Content-Type': 'application/json',
|
|
24814
25294
|
'X-CSRF-Token': csrfToken,
|
|
24815
25295
|
},
|
|
24816
|
-
body: JSON.stringify(
|
|
24817
|
-
messages: request.context.project ? [
|
|
24818
|
-
{ role: 'system', content: 'You are an expert Rogatio rule author.' },
|
|
24819
|
-
{ role: 'user', content: request.prompt }
|
|
24820
|
-
] : [{ role: 'user', content: request.prompt }],
|
|
24821
|
-
model: 'gpt-4o-mini',
|
|
24822
|
-
stream: true,
|
|
24823
|
-
}),
|
|
25296
|
+
body: JSON.stringify(request),
|
|
24824
25297
|
});
|
|
24825
|
-
|
|
25298
|
+
const payload = await res.json().catch(function() { return null; });
|
|
24826
25299
|
if (!res.ok) {
|
|
24827
|
-
|
|
24828
|
-
|
|
24829
|
-
|
|
25300
|
+
var message = payload && payload.message
|
|
25301
|
+
? payload.message
|
|
25302
|
+
: 'AI Assist failed';
|
|
25303
|
+
throw new Error(message);
|
|
24830
25304
|
}
|
|
24831
|
-
|
|
24832
|
-
|
|
24833
|
-
const decoder = new TextDecoder();
|
|
24834
|
-
if (!reader) {
|
|
24835
|
-
yield { type: 'error', error: 'No response body' };
|
|
24836
|
-
return;
|
|
24837
|
-
}
|
|
24838
|
-
|
|
24839
|
-
let buffer = '';
|
|
24840
|
-
while (true) {
|
|
24841
|
-
const { done, value } = await reader.read();
|
|
24842
|
-
if (done) break;
|
|
24843
|
-
buffer += decoder.decode(value, { stream: true });
|
|
24844
|
-
const lines = buffer.split('\\n');
|
|
24845
|
-
buffer = lines.pop() || '';
|
|
24846
|
-
for (const line of lines) {
|
|
24847
|
-
if (!line.startsWith('data: ')) continue;
|
|
24848
|
-
const data = line.slice(6).trim();
|
|
24849
|
-
if (data === '[DONE]') continue;
|
|
24850
|
-
try {
|
|
24851
|
-
const parsed = JSON.parse(data);
|
|
24852
|
-
if (parsed.type === 'token' && parsed.content) {
|
|
24853
|
-
yield { type: 'token', content: parsed.content };
|
|
24854
|
-
} else if (parsed.done) {
|
|
24855
|
-
yield { type: 'done', proposal: parsed.proposal };
|
|
24856
|
-
return;
|
|
24857
|
-
} else if (parsed.error) {
|
|
24858
|
-
yield { type: 'error', error: parsed.error };
|
|
24859
|
-
return;
|
|
24860
|
-
}
|
|
24861
|
-
} catch {
|
|
24862
|
-
// Ignore parse errors
|
|
24863
|
-
}
|
|
24864
|
-
}
|
|
25305
|
+
if (!payload || !payload.proposal) {
|
|
25306
|
+
throw new Error('AI Assist returned no proposal');
|
|
24865
25307
|
}
|
|
24866
|
-
|
|
25308
|
+
return { proposal: payload.proposal };
|
|
24867
25309
|
},` : "";
|
|
24868
25310
|
return `<!DOCTYPE html>
|
|
24869
25311
|
<html lang="en">
|
package/package.json
CHANGED