@noodleseed/one 0.89.1 → 0.90.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.
Files changed (178) hide show
  1. package/dist/commands/billing-enforcement-activation-ops.js +1 -11
  2. package/dist/commands/billing-enforcement-activation-ops.js.map +1 -1
  3. package/dist/commands/billing-enforcement-ops.js +1 -11
  4. package/dist/commands/billing-enforcement-ops.js.map +1 -1
  5. package/dist/commands/billing-metering-ops.js +1 -11
  6. package/dist/commands/billing-metering-ops.js.map +1 -1
  7. package/dist/commands/billing-metering-validation-ops.js +1 -11
  8. package/dist/commands/billing-metering-validation-ops.js.map +1 -1
  9. package/dist/commands/billing-read-ops.js +1 -11
  10. package/dist/commands/billing-read-ops.js.map +1 -1
  11. package/dist/commands/billing-stripe-ops.js +1 -11
  12. package/dist/commands/billing-stripe-ops.js.map +1 -1
  13. package/dist/commands/org-admin.d.ts +6 -0
  14. package/dist/commands/org-admin.d.ts.map +1 -1
  15. package/dist/commands/org-admin.js +1 -29
  16. package/dist/commands/org-admin.js.map +1 -1
  17. package/dist/commands/org-domains.js +1 -22
  18. package/dist/commands/org-domains.js.map +1 -1
  19. package/dist/commands/org-member-args.d.ts +1 -1
  20. package/dist/commands/org-member-args.d.ts.map +1 -1
  21. package/dist/commands/org-member-args.js +1 -10
  22. package/dist/commands/org-member-args.js.map +1 -1
  23. package/dist/commands/org-openai-challenge.js +1 -22
  24. package/dist/commands/org-openai-challenge.js.map +1 -1
  25. package/dist/commands/platform-account-reset-args.d.ts.map +1 -1
  26. package/dist/commands/platform-account-reset-args.js +1 -6
  27. package/dist/commands/platform-account-reset-args.js.map +1 -1
  28. package/dist/commands/platform-account-reset-ops.js +1 -11
  29. package/dist/commands/platform-account-reset-ops.js.map +1 -1
  30. package/dist/commands/platform-auth-migration-args.d.ts +2 -0
  31. package/dist/commands/platform-auth-migration-args.d.ts.map +1 -1
  32. package/dist/commands/platform-auth-migration-args.js +2 -1
  33. package/dist/commands/platform-auth-migration-args.js.map +1 -1
  34. package/dist/commands/platform-auth-migration-ops.js +1 -11
  35. package/dist/commands/platform-auth-migration-ops.js.map +1 -1
  36. package/dist/commands/shared.d.ts +6 -0
  37. package/dist/commands/shared.d.ts.map +1 -1
  38. package/dist/commands/shared.js +34 -0
  39. package/dist/commands/shared.js.map +1 -1
  40. package/dist/deploy.d.ts +2 -0
  41. package/dist/deploy.d.ts.map +1 -1
  42. package/dist/deploy.js +4 -4
  43. package/dist/deploy.js.map +1 -1
  44. package/dist/dev.d.ts.map +1 -1
  45. package/dist/dev.js +1 -7
  46. package/dist/dev.js.map +1 -1
  47. package/dist/devtools-brand.d.ts +14 -0
  48. package/dist/devtools-brand.d.ts.map +1 -0
  49. package/dist/devtools-brand.js +98 -0
  50. package/dist/devtools-brand.js.map +1 -0
  51. package/dist/devtools-chat-anthropic.d.ts +3 -0
  52. package/dist/devtools-chat-anthropic.d.ts.map +1 -0
  53. package/dist/devtools-chat-anthropic.js +75 -0
  54. package/dist/devtools-chat-anthropic.js.map +1 -0
  55. package/dist/devtools-chat-gemini.d.ts +3 -0
  56. package/dist/devtools-chat-gemini.d.ts.map +1 -0
  57. package/dist/devtools-chat-gemini.js +82 -0
  58. package/dist/devtools-chat-gemini.js.map +1 -0
  59. package/dist/devtools-chat-types.d.ts +64 -0
  60. package/dist/devtools-chat-types.d.ts.map +1 -0
  61. package/dist/devtools-chat-types.js +64 -0
  62. package/dist/devtools-chat-types.js.map +1 -0
  63. package/dist/devtools-chat.d.ts +7 -39
  64. package/dist/devtools-chat.d.ts.map +1 -1
  65. package/dist/devtools-chat.js +33 -48
  66. package/dist/devtools-chat.js.map +1 -1
  67. package/dist/devtools-copy-styles.d.ts +3 -0
  68. package/dist/devtools-copy-styles.d.ts.map +1 -0
  69. package/dist/devtools-copy-styles.js +54 -0
  70. package/dist/devtools-copy-styles.js.map +1 -0
  71. package/dist/devtools-design.d.ts +5 -0
  72. package/dist/devtools-design.d.ts.map +1 -0
  73. package/dist/devtools-design.js +62 -0
  74. package/dist/devtools-design.js.map +1 -0
  75. package/dist/devtools-harness.d.ts +2 -2
  76. package/dist/devtools-harness.d.ts.map +1 -1
  77. package/dist/devtools-harness.js +130 -216
  78. package/dist/devtools-harness.js.map +1 -1
  79. package/dist/devtools-json.d.ts +10 -0
  80. package/dist/devtools-json.d.ts.map +1 -0
  81. package/dist/devtools-json.js +23 -0
  82. package/dist/devtools-json.js.map +1 -0
  83. package/dist/devtools-preview-control-styles.d.ts +3 -0
  84. package/dist/devtools-preview-control-styles.d.ts.map +1 -0
  85. package/dist/devtools-preview-control-styles.js +207 -0
  86. package/dist/devtools-preview-control-styles.js.map +1 -0
  87. package/dist/devtools-preview.d.ts +7 -3
  88. package/dist/devtools-preview.d.ts.map +1 -1
  89. package/dist/devtools-preview.js +189 -43
  90. package/dist/devtools-preview.js.map +1 -1
  91. package/dist/devtools-provider-logos.d.ts +5 -0
  92. package/dist/devtools-provider-logos.d.ts.map +1 -0
  93. package/dist/devtools-provider-logos.js +5 -0
  94. package/dist/devtools-provider-logos.js.map +1 -0
  95. package/dist/devtools-responsive-styles.d.ts +3 -0
  96. package/dist/devtools-responsive-styles.d.ts.map +1 -0
  97. package/dist/devtools-responsive-styles.js +75 -0
  98. package/dist/devtools-responsive-styles.js.map +1 -0
  99. package/dist/devtools-styles.d.ts +3 -0
  100. package/dist/devtools-styles.d.ts.map +1 -0
  101. package/dist/devtools-styles.js +694 -0
  102. package/dist/devtools-styles.js.map +1 -0
  103. package/dist/local-target.d.ts.map +1 -1
  104. package/dist/local-target.js +1 -6
  105. package/dist/local-target.js.map +1 -1
  106. package/dist/plugin-mode/build-readiness-server.d.ts +4 -10
  107. package/dist/plugin-mode/build-readiness-server.d.ts.map +1 -1
  108. package/dist/plugin-mode/build-readiness-server.js +68 -67
  109. package/dist/plugin-mode/build-readiness-server.js.map +1 -1
  110. package/dist/plugin-mode/plugin-command-runner.d.ts +41 -0
  111. package/dist/plugin-mode/plugin-command-runner.d.ts.map +1 -0
  112. package/dist/plugin-mode/plugin-command-runner.js +122 -0
  113. package/dist/plugin-mode/plugin-command-runner.js.map +1 -0
  114. package/dist/plugin-mode/plugin-operation-contract.d.ts +237 -0
  115. package/dist/plugin-mode/plugin-operation-contract.d.ts.map +1 -0
  116. package/dist/plugin-mode/plugin-operation-contract.js +164 -0
  117. package/dist/plugin-mode/plugin-operation-contract.js.map +1 -0
  118. package/dist/plugin-mode/plugin-operation-tools.d.ts +20 -0
  119. package/dist/plugin-mode/plugin-operation-tools.d.ts.map +1 -0
  120. package/dist/plugin-mode/plugin-operation-tools.js +289 -0
  121. package/dist/plugin-mode/plugin-operation-tools.js.map +1 -0
  122. package/dist/preview-session.d.ts.map +1 -1
  123. package/dist/preview-session.js +22 -5
  124. package/dist/preview-session.js.map +1 -1
  125. package/dist/project.d.ts.map +1 -1
  126. package/dist/project.js +1 -7
  127. package/dist/project.js.map +1 -1
  128. package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +1 -1
  129. package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js.map +1 -1
  130. package/node_modules/@noodle-borg/agent-kit/dist/plugin-bundle.d.ts.map +1 -1
  131. package/node_modules/@noodle-borg/agent-kit/dist/plugin-bundle.js +16 -12
  132. package/node_modules/@noodle-borg/agent-kit/dist/plugin-bundle.js.map +1 -1
  133. package/node_modules/@noodle-borg/agent-kit/dist/plugin-marketplace-docs.js +9 -9
  134. package/node_modules/@noodle-borg/agent-kit/dist/skill-feedback-ref.js +4 -4
  135. package/node_modules/@noodle-borg/agent-kit/dist/skill-feedback-ref.js.map +1 -1
  136. package/node_modules/@noodle-borg/agent-kit/dist/skill-router.d.ts.map +1 -1
  137. package/node_modules/@noodle-borg/agent-kit/dist/skill-router.js +6 -3
  138. package/node_modules/@noodle-borg/agent-kit/dist/skill-router.js.map +1 -1
  139. package/node_modules/@noodle-borg/connector-defs/dist/index.d.ts +1 -1
  140. package/node_modules/@noodle-borg/connector-defs/dist/index.d.ts.map +1 -1
  141. package/node_modules/@noodle-borg/connector-defs/dist/index.js.map +1 -1
  142. package/node_modules/@noodle-borg/module-audit/dist/route.d.ts +0 -2
  143. package/node_modules/@noodle-borg/module-audit/dist/route.d.ts.map +1 -1
  144. package/node_modules/@noodle-borg/module-audit/dist/route.js +0 -4
  145. package/node_modules/@noodle-borg/module-audit/dist/route.js.map +1 -1
  146. package/node_modules/@noodle-borg/protocol/dist/input-validation.d.ts +0 -2
  147. package/node_modules/@noodle-borg/protocol/dist/input-validation.d.ts.map +1 -1
  148. package/node_modules/@noodle-borg/protocol/dist/input-validation.js +1 -5
  149. package/node_modules/@noodle-borg/protocol/dist/input-validation.js.map +1 -1
  150. package/node_modules/@noodle-borg/service/dist/auth/deploy-gate.d.ts +0 -2
  151. package/node_modules/@noodle-borg/service/dist/auth/deploy-gate.d.ts.map +1 -1
  152. package/node_modules/@noodle-borg/service/dist/auth/deploy-gate.js +0 -7
  153. package/node_modules/@noodle-borg/service/dist/auth/deploy-gate.js.map +1 -1
  154. package/node_modules/@noodle-borg/service/dist/credential-binding-index.d.ts +1 -3
  155. package/node_modules/@noodle-borg/service/dist/credential-binding-index.d.ts.map +1 -1
  156. package/node_modules/@noodle-borg/service/dist/credential-binding-index.js +1 -14
  157. package/node_modules/@noodle-borg/service/dist/credential-binding-index.js.map +1 -1
  158. package/node_modules/@noodle-borg/service/dist/index.d.ts +5 -5
  159. package/node_modules/@noodle-borg/service/dist/index.d.ts.map +1 -1
  160. package/node_modules/@noodle-borg/service/dist/index.js +1 -1
  161. package/node_modules/@noodle-borg/service/dist/index.js.map +1 -1
  162. package/node_modules/@noodle-borg/service/dist/mcp-public-routing.d.ts +0 -2
  163. package/node_modules/@noodle-borg/service/dist/mcp-public-routing.d.ts.map +1 -1
  164. package/node_modules/@noodle-borg/service/dist/mcp-public-routing.js +0 -6
  165. package/node_modules/@noodle-borg/service/dist/mcp-public-routing.js.map +1 -1
  166. package/node_modules/@noodle-borg/service/dist/oauth/console-url.d.ts +2 -0
  167. package/node_modules/@noodle-borg/service/dist/oauth/console-url.d.ts.map +1 -0
  168. package/node_modules/@noodle-borg/service/dist/oauth/console-url.js +4 -0
  169. package/node_modules/@noodle-borg/service/dist/oauth/console-url.js.map +1 -0
  170. package/node_modules/@noodle-borg/service/dist/oauth/service-bootstrap.d.ts.map +1 -1
  171. package/node_modules/@noodle-borg/service/dist/oauth/service-bootstrap.js +2 -1
  172. package/node_modules/@noodle-borg/service/dist/oauth/service-bootstrap.js.map +1 -1
  173. package/node_modules/@noodle-borg/service/dist/registry-targets.d.ts +1 -3
  174. package/node_modules/@noodle-borg/service/dist/registry-targets.d.ts.map +1 -1
  175. package/node_modules/@noodle-borg/service/dist/registry-targets.js +0 -17
  176. package/node_modules/@noodle-borg/service/dist/registry-targets.js.map +1 -1
  177. package/node_modules/@noodle-borg/service/package.json +0 -1
  178. package/package.json +1 -1
