@rogatio/cli 5.16.0 → 5.18.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.
@@ -1844,9 +1844,6 @@ var CREATABLE_RULE_FIELDS = /* @__PURE__ */ new Set([
1844
1844
  ...ACTION_PAYLOAD_FIELDS,
1845
1845
  "redactSensitiveInLogs"
1846
1846
  ]);
1847
- function isBodyRuleType(type) {
1848
- return type === "request-body" || type === "response-body";
1849
- }
1850
1847
  function setValueAtPath(root, path, value) {
1851
1848
  const segments = decodePointer(path);
1852
1849
  if (!segments || segments.length === 0) return false;
@@ -3937,25 +3934,23 @@ var EditorControllerImpl = class {
3937
3934
  this.renderField(matcherGrid, "Method", `${rulePath}/method`, method);
3938
3935
  matcherFields.append(matcherGrid);
3939
3936
  card.append(matcherFields);
3940
- if (!isBodyRuleType(rule.type)) {
3941
- const redactFieldset = this.document.createElement("fieldset");
3942
- const redactLegend = this.document.createElement("legend");
3943
- redactLegend.textContent = "Match logging";
3944
- redactFieldset.append(redactLegend);
3945
- const redactGrid = this.document.createElement("div");
3946
- redactGrid.dataset.editorFields = "true";
3947
- const redactCheckbox = this.document.createElement("input");
3948
- redactCheckbox.type = "checkbox";
3949
- redactCheckbox.checked = rule.redactSensitiveInLogs === true;
3950
- this.renderField(
3951
- redactGrid,
3952
- "Redact sensitive fields in logs",
3953
- `${rulePath}/redactSensitiveInLogs`,
3954
- redactCheckbox
3955
- );
3956
- redactFieldset.append(redactGrid);
3957
- card.append(redactFieldset);
3958
- }
3937
+ const redactFieldset = this.document.createElement("fieldset");
3938
+ const redactLegend = this.document.createElement("legend");
3939
+ redactLegend.textContent = "Match logging";
3940
+ redactFieldset.append(redactLegend);
3941
+ const redactGrid = this.document.createElement("div");
3942
+ redactGrid.dataset.editorFields = "true";
3943
+ const redactCheckbox = this.document.createElement("input");
3944
+ redactCheckbox.type = "checkbox";
3945
+ redactCheckbox.checked = rule.redactSensitiveInLogs === true;
3946
+ this.renderField(
3947
+ redactGrid,
3948
+ "Redact sensitive fields in logs",
3949
+ `${rulePath}/redactSensitiveInLogs`,
3950
+ redactCheckbox
3951
+ );
3952
+ redactFieldset.append(redactGrid);
3953
+ card.append(redactFieldset);
3959
3954
  if (this.extensions.length > 0) {
3960
3955
  const currentType = this.extensions.find(
3961
3956
  (e) => e.matches(rule)
@@ -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 isRecord(value) {
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 (!isRecord(body) || !isRecord(body.project)) {
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 (!isRecord(body) || !Array.isArray(body.messages)) {
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 (!isRecord(body) || !Array.isArray(body.messages)) {
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*(request) {
24810
- const res = await fetch(apiBase + '/api/ai/stream', {
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
- const error = await res.json();
24828
- yield { type: 'error', error: error.message };
24829
- return;
25300
+ var message = payload && payload.message
25301
+ ? payload.message
25302
+ : 'AI Assist failed';
25303
+ throw new Error(message);
24830
25304
  }
24831
-
24832
- const reader = res.body?.getReader();
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
- yield { type: 'done', proposal: null };
25308
+ return { proposal: payload.proposal };
24867
25309
  },` : "";
24868
25310
  return `<!DOCTYPE html>
24869
25311
  <html lang="en">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogatio/cli",
3
- "version": "5.16.0",
3
+ "version": "5.18.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Local-first browser request and response rules — editor host, file verification, test runner, and runtime dispatch.",