@rogatio/cli 5.16.0 → 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/node/index.js +496 -54
- package/package.json +1 -1
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