@@ -0,0 +1,10 @@
1
+ /** Syntax colours for structured output in preview, chat tool calls, and the MCP activity rail. */
2
+ export declare const DEVTOOLS_JSON_STYLES: string;
3
+ /**
4
+ * A dependency-free JSON renderer for the classic devtools client.
5
+ *
6
+ * Values are first serialized with native JSON formatting, then lexed into text-only spans. Tool data
7
+ * never reaches `innerHTML`, so strings such as `</script>` and `<img onerror=…>` remain inert output.
8
+ */
9
+ export declare const DEVTOOLS_JSON_CLIENT_JS: string;
10
+ //# sourceMappingURL=devtools-json.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devtools-json.d.ts","sourceRoot":"","sources":["../src/devtools-json.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,eAAO,MAAM,oBAAoB,QAQhC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,QAKxB,CAAC"}
@@ -0,0 +1,23 @@
1
+ /** Syntax colours for structured output in preview, chat tool calls, and the MCP activity rail. */
2
+ export const DEVTOOLS_JSON_STYLES = String.raw `
3
+ .json-key{color:#F97316}
4
+ .json-string{color:#F5C0A8}
5
+ .json-number{color:#F59E0B}
6
+ .json-literal{color:#F43F5E}
7
+ .json-punctuation{color:#7C7470}
8
+ .json-fallback{color:#EDE8E6}
9
+ .json-error{color:#FB7185}
10
+ `;
11
+ /**
12
+ * A dependency-free JSON renderer for the classic devtools client.
13
+ *
14
+ * Values are first serialized with native JSON formatting, then lexed into text-only spans. Tool data
15
+ * never reaches `innerHTML`, so strings such as `</script>` and `<img onerror=…>` remain inert output.
16
+ */
17
+ export const DEVTOOLS_JSON_CLIENT_JS = [
18
+ 'function jsonSource(value){ if(typeof value==="string"){ var trimmed=value.trim(); if((trimmed.charAt(0)==="{"&&trimmed.charAt(trimmed.length-1)==="}")||(trimmed.charAt(0)==="["&&trimmed.charAt(trimmed.length-1)==="]")){ try{return JSON.stringify(JSON.parse(value),null,2);}catch(e){} } return value; } try{ var encoded=JSON.stringify(value,null,2); return encoded===undefined?String(value):encoded; }catch(e){ return String(value); } }',
19
+ 'function jsonClass(token,source,end){ if(token.charAt(0)==="\\""){ return /^\\s*:/.test(source.slice(end))?"json-key":"json-string"; } if(/^-?\\d/.test(token)) return "json-number"; if(/^(true|false|null)$/.test(token)) return "json-literal"; return "json-punctuation"; }',
20
+ 'function appendJsonText(parent,text,cls){ if(!text) return; if(!cls){ parent.appendChild(document.createTextNode(text)); return; } var span=document.createElement("span"); span.className=cls; span.textContent=text; parent.appendChild(span); }',
21
+ 'function renderJson(pre,value){ var source=jsonSource(value); var pattern=/"(?:\\\\.|[^"\\\\])*"|-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?|\\b(?:true|false|null)\\b|[{}\\[\\],:]/g; var fragment=document.createDocumentFragment(); var cursor=0; var match; while((match=pattern.exec(source))!==null){ appendJsonText(fragment,source.slice(cursor,match.index),""); appendJsonText(fragment,match[0],jsonClass(match[0],source,pattern.lastIndex)); cursor=pattern.lastIndex; } appendJsonText(fragment,source.slice(cursor),""); pre.replaceChildren(fragment); pre.setAttribute("aria-label","JSON output"); }',
22
+ ].join('\n');
23
+ //# sourceMappingURL=devtools-json.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devtools-json.js","sourceRoot":"","sources":["../src/devtools-json.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;CAQ7C,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG;IACrC,sbAAsb;IACtb,iRAAiR;IACjR,oPAAoP;IACpP,ilBAAilB;CACllB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC"}
@@ -0,0 +1,3 @@
1
+ /** Compact, direct preview controls for theme, device presets, width, reload, and fullscreen. */
2
+ export declare const DEVTOOLS_PREVIEW_CONTROL_STYLES = "\n.controls{\n display:flex;\n align-items:center;\n gap:9px;\n flex-wrap:wrap;\n margin-bottom:16px;\n padding:7px;\n border:0;\n border-radius:15px;\n background:rgba(255,255,255,.035);\n}\n.ctl{display:flex;align-items:center;gap:7px}\n.ctl__lbl{\n color:var(--nd-faint);\n font-size:9px;\n font-weight:650;\n letter-spacing:.09em;\n text-transform:uppercase;\n}\n.theme-switch,.device-switch{\n display:inline-flex;\n align-items:center;\n border:0;\n border-radius:999px;\n background:rgba(0,0,0,.34);\n}\n.theme-switch{\n gap:6px;\n min-height:31px;\n padding:4px 7px;\n color:var(--nd-faint);\n cursor:pointer;\n}\n.theme-switch:hover{color:var(--nd-text)}\n.theme-switch__icon{display:grid;width:14px;height:14px;place-items:center}\n.theme-switch__icon svg{width:13px;height:13px}\n.theme-switch__track{\n position:relative;\n width:27px;\n height:15px;\n border-radius:999px;\n background:rgba(255,255,255,.11);\n transition:background .18s ease;\n}\n.theme-switch__thumb{\n position:absolute;\n top:3px;\n left:3px;\n width:9px;\n height:9px;\n border-radius:50%;\n background:#f4f4f5;\n box-shadow:0 1px 5px rgba(0,0,0,.45);\n transition:transform .2s cubic-bezier(.2,.8,.2,1),background .18s ease;\n}\n.theme-switch[aria-pressed=true] .theme-switch__track{background:rgba(249,115,22,.32)}\n.theme-switch[aria-pressed=true] .theme-switch__thumb{\n background:#fdba74;\n transform:translateX(12px);\n}\n.device-switch{gap:2px;padding:3px}\n.device-switch button{\n display:grid;\n width:29px;\n height:25px;\n place-items:center;\n padding:0;\n border:0;\n border-radius:999px;\n background:transparent;\n color:var(--nd-faint);\n cursor:pointer;\n transition:background .18s ease,color .18s ease,transform .18s ease;\n}\n.device-switch button:hover{color:var(--nd-text);transform:translateY(-1px)}\n.device-switch button.is-active{\n background:rgba(255,255,255,.11);\n color:#fff;\n}\n.device-switch svg{width:14px;height:14px}\n.ctl--width{\n gap:10px;\n padding:3px 4px 3px 9px;\n border-radius:999px;\n background:rgba(0,0,0,.28);\n transition:background .18s ease,box-shadow .18s ease;\n}\n.ctl--width:hover{background:rgba(0,0,0,.42)}\n.ctl--width:focus-within{\n background:rgba(255,255,255,.055);\n box-shadow:0 8px 24px rgba(0,0,0,.18),0 0 20px rgba(249,115,22,.07);\n}\n.ctl--width input[type=range]{\n --width-progress:56.82%;\n appearance:none;\n -webkit-appearance:none;\n width:clamp(132px,12vw,184px);\n height:24px;\n margin:0;\n padding:0;\n border:0;\n background:transparent;\n cursor:ew-resize;\n}\n.ctl--width input[type=range]:focus-visible{background:transparent;box-shadow:none}\n.ctl--width input[type=range]::-webkit-slider-runnable-track{\n height:5px;\n border-radius:999px;\n background:\n linear-gradient(90deg,var(--nd-accent),var(--nd-rose)) 0/var(--width-progress) 100% no-repeat,\n rgba(255,255,255,.11);\n box-shadow:inset 0 1px 2px rgba(0,0,0,.55);\n}\n.ctl--width input[type=range]::-webkit-slider-thumb{\n appearance:none;\n -webkit-appearance:none;\n width:15px;\n height:15px;\n margin-top:-5px;\n border:0;\n border-radius:50%;\n background:#fafafa;\n box-shadow:\n 0 2px 8px rgba(0,0,0,.62),\n 0 0 0 4px rgba(249,115,22,.13),\n 0 0 14px rgba(244,63,94,.13);\n transition:transform .17s cubic-bezier(.2,.8,.2,1),box-shadow .17s ease;\n}\n.ctl--width input[type=range]::-moz-range-track{\n height:5px;\n border:0;\n border-radius:999px;\n background:rgba(255,255,255,.11);\n box-shadow:inset 0 1px 2px rgba(0,0,0,.55);\n}\n.ctl--width input[type=range]::-moz-range-progress{\n height:5px;\n border-radius:999px;\n background:linear-gradient(90deg,var(--nd-accent),var(--nd-rose));\n}\n.ctl--width input[type=range]::-moz-range-thumb{\n width:15px;\n height:15px;\n border:0;\n border-radius:50%;\n background:#fafafa;\n box-shadow:\n 0 2px 8px rgba(0,0,0,.62),\n 0 0 0 4px rgba(249,115,22,.13),\n 0 0 14px rgba(244,63,94,.13);\n transition:transform .17s cubic-bezier(.2,.8,.2,1),box-shadow .17s ease;\n}\n.ctl--width:hover input[type=range]::-webkit-slider-thumb,\n.ctl--width:focus-within input[type=range]::-webkit-slider-thumb{\n transform:scale(1.1);\n box-shadow:\n 0 3px 10px rgba(0,0,0,.7),\n 0 0 0 5px rgba(249,115,22,.17),\n 0 0 18px rgba(244,63,94,.2);\n}\n.ctl--width:hover input[type=range]::-moz-range-thumb,\n.ctl--width:focus-within input[type=range]::-moz-range-thumb{\n transform:scale(1.1);\n box-shadow:\n 0 3px 10px rgba(0,0,0,.7),\n 0 0 0 5px rgba(249,115,22,.17),\n 0 0 18px rgba(244,63,94,.2);\n}\n.ctl--width:active input[type=range]::-webkit-slider-thumb{transform:scale(.94)}\n.ctl--width:active input[type=range]::-moz-range-thumb{transform:scale(.94)}\n.ctl__val{\n min-width:57px;\n padding:5px 8px;\n border-radius:999px;\n background:rgba(255,255,255,.06);\n color:#e4e4e7;\n font-family:var(--nd-mono);\n font-size:10.5px;\n font-variant-numeric:tabular-nums;\n line-height:1;\n text-align:center;\n}\n@media (prefers-reduced-motion:reduce){\n .ctl--width input[type=range]::-webkit-slider-thumb,\n .ctl--width input[type=range]::-moz-range-thumb{transform:none!important}\n}\n.ctl__spacer{margin-left:auto}\n.ctl-btn{\n display:inline-flex;\n align-items:center;\n gap:6px;\n min-height:31px;\n padding:6px 10px;\n border:0;\n border-radius:9px;\n background:rgba(0,0,0,.34);\n color:var(--nd-muted);\n font-size:11px;\n font-weight:550;\n cursor:pointer;\n}\n.ctl-btn:hover{background:var(--nd-raised);color:var(--nd-text)}\n.ctl-btn svg{width:13px;height:13px}\n";
3
+ //# sourceMappingURL=devtools-preview-control-styles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devtools-preview-control-styles.d.ts","sourceRoot":"","sources":["../src/devtools-preview-control-styles.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,eAAO,MAAM,+BAA+B,y/KA4M3C,CAAC"}
@@ -0,0 +1,207 @@
1
+ /** Compact, direct preview controls for theme, device presets, width, reload, and fullscreen. */
2
+ export const DEVTOOLS_PREVIEW_CONTROL_STYLES = `
3
+ .controls{
4
+ display:flex;
5
+ align-items:center;
6
+ gap:9px;
7
+ flex-wrap:wrap;
8
+ margin-bottom:16px;
9
+ padding:7px;
10
+ border:0;
11
+ border-radius:15px;
12
+ background:rgba(255,255,255,.035);
13
+ }
14
+ .ctl{display:flex;align-items:center;gap:7px}
15
+ .ctl__lbl{
16
+ color:var(--nd-faint);
17
+ font-size:9px;
18
+ font-weight:650;
19
+ letter-spacing:.09em;
20
+ text-transform:uppercase;
21
+ }
22
+ .theme-switch,.device-switch{
23
+ display:inline-flex;
24
+ align-items:center;
25
+ border:0;
26
+ border-radius:999px;
27
+ background:rgba(0,0,0,.34);
28
+ }
29
+ .theme-switch{
30
+ gap:6px;
31
+ min-height:31px;
32
+ padding:4px 7px;
33
+ color:var(--nd-faint);
34
+ cursor:pointer;
35
+ }
36
+ .theme-switch:hover{color:var(--nd-text)}
37
+ .theme-switch__icon{display:grid;width:14px;height:14px;place-items:center}
38
+ .theme-switch__icon svg{width:13px;height:13px}
39
+ .theme-switch__track{
40
+ position:relative;
41
+ width:27px;
42
+ height:15px;
43
+ border-radius:999px;
44
+ background:rgba(255,255,255,.11);
45
+ transition:background .18s ease;
46
+ }
47
+ .theme-switch__thumb{
48
+ position:absolute;
49
+ top:3px;
50
+ left:3px;
51
+ width:9px;
52
+ height:9px;
53
+ border-radius:50%;
54
+ background:#f4f4f5;
55
+ box-shadow:0 1px 5px rgba(0,0,0,.45);
56
+ transition:transform .2s cubic-bezier(.2,.8,.2,1),background .18s ease;
57
+ }
58
+ .theme-switch[aria-pressed=true] .theme-switch__track{background:rgba(249,115,22,.32)}
59
+ .theme-switch[aria-pressed=true] .theme-switch__thumb{
60
+ background:#fdba74;
61
+ transform:translateX(12px);
62
+ }
63
+ .device-switch{gap:2px;padding:3px}
64
+ .device-switch button{
65
+ display:grid;
66
+ width:29px;
67
+ height:25px;
68
+ place-items:center;
69
+ padding:0;
70
+ border:0;
71
+ border-radius:999px;
72
+ background:transparent;
73
+ color:var(--nd-faint);
74
+ cursor:pointer;
75
+ transition:background .18s ease,color .18s ease,transform .18s ease;
76
+ }
77
+ .device-switch button:hover{color:var(--nd-text);transform:translateY(-1px)}
78
+ .device-switch button.is-active{
79
+ background:rgba(255,255,255,.11);
80
+ color:#fff;
81
+ }
82
+ .device-switch svg{width:14px;height:14px}
83
+ .ctl--width{
84
+ gap:10px;
85
+ padding:3px 4px 3px 9px;
86
+ border-radius:999px;
87
+ background:rgba(0,0,0,.28);
88
+ transition:background .18s ease,box-shadow .18s ease;
89
+ }
90
+ .ctl--width:hover{background:rgba(0,0,0,.42)}
91
+ .ctl--width:focus-within{
92
+ background:rgba(255,255,255,.055);
93
+ box-shadow:0 8px 24px rgba(0,0,0,.18),0 0 20px rgba(249,115,22,.07);
94
+ }
95
+ .ctl--width input[type=range]{
96
+ --width-progress:56.82%;
97
+ appearance:none;
98
+ -webkit-appearance:none;
99
+ width:clamp(132px,12vw,184px);
100
+ height:24px;
101
+ margin:0;
102
+ padding:0;
103
+ border:0;
104
+ background:transparent;
105
+ cursor:ew-resize;
106
+ }
107
+ .ctl--width input[type=range]:focus-visible{background:transparent;box-shadow:none}
108
+ .ctl--width input[type=range]::-webkit-slider-runnable-track{
109
+ height:5px;
110
+ border-radius:999px;
111
+ background:
112
+ linear-gradient(90deg,var(--nd-accent),var(--nd-rose)) 0/var(--width-progress) 100% no-repeat,
113
+ rgba(255,255,255,.11);
114
+ box-shadow:inset 0 1px 2px rgba(0,0,0,.55);
115
+ }
116
+ .ctl--width input[type=range]::-webkit-slider-thumb{
117
+ appearance:none;
118
+ -webkit-appearance:none;
119
+ width:15px;
120
+ height:15px;
121
+ margin-top:-5px;
122
+ border:0;
123
+ border-radius:50%;
124
+ background:#fafafa;
125
+ box-shadow:
126
+ 0 2px 8px rgba(0,0,0,.62),
127
+ 0 0 0 4px rgba(249,115,22,.13),
128
+ 0 0 14px rgba(244,63,94,.13);
129
+ transition:transform .17s cubic-bezier(.2,.8,.2,1),box-shadow .17s ease;
130
+ }
131
+ .ctl--width input[type=range]::-moz-range-track{
132
+ height:5px;
133
+ border:0;
134
+ border-radius:999px;
135
+ background:rgba(255,255,255,.11);
136
+ box-shadow:inset 0 1px 2px rgba(0,0,0,.55);
137
+ }
138
+ .ctl--width input[type=range]::-moz-range-progress{
139
+ height:5px;
140
+ border-radius:999px;
141
+ background:linear-gradient(90deg,var(--nd-accent),var(--nd-rose));
142
+ }
143
+ .ctl--width input[type=range]::-moz-range-thumb{
144
+ width:15px;
145
+ height:15px;
146
+ border:0;
147
+ border-radius:50%;
148
+ background:#fafafa;
149
+ box-shadow:
150
+ 0 2px 8px rgba(0,0,0,.62),
151
+ 0 0 0 4px rgba(249,115,22,.13),
152
+ 0 0 14px rgba(244,63,94,.13);
153
+ transition:transform .17s cubic-bezier(.2,.8,.2,1),box-shadow .17s ease;
154
+ }
155
+ .ctl--width:hover input[type=range]::-webkit-slider-thumb,
156
+ .ctl--width:focus-within input[type=range]::-webkit-slider-thumb{
157
+ transform:scale(1.1);
158
+ box-shadow:
159
+ 0 3px 10px rgba(0,0,0,.7),
160
+ 0 0 0 5px rgba(249,115,22,.17),
161
+ 0 0 18px rgba(244,63,94,.2);
162
+ }
163
+ .ctl--width:hover input[type=range]::-moz-range-thumb,
164
+ .ctl--width:focus-within input[type=range]::-moz-range-thumb{
165
+ transform:scale(1.1);
166
+ box-shadow:
167
+ 0 3px 10px rgba(0,0,0,.7),
168
+ 0 0 0 5px rgba(249,115,22,.17),
169
+ 0 0 18px rgba(244,63,94,.2);
170
+ }
171
+ .ctl--width:active input[type=range]::-webkit-slider-thumb{transform:scale(.94)}
172
+ .ctl--width:active input[type=range]::-moz-range-thumb{transform:scale(.94)}
173
+ .ctl__val{
174
+ min-width:57px;
175
+ padding:5px 8px;
176
+ border-radius:999px;
177
+ background:rgba(255,255,255,.06);
178
+ color:#e4e4e7;
179
+ font-family:var(--nd-mono);
180
+ font-size:10.5px;
181
+ font-variant-numeric:tabular-nums;
182
+ line-height:1;
183
+ text-align:center;
184
+ }
185
+ @media (prefers-reduced-motion:reduce){
186
+ .ctl--width input[type=range]::-webkit-slider-thumb,
187
+ .ctl--width input[type=range]::-moz-range-thumb{transform:none!important}
188
+ }
189
+ .ctl__spacer{margin-left:auto}
190
+ .ctl-btn{
191
+ display:inline-flex;
192
+ align-items:center;
193
+ gap:6px;
194
+ min-height:31px;
195
+ padding:6px 10px;
196
+ border:0;
197
+ border-radius:9px;
198
+ background:rgba(0,0,0,.34);
199
+ color:var(--nd-muted);
200
+ font-size:11px;
201
+ font-weight:550;
202
+ cursor:pointer;
203
+ }
204
+ .ctl-btn:hover{background:var(--nd-raised);color:var(--nd-text)}
205
+ .ctl-btn svg{width:13px;height:13px}
206
+ `;
207
+ //# sourceMappingURL=devtools-preview-control-styles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devtools-preview-control-styles.js","sourceRoot":"","sources":["../src/devtools-preview-control-styles.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,MAAM,CAAC,MAAM,+BAA+B,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4M9C,CAAC"}
@@ -1,4 +1,4 @@
1
- import { harnessHtml, openAiShimScript, type PreviewDevice, type PreviewTheme, wrapWidgetHtml } from './devtools-harness.js';
1
+ import { calculateRangeProgress, harnessHtml, openAiShimScript, type PreviewDevice, type PreviewTheme, wrapWidgetHtml } from './devtools-harness.js';
2
2
  /**
3
3
  * `noodle devtools` preview server. It boots alongside a `dev` MCP endpoint and:
4
4
  * - serves the three-pane harness shell (HTML/CSS/JS in `devtools-harness.ts`),
@@ -9,15 +9,19 @@ import { harnessHtml, openAiShimScript, type PreviewDevice, type PreviewTheme, w
9
9
  * `fetch('/rpc')` the harness proxies and records, without reimplementing the ext-apps host protocol.
10
10
  */
