@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.
@@ -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 addMessage(message) {
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 + content;
518
- const bubbles = messagesContainer.querySelectorAll(
519
- "div > div:first-child"
520
- );
521
- if (bubbles[streamingMessageIndex]) {
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
- // onClose
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.hide();
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
- const actionField = ruleProposal.kind === "redirect" ? ruleProposal.kind : "action";
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;
@@ -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.15.1",
3
+ "version": "5.17.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.",