11
11
  export type { PreviewDevice, PreviewTheme };
12
- export { harnessHtml, openAiShimScript, wrapWidgetHtml };
12
+ export { calculateRangeProgress, harnessHtml, openAiShimScript, wrapWidgetHtml };
13
13
  export interface PreviewOptions {
14
14
  readonly mcpUrl: string;
15
15
  readonly theme: PreviewTheme;
16
16
  readonly device: PreviewDevice;
17
17
  readonly port?: number;
18
18
  readonly protocolVersion?: string;
19
- /** OpenAI model for the chat playground; falls back to `OPENAI_MODEL` then {@link DEFAULT_CHAT_MODEL}. */
19
+ /** OpenAI model for the chat playground; falls back to `OPENAI_MODEL` then {@link DEFAULT_CHAT_MODELS}. */
20
20
  readonly openaiModel?: string;
21
+ /** Anthropic model for the chat playground; falls back to `ANTHROPIC_MODEL`. */
22
+ readonly anthropicModel?: string;
23
+ /** Gemini model for the chat playground; falls back to `GEMINI_MODEL`. */
24
+ readonly geminiModel?: string;
21
25
  }
22
26
  export interface RpcLogEntry {
23
27
  readonly time: number;
@@ -1 +1 @@
1
- {"version":3,"file":"devtools-preview.d.ts","sourceRoot":"","sources":["../src/devtools-preview.ts"],"names":[],"mappings":"AAOA,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,cAAc,EACf,MAAM,uBAAuB,CAAC;AAE/B;;;;;;;;GAQG;AAEH,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,CAAC;AAE5C,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,cAAc,EAAE,CAAC;AAEzD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,0GAA0G;IAC1G,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAOD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC;IAChC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,sEAAsE;IACtE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,GAAG,EAAE,SAAS,WAAW,EAAE,CAAC;IACrC,4FAA4F;IAC5F,YAAY,IAAI,IAAI,CAAC;IACrB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AA8ED,wBAAsB,YAAY,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CA2UlF"}
1
+ {"version":3,"file":"devtools-preview.d.ts","sourceRoot":"","sources":["../src/devtools-preview.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,sBAAsB,EACtB,WAAW,EACX,gBAAgB,EAChB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,cAAc,EACf,MAAM,uBAAuB,CAAC;AAE/B;;;;;;;;GAQG;AAEH,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,CAAC;AAE5C,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,gBAAgB,EAAE,cAAc,EAAE,CAAC;AAEjF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,2GAA2G;IAC3G,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,gFAAgF;IAChF,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,0EAA0E;IAC1E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAOD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC;IAChC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,sEAAsE;IACtE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,GAAG,EAAE,SAAS,WAAW,EAAE,CAAC;IACrC,4FAA4F;IAC5F,YAAY,IAAI,IAAI,CAAC;IACrB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAuHD,wBAAsB,YAAY,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CAuclF"}
@@ -1,12 +1,41 @@
1
1
  import { createServer } from 'node:http';
2
- import { DEFAULT_CHAT_MODEL, runChatTurn, } from './devtools-chat.js';
3
- import { harnessHtml, openAiShimScript, wrapWidgetHtml, } from './devtools-harness.js';
2
+ import { DEFAULT_CHAT_MODELS, runProviderChatTurn, } from './devtools-chat.js';
3
+ import { calculateRangeProgress, harnessHtml, openAiShimScript, wrapWidgetHtml, } from './devtools-harness.js';
4
4
  // Re-exported so `devtools-preview` stays the single import surface for the command and tests.
5
- export { harnessHtml, openAiShimScript, wrapWidgetHtml };
5
+ export { calculateRangeProgress, harnessHtml, openAiShimScript, wrapWidgetHtml };
6
6
  const CHAT_SYSTEM_PROMPT = 'You are an assistant embedded in the Noodle Seed devtools playground. You can call the MCP tools ' +
7
7
  'exposed by the local server the developer is previewing. Call a tool whenever it can answer the ' +
8
8
  'request or render a widget, then explain the result briefly. Keep answers concise.';
9
9
  const DEFAULT_PROTOCOL_VERSION = '2025-11-25';
10
+ const CHAT_PROVIDERS = ['openai', 'anthropic', 'gemini'];
11
+ const CHAT_PROVIDER_CONFIG = {
12
+ openai: {
13
+ envName: 'OPENAI_API_KEY',
14
+ modelEnvName: 'OPENAI_MODEL',
15
+ baseUrlEnvName: 'OPENAI_BASE_URL',
16
+ },
17
+ anthropic: {
18
+ envName: 'ANTHROPIC_API_KEY',
19
+ modelEnvName: 'ANTHROPIC_MODEL',
20
+ baseUrlEnvName: 'ANTHROPIC_BASE_URL',
21
+ },
22
+ gemini: {
23
+ envName: 'GEMINI_API_KEY',
24
+ modelEnvName: 'GEMINI_MODEL',
25
+ baseUrlEnvName: 'GEMINI_BASE_URL',
26
+ },
27
+ };
28
+ function isChatProvider(value) {
29
+ return typeof value === 'string' && CHAT_PROVIDERS.includes(value);
30
+ }
31
+ /** Infer the provider from the API-key formats developers already recognize, without adding a picker. */
32
+ function detectChatProvider(key) {
33
+ if (key.startsWith('sk-ant-'))
34
+ return 'anthropic';
35
+ if (key.startsWith('AIza'))
36
+ return 'gemini';
37
+ return 'openai';
38
+ }
10
39
  /** Parse the `/widget?args=` query param (a JSON object) into tool-call arguments; `{}` on absent/invalid. */
11
40
  function parseArgsParam(raw) {
12
41
  if (!raw)
@@ -21,6 +50,18 @@ function parseArgsParam(raw) {
21
50
  return {};
22
51
  }
23
52
  }
53
+ /** Parse a JSON request body only when its top-level value is an object. */
54
+ function parseJsonObject(raw) {
55
+ try {
56
+ const parsed = JSON.parse(raw.toString('utf8'));
57
+ return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
58
+ ? parsed
59
+ : undefined;
60
+ }
61
+ catch {
62
+ return undefined;
63
+ }
64
+ }
24
65
  /** Cap a value for the inspector log so a huge tool result does not flood the SSE stream. */
25
66
  function capForLog(value, max = 20000) {
26
67
  if (value === undefined)
@@ -84,21 +125,64 @@ export async function startPreview(options) {
84
125
  const log = [];
85
126
  const subscribers = new Set();
86
127
  const reloadSubscribers = new Set();
87
- // The OpenAI key lives ONLY in this server process (session) or the env never in the browser. The
128
+ // Provider keys live ONLY in this server process (session) or the env, never in the browser. The
88
129
  // /widget iframes are same-origin and unsandboxed, so anything the page holds (localStorage or a JS var)
89
130
  // is readable by widget scripts and could be exfiltrated. The browser sets it via POST /chat/key and then
90
131
  // drops it; the gate state comes from GET /chat/status. The key is never logged or sent back to the page.
91
- let sessionApiKey;
92
- function resolveApiKey() {
93
- return sessionApiKey ?? process.env.OPENAI_API_KEY;
132
+ const sessionApiKeys = {};
133
+ const sessionModels = {};
134
+ let activeChatProvider;
135
+ function resolveApiKey(provider) {
136
+ return sessionApiKeys[provider] ?? process.env[CHAT_PROVIDER_CONFIG[provider].envName];
94
137
  }
95
- function apiKeySource() {
96
- if (sessionApiKey)
138
+ function apiKeySource(provider) {
139
+ if (sessionApiKeys[provider])
97
140
  return 'session';
98
- if (process.env.OPENAI_API_KEY)
141
+ if (process.env[CHAT_PROVIDER_CONFIG[provider].envName])
99
142
  return 'env';
100
143
  return 'none';
101
144
  }
145
+ function configuredModel(provider) {
146
+ const optionModel = provider === 'openai'
147
+ ? options.openaiModel
148
+ : provider === 'anthropic'
149
+ ? options.anthropicModel
150
+ : options.geminiModel;
151
+ return (sessionModels[provider] ??
152
+ optionModel ??
153
+ process.env[CHAT_PROVIDER_CONFIG[provider].modelEnvName] ??
154
+ DEFAULT_CHAT_MODELS[provider]);
155
+ }
156
+ function providerStatus(provider) {
157
+ return {
158
+ hasKey: Boolean(resolveApiKey(provider)),
159
+ source: apiKeySource(provider),
160
+ envName: CHAT_PROVIDER_CONFIG[provider].envName,
161
+ model: configuredModel(provider),
162
+ };
163
+ }
164
+ function configuredChatProviders() {
165
+ return CHAT_PROVIDERS.filter((provider) => providerStatus(provider).hasKey);
166
+ }
167
+ function resolveActiveChatProvider() {
168
+ if (activeChatProvider && providerStatus(activeChatProvider).hasKey) {
169
+ return activeChatProvider;
170
+ }
171
+ const configured = configuredChatProviders();
172
+ if (configured.length === 1)
173
+ return configured[0];
174
+ if (configured.length === 0)
175
+ return 'openai';
176
+ return undefined;
177
+ }
178
+ function ambiguousProviderError() {
179
+ if (resolveActiveChatProvider() !== undefined)
180
+ return undefined;
181
+ return {
182
+ code: 'ambiguous_provider',
183
+ message: 'Multiple provider keys are configured. Paste one API key to choose this chat session.',
184
+ };
185
+ }
102
186
  function record(entry) {
103
187
  log.push(entry);
104
188
  if (log.length > 500)
@@ -198,16 +282,18 @@ export async function startPreview(options) {
198
282
  });
199
283
  }
200
284
  /** Run one chat-playground turn against the local tools; the API key is read fresh and never logged. */
201
- async function handleChat(incoming, apiKey) {
285
+ async function handleChat(provider, incoming, apiKey, model) {
202
286
  const messages = incoming[0]?.role === 'system'
203
287
  ? incoming
204
288
  : [{ role: 'system', content: CHAT_SYSTEM_PROMPT }, ...incoming];
205
- return runChatTurn({
289
+ const baseUrl = process.env[CHAT_PROVIDER_CONFIG[provider].baseUrlEnvName];
290
+ return runProviderChatTurn({
291
+ provider,
206
292
  messages,
207
293
  tools: await chatToolDefs(),
208
294
  apiKey,
209
- model: options.openaiModel ?? process.env.OPENAI_MODEL ?? DEFAULT_CHAT_MODEL,
210
- ...(process.env.OPENAI_BASE_URL ? { baseUrl: process.env.OPENAI_BASE_URL } : {}),
295
+ model: model?.trim() || configuredModel(provider),
296
+ ...(baseUrl ? { baseUrl } : {}),
211
297
  callTool: async (name, args) => {
212
298
  const call = await forward({
213
299
  method: 'tools/call',
@@ -225,7 +311,7 @@ export async function startPreview(options) {
225
311
  const url = new URL(req.url ?? '/', 'http://localhost');
226
312
  if (req.method === 'GET' && url.pathname === '/') {
227
313
  res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
228
- res.end(harnessHtml({ ...options, hasServerKey: Boolean(resolveApiKey()) }));
314
+ res.end(harnessHtml(options));
229
315
  return;
230
316
  }
231
317
  if (req.method === 'GET' && url.pathname === '/widget') {
@@ -266,9 +352,16 @@ export async function startPreview(options) {
266
352
  return;
267
353
  }
268
354
  if (req.method === 'GET' && url.pathname === '/chat/status') {
269
- // Report only whether a key is available (and its source) never the value.
355
+ // Report only whether each key is available, plus non-secret configuration. Never return a key.
356
+ const activeProvider = resolveActiveChatProvider();
357
+ const error = ambiguousProviderError();
270
358
  res.writeHead(200, { 'content-type': 'application/json' });
271
- res.end(JSON.stringify({ hasKey: Boolean(resolveApiKey()), source: apiKeySource() }));
359
+ res.end(JSON.stringify({
360
+ activeProvider: activeProvider ?? null,
361
+ requiresKey: Boolean(error),
362
+ error,
363
+ providers: Object.fromEntries(CHAT_PROVIDERS.map((provider) => [provider, providerStatus(provider)])),
364
+ }));
272
365
  return;
273
366
  }
274
367
  if (req.method === 'POST' && url.pathname === '/chat/key') {
@@ -276,48 +369,101 @@ export async function startPreview(options) {
276
369
  const chunks = [];
277
370
  req.on('data', (c) => chunks.push(c));
278
371
  req.on('end', () => {
279
- let key = '';
280
- try {
281
- key = String(JSON.parse(Buffer.concat(chunks).toString('utf8')).key ?? '');
282
- }
283
- catch {
372
+ const body = parseJsonObject(Buffer.concat(chunks));
373
+ if (!body) {
284
374
  res.writeHead(400, { 'content-type': 'application/json' });
285
375
  res.end(JSON.stringify({ error: { code: 'invalid_json', message: 'invalid JSON' } }));
286
376
  return;
287
377
  }
288
- const trimmed = key.trim();
289
- sessionApiKey = trimmed === '' ? undefined : trimmed;
378
+ const submittedKey = typeof body.key === 'string' ? body.key.trim() : '';
379
+ const provider = submittedKey === ''
380
+ ? (body.provider ?? resolveActiveChatProvider())
381
+ : detectChatProvider(submittedKey);
382
+ const ambiguity = ambiguousProviderError();
383
+ if (submittedKey === '' && ambiguity) {
384
+ res.writeHead(409, { 'content-type': 'application/json' });
385
+ res.end(JSON.stringify({ error: ambiguity }));
386
+ return;
387
+ }
388
+ if (!isChatProvider(provider)) {
389
+ res.writeHead(400, { 'content-type': 'application/json' });
390
+ res.end(JSON.stringify({ error: { code: 'invalid_provider', message: 'invalid provider' } }));
391
+ return;
392
+ }
393
+ if (body.key !== undefined) {
394
+ if (submittedKey === '')
395
+ delete sessionApiKeys[provider];
396
+ else
397
+ sessionApiKeys[provider] = submittedKey;
398
+ }
399
+ const model = typeof body.model === 'string' ? body.model.trim() : '';
400
+ if (model)
401
+ sessionModels[provider] = model;
402
+ if (resolveApiKey(provider))
403
+ activeChatProvider = provider;
404
+ else if (activeChatProvider === provider)
405
+ activeChatProvider = undefined;
290
406
  res.writeHead(200, { 'content-type': 'application/json' });
291
- res.end(JSON.stringify({ ok: true, hasKey: Boolean(resolveApiKey()), source: apiKeySource() }));
407
+ res.end(JSON.stringify({ ok: true, provider, ...providerStatus(provider) }));
292
408
  });
293
409
  return;
294
410
  }
295
411
  if (req.method === 'POST' && url.pathname === '/chat') {
296
- // The key lives server-side (session key set via /chat/key, or the env). The browser never sends it.
297
- const apiKey = resolveApiKey();
298
- if (!apiKey) {
299
- res.writeHead(200, { 'content-type': 'application/json' });
300
- res.end(JSON.stringify({
301
- error: {
302
- code: 'no_api_key',
303
- message: 'No OpenAI key set. Add one in the Chat tab or set OPENAI_API_KEY.',
304
- },
305
- }));
306
- return;
307
- }
308
412
  const chunks = [];
309
413
  req.on('data', (c) => chunks.push(c));
310
414
  req.on('end', () => {
311
- let body;
312
- try {
313
- body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
314
- }
315
- catch {
415
+ const body = parseJsonObject(Buffer.concat(chunks));
416
+ if (!body) {
316
417
  res.writeHead(400, { 'content-type': 'application/json' });
317
418
  res.end(JSON.stringify({ error: { code: 'invalid_json', message: 'invalid JSON' } }));
318
419
  return;
319
420
  }
320
- handleChat(Array.isArray(body.messages) ? body.messages : [], apiKey)
421
+ const requestedProvider = body.provider;
422
+ if (requestedProvider !== undefined && !isChatProvider(requestedProvider)) {
423
+ res.writeHead(400, { 'content-type': 'application/json' });
424
+ res.end(JSON.stringify({ error: { code: 'invalid_provider', message: 'invalid provider' } }));
425
+ return;
426
+ }
427
+ const ambiguity = ambiguousProviderError();
428
+ if (ambiguity) {
429
+ res.writeHead(409, { 'content-type': 'application/json' });
430
+ res.end(JSON.stringify({ error: ambiguity }));
431
+ return;
432
+ }
433
+ const provider = resolveActiveChatProvider();
434
+ if (!provider) {
435
+ res.writeHead(409, { 'content-type': 'application/json' });
436
+ res.end(JSON.stringify({
437
+ error: {
438
+ code: 'ambiguous_provider',
439
+ message: 'Paste one API key to choose this chat session.',
440
+ },
441
+ }));
442
+ return;
443
+ }
444
+ if (requestedProvider !== undefined && requestedProvider !== provider) {
445
+ res.writeHead(409, { 'content-type': 'application/json' });
446
+ res.end(JSON.stringify({
447
+ error: {
448
+ code: 'provider_mismatch',
449
+ message: 'The chat provider does not match the connected API key.',
450
+ },
451
+ }));
452
+ return;
453
+ }
454
+ // The key and inferred provider live server-side. The browser can only use the bound provider.
455
+ const apiKey = resolveApiKey(provider);
456
+ if (!apiKey) {
457
+ res.writeHead(200, { 'content-type': 'application/json' });
458
+ res.end(JSON.stringify({
459
+ error: {
460
+ code: 'no_api_key',
461
+ message: `No key set for this provider. Add one in Chat or set ${CHAT_PROVIDER_CONFIG[provider].envName}.`,
462
+ },
463
+ }));
464
+ return;
465
+ }
466
+ handleChat(provider, Array.isArray(body.messages) ? body.messages : [], apiKey, typeof body.model === 'string' ? body.model : undefined)
321
467
  .then((result) => {
322
468
  res.writeHead(200, { 'content-type': 'application/json' });
323
469
  res.end(JSON.stringify(result));