@astralyn/pi 0.84.13 → 0.84.15

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 (202) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/core/agent-session.d.ts.map +1 -1
  3. package/dist/core/agent-session.js +12 -6
  4. package/dist/core/agent-session.js.map +1 -1
  5. package/dist/core/extensions/types.d.ts +4 -1
  6. package/dist/core/extensions/types.d.ts.map +1 -1
  7. package/dist/core/extensions/types.js.map +1 -1
  8. package/dist/core/tools/bash.d.ts +1 -0
  9. package/dist/core/tools/bash.d.ts.map +1 -1
  10. package/dist/core/tools/bash.js +4 -1
  11. package/dist/core/tools/bash.js.map +1 -1
  12. package/dist/core/tools/find.d.ts +2 -0
  13. package/dist/core/tools/find.d.ts.map +1 -1
  14. package/dist/core/tools/find.js +18 -15
  15. package/dist/core/tools/find.js.map +1 -1
  16. package/dist/extensions/background/actions.d.ts.map +1 -1
  17. package/dist/extensions/background/actions.js +3 -2
  18. package/dist/extensions/background/actions.js.map +1 -1
  19. package/dist/extensions/background/manager.d.ts.map +1 -1
  20. package/dist/extensions/background/manager.js +8 -9
  21. package/dist/extensions/background/manager.js.map +1 -1
  22. package/dist/extensions/background/registry.d.ts +4 -1
  23. package/dist/extensions/background/registry.d.ts.map +1 -1
  24. package/dist/extensions/background/registry.js +30 -17
  25. package/dist/extensions/background/registry.js.map +1 -1
  26. package/dist/extensions/background/task-view.d.ts +6 -0
  27. package/dist/extensions/background/task-view.d.ts.map +1 -1
  28. package/dist/extensions/background/task-view.js +11 -6
  29. package/dist/extensions/background/task-view.js.map +1 -1
  30. package/dist/extensions/deepwiki/client.d.ts.map +1 -1
  31. package/dist/extensions/deepwiki/client.js +3 -0
  32. package/dist/extensions/deepwiki/client.js.map +1 -1
  33. package/dist/extensions/deepwiki/execute.d.ts +2 -1
  34. package/dist/extensions/deepwiki/execute.d.ts.map +1 -1
  35. package/dist/extensions/deepwiki/execute.js +17 -2
  36. package/dist/extensions/deepwiki/execute.js.map +1 -1
  37. package/dist/extensions/deepwiki/http.d.ts +1 -0
  38. package/dist/extensions/deepwiki/http.d.ts.map +1 -1
  39. package/dist/extensions/deepwiki/http.js +16 -2
  40. package/dist/extensions/deepwiki/http.js.map +1 -1
  41. package/dist/extensions/deepwiki/schema.d.ts +1 -1
  42. package/dist/extensions/deepwiki/schema.d.ts.map +1 -1
  43. package/dist/extensions/deepwiki/schema.js +8 -2
  44. package/dist/extensions/deepwiki/schema.js.map +1 -1
  45. package/dist/extensions/question/dialog.d.ts.map +1 -1
  46. package/dist/extensions/question/dialog.js +80 -9
  47. package/dist/extensions/question/dialog.js.map +1 -1
  48. package/dist/extensions/question/index.d.ts.map +1 -1
  49. package/dist/extensions/question/index.js +8 -0
  50. package/dist/extensions/question/index.js.map +1 -1
  51. package/dist/extensions/question/schema.d.ts +2 -2
  52. package/dist/extensions/question/schema.d.ts.map +1 -1
  53. package/dist/extensions/question/schema.js +41 -11
  54. package/dist/extensions/question/schema.js.map +1 -1
  55. package/dist/extensions/question/types.d.ts +1 -1
  56. package/dist/extensions/question/types.d.ts.map +1 -1
  57. package/dist/extensions/question/types.js.map +1 -1
  58. package/dist/extensions/router/probe.d.ts.map +1 -1
  59. package/dist/extensions/router/probe.js +10 -29
  60. package/dist/extensions/router/probe.js.map +1 -1
  61. package/dist/extensions/router/register.d.ts.map +1 -1
  62. package/dist/extensions/router/register.js +16 -6
  63. package/dist/extensions/router/register.js.map +1 -1
  64. package/dist/extensions/router/store.d.ts.map +1 -1
  65. package/dist/extensions/router/store.js +108 -51
  66. package/dist/extensions/router/store.js.map +1 -1
  67. package/dist/extensions/router/ui.d.ts +6 -0
  68. package/dist/extensions/router/ui.d.ts.map +1 -1
  69. package/dist/extensions/router/ui.js +14 -6
  70. package/dist/extensions/router/ui.js.map +1 -1
  71. package/dist/extensions/statusline/index.d.ts.map +1 -1
  72. package/dist/extensions/statusline/index.js +40 -43
  73. package/dist/extensions/statusline/index.js.map +1 -1
  74. package/dist/extensions/subagent/activity.d.ts +7 -1
  75. package/dist/extensions/subagent/activity.d.ts.map +1 -1
  76. package/dist/extensions/subagent/activity.js +69 -6
  77. package/dist/extensions/subagent/activity.js.map +1 -1
  78. package/dist/extensions/subagent/agents-command.js +1 -1
  79. package/dist/extensions/subagent/agents-command.js.map +1 -1
  80. package/dist/extensions/subagent/agents.d.ts.map +1 -1
  81. package/dist/extensions/subagent/agents.js +0 -3
  82. package/dist/extensions/subagent/agents.js.map +1 -1
  83. package/dist/extensions/subagent/budget.d.ts.map +1 -1
  84. package/dist/extensions/subagent/budget.js +29 -2
  85. package/dist/extensions/subagent/budget.js.map +1 -1
  86. package/dist/extensions/subagent/cancellation.d.ts +4 -4
  87. package/dist/extensions/subagent/cancellation.d.ts.map +1 -1
  88. package/dist/extensions/subagent/cancellation.js +31 -13
  89. package/dist/extensions/subagent/cancellation.js.map +1 -1
  90. package/dist/extensions/subagent/constants.d.ts +1 -0
  91. package/dist/extensions/subagent/constants.d.ts.map +1 -1
  92. package/dist/extensions/subagent/constants.js +1 -0
  93. package/dist/extensions/subagent/constants.js.map +1 -1
  94. package/dist/extensions/subagent/index.d.ts.map +1 -1
  95. package/dist/extensions/subagent/index.js +1 -0
  96. package/dist/extensions/subagent/index.js.map +1 -1
  97. package/dist/extensions/subagent/render.d.ts +8 -1
  98. package/dist/extensions/subagent/render.d.ts.map +1 -1
  99. package/dist/extensions/subagent/render.js +148 -178
  100. package/dist/extensions/subagent/render.js.map +1 -1
  101. package/dist/extensions/subagent/runner.d.ts +1 -0
  102. package/dist/extensions/subagent/runner.d.ts.map +1 -1
  103. package/dist/extensions/subagent/runner.js +53 -44
  104. package/dist/extensions/subagent/runner.js.map +1 -1
  105. package/dist/extensions/subagent/sdk-runner.d.ts.map +1 -1
  106. package/dist/extensions/subagent/sdk-runner.js +0 -3
  107. package/dist/extensions/subagent/sdk-runner.js.map +1 -1
  108. package/dist/extensions/subagent/settings.d.ts +1 -1
  109. package/dist/extensions/subagent/settings.d.ts.map +1 -1
  110. package/dist/extensions/subagent/settings.js +74 -25
  111. package/dist/extensions/subagent/settings.js.map +1 -1
  112. package/dist/extensions/subagent/state.d.ts +1 -0
  113. package/dist/extensions/subagent/state.d.ts.map +1 -1
  114. package/dist/extensions/subagent/state.js +46 -16
  115. package/dist/extensions/subagent/state.js.map +1 -1
  116. package/dist/extensions/subagent/types.d.ts +0 -7
  117. package/dist/extensions/subagent/types.d.ts.map +1 -1
  118. package/dist/extensions/subagent/types.js.map +1 -1
  119. package/dist/extensions/subagent/ui/agents-dialog.js +1 -1
  120. package/dist/extensions/subagent/ui/agents-dialog.js.map +1 -1
  121. package/dist/extensions/subagent/ui/model-list.d.ts.map +1 -1
  122. package/dist/extensions/subagent/ui/model-list.js +3 -4
  123. package/dist/extensions/subagent/ui/model-list.js.map +1 -1
  124. package/dist/extensions/subagent/ui/model-picker.d.ts.map +1 -1
  125. package/dist/extensions/subagent/ui/model-picker.js +2 -1
  126. package/dist/extensions/subagent/ui/model-picker.js.map +1 -1
  127. package/dist/extensions/web-search/constants.d.ts +2 -2
  128. package/dist/extensions/web-search/constants.d.ts.map +1 -1
  129. package/dist/extensions/web-search/constants.js +4 -5
  130. package/dist/extensions/web-search/constants.js.map +1 -1
  131. package/dist/extensions/web-search/execute.d.ts +13 -0
  132. package/dist/extensions/web-search/execute.d.ts.map +1 -0
  133. package/dist/extensions/web-search/execute.js +101 -0
  134. package/dist/extensions/web-search/execute.js.map +1 -0
  135. package/dist/extensions/web-search/format.d.ts +12 -0
  136. package/dist/extensions/web-search/format.d.ts.map +1 -0
  137. package/dist/extensions/web-search/format.js +109 -0
  138. package/dist/extensions/web-search/format.js.map +1 -0
  139. package/dist/extensions/web-search/index.d.ts.map +1 -1
  140. package/dist/extensions/web-search/index.js +12 -9
  141. package/dist/extensions/web-search/index.js.map +1 -1
  142. package/dist/extensions/web-search/providers/deepseek.d.ts +0 -2
  143. package/dist/extensions/web-search/providers/deepseek.d.ts.map +1 -1
  144. package/dist/extensions/web-search/providers/deepseek.js +82 -34
  145. package/dist/extensions/web-search/providers/deepseek.js.map +1 -1
  146. package/dist/extensions/web-search/providers/http.d.ts +2 -7
  147. package/dist/extensions/web-search/providers/http.d.ts.map +1 -1
  148. package/dist/extensions/web-search/providers/http.js +30 -13
  149. package/dist/extensions/web-search/providers/http.js.map +1 -1
  150. package/dist/extensions/web-search/providers/minimax.d.ts +0 -2
  151. package/dist/extensions/web-search/providers/minimax.d.ts.map +1 -1
  152. package/dist/extensions/web-search/providers/minimax.js +43 -50
  153. package/dist/extensions/web-search/providers/minimax.js.map +1 -1
  154. package/dist/extensions/web-search/render.d.ts +1 -2
  155. package/dist/extensions/web-search/render.d.ts.map +1 -1
  156. package/dist/extensions/web-search/render.js +22 -17
  157. package/dist/extensions/web-search/render.js.map +1 -1
  158. package/dist/extensions/web-search/results.d.ts +35 -0
  159. package/dist/extensions/web-search/results.d.ts.map +1 -0
  160. package/dist/extensions/web-search/results.js +172 -0
  161. package/dist/extensions/web-search/results.js.map +1 -0
  162. package/dist/extensions/web-search/schema.d.ts +0 -2
  163. package/dist/extensions/web-search/schema.d.ts.map +1 -1
  164. package/dist/extensions/web-search/schema.js +4 -25
  165. package/dist/extensions/web-search/schema.js.map +1 -1
  166. package/dist/extensions/web-search/types.d.ts +0 -62
  167. package/dist/extensions/web-search/types.d.ts.map +1 -1
  168. package/dist/extensions/web-search/types.js.map +1 -1
  169. package/dist/modes/interactive/components/tool-execution.d.ts +1 -0
  170. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  171. package/dist/modes/interactive/components/tool-execution.js +7 -0
  172. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  173. package/dist/modes/interactive/components/tool-group.d.ts.map +1 -1
  174. package/dist/modes/interactive/components/tool-group.js +10 -0
  175. package/dist/modes/interactive/components/tool-group.js.map +1 -1
  176. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  177. package/dist/modes/interactive/interactive-mode.js +3 -40
  178. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  179. package/dist/modes/interactive/theme/theme.d.ts +0 -4
  180. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  181. package/dist/modes/interactive/theme/theme.js +16 -23
  182. package/dist/modes/interactive/theme/theme.js.map +1 -1
  183. package/dist/utils/http-response.d.ts +10 -0
  184. package/dist/utils/http-response.d.ts.map +1 -0
  185. package/dist/utils/http-response.js +68 -0
  186. package/dist/utils/http-response.js.map +1 -0
  187. package/docs/bundled/extensions/background.md +2 -1
  188. package/docs/bundled/extensions/deepwiki.md +7 -5
  189. package/docs/bundled/extensions/question.md +6 -4
  190. package/docs/bundled/extensions/router.md +1 -1
  191. package/docs/bundled/extensions/statusline.md +1 -0
  192. package/docs/bundled/extensions/subagent.md +29 -39
  193. package/docs/bundled/extensions/web-search.md +8 -12
  194. package/docs/development.md +7 -2
  195. package/docs/docs.json +4 -0
  196. package/docs/extensions.md +2 -2
  197. package/npm-shrinkwrap.json +2 -2
  198. package/package.json +1 -1
  199. package/dist/extensions/web-search/fusion.d.ts +0 -36
  200. package/dist/extensions/web-search/fusion.d.ts.map +0 -1
  201. package/dist/extensions/web-search/fusion.js +0 -272
  202. package/dist/extensions/web-search/fusion.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/extensions/question/schema.ts"],"names":[],"mappings":"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","sourcesContent":["import { Type } from \"typebox\";\nimport { QUESTION_LIMITS } from \"./limits.ts\";\nimport type { Question, QuestionOption, QuestionToolError } from \"./types.ts\";\n\nconst MIN_OPTIONS = 2;\nconst MAX_OPTIONS = 4;\nconst MAX_QUESTIONS = 4;\nconst RESERVED_LABELS = [\"Other\", \"Type something\", \"Type something.\", \"Chat about this\"] as const;\n\nconst normalizeLabel = (text: string) => text.trim().toLowerCase();\nconst RESERVED_NORMALIZED = new Set(RESERVED_LABELS.map(normalizeLabel));\n\nconst OptionSchema = Type.Object({\n\tlabel: Type.String({\n\t\tmaxLength: QUESTION_LIMITS.optionLabelChars,\n\t\tdescription:\n\t\t\t\"Short user-facing option label (1-5 words), distinct within the question. Reserved labels ('Other', 'Type something', 'Chat about this') are rejected — the UI adds the custom-answer path itself.\",\n\t}),\n\tdescription: Type.String({\n\t\tmaxLength: QUESTION_LIMITS.optionDescriptionChars,\n\t\tdescription: \"One concise sentence explaining the option's meaning, consequence, or tradeoff.\",\n\t}),\n\tpreview: Type.Optional(\n\t\tType.String({\n\t\t\tmaxLength: QUESTION_LIMITS.previewChars,\n\t\t\tdescription:\n\t\t\t\t\"Optional markdown preview shown only for focused single-select options; use for concrete snippets, layouts, copy, or config comparisons.\",\n\t\t}),\n\t),\n});\n\nconst QuestionSchema = Type.Object({\n\tquestion: Type.String({\n\t\tmaxLength: QUESTION_LIMITS.questionChars,\n\t\tdescription:\n\t\t\t\"One clear, specific decision or preference question ending in a question mark. Ask only what is needed to proceed.\",\n\t}),\n\theader: Type.String({\n\t\tmaxLength: QUESTION_LIMITS.headerChars,\n\t\tdescription: \"Very short decision label shown as a chip; aim for ~12 chars, max 32. E.g. 'Auth method'.\",\n\t}),\n\toptions: Type.Array(OptionSchema, {\n\t\tminItems: MIN_OPTIONS,\n\t\tmaxItems: MAX_OPTIONS,\n\t\tdescription: \"Mutually distinct options for this decision; do not include reserved custom-answer labels.\",\n\t}),\n\tmultiSelect: Type.Optional(\n\t\tType.Boolean({\n\t\t\tdescription:\n\t\t\t\t\"Allow selecting multiple options only when choices can be combined. Defaults to false for mutually exclusive decisions.\",\n\t\t}),\n\t),\n});\n\nexport const QuestionParams = Type.Object({\n\tquestions: Type.Array(QuestionSchema, {\n\t\tminItems: 1,\n\t\tmaxItems: MAX_QUESTIONS,\n\t\tdescription: \"Related decisions to ask now (1-4); ask only what is needed to unblock progress.\",\n\t}),\n});\n\n/**\n * Checks the schema cannot express. Length and count limits are omitted:\n * the agent core validates params against `QuestionParams` before execute runs.\n */\nexport function validateQuestions(\n\tquestions: Question[],\n): { ok: true } | { ok: false; error: QuestionToolError; message: string } {\n\tconst seenQuestions = new Set<string>();\n\tfor (const q of questions) {\n\t\tconst questionKey = normalizeLabel(q.question);\n\t\tif (seenQuestions.has(questionKey)) {\n\t\t\treturn { ok: false, error: \"duplicate_question\", message: \"Question text must be unique\" };\n\t\t}\n\t\tseenQuestions.add(questionKey);\n\n\t\tif (q.multiSelect && q.options.some((option) => option.preview)) {\n\t\t\treturn {\n\t\t\t\tok: false,\n\t\t\t\terror: \"preview_multiselect\",\n\t\t\t\tmessage:\n\t\t\t\t\t\"Option previews are not supported on multiSelect questions; drop the previews or make the question single-select\",\n\t\t\t};\n\t\t}\n\n\t\tconst seenLabels = new Set<string>();\n\t\tfor (const option of q.options as QuestionOption[]) {\n\t\t\tconst labelKey = normalizeLabel(option.label);\n\t\t\tif (RESERVED_NORMALIZED.has(labelKey)) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\terror: \"reserved_label\",\n\t\t\t\t\tmessage: `Option label is reserved (${RESERVED_LABELS.join(\", \")})`,\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (seenLabels.has(labelKey)) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\terror: \"duplicate_option_label\",\n\t\t\t\t\tmessage: \"Option labels must be unique within a question\",\n\t\t\t\t};\n\t\t\t}\n\t\t\tseenLabels.add(labelKey);\n\t\t}\n\t}\n\n\treturn { ok: true };\n}\n"]}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/extensions/question/schema.ts"],"names":[],"mappings":"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","sourcesContent":["import { Type } from \"typebox\";\nimport { QUESTION_LIMITS } from \"./limits.ts\";\nimport type { Question, QuestionOption, QuestionToolError } from \"./types.ts\";\n\nconst MIN_OPTIONS = 2;\nconst MAX_OPTIONS = 4;\nconst MAX_QUESTIONS = 4;\nconst RESERVED_LABELS = [\"Other\", \"Type something\", \"Type something.\", \"Chat about this\"] as const;\n\nconst normalizeLabel = (text: string) => text.trim().toLowerCase();\nconst RESERVED_NORMALIZED = new Set(RESERVED_LABELS.map(normalizeLabel));\n\nconst OptionSchema = Type.Object({\n\tlabel: Type.String({\n\t\tminLength: 1,\n\t\tmaxLength: QUESTION_LIMITS.optionLabelChars,\n\t\tdescription:\n\t\t\t\"Short user-facing option label (1-5 words), distinct within the question. Reserved labels ('Other', 'Type something', 'Chat about this') are rejected — the UI adds the custom-answer path itself.\",\n\t}),\n\tdescription: Type.String({\n\t\tminLength: 1,\n\t\tmaxLength: QUESTION_LIMITS.optionDescriptionChars,\n\t\tdescription: \"One concise sentence explaining the option's meaning, consequence, or tradeoff.\",\n\t}),\n\tpreview: Type.Optional(\n\t\tType.String({\n\t\t\tminLength: 1,\n\t\t\tmaxLength: QUESTION_LIMITS.previewChars,\n\t\t\tdescription:\n\t\t\t\t\"Optional markdown preview shown only for focused single-select options; use for concrete snippets, layouts, copy, or config comparisons.\",\n\t\t}),\n\t),\n});\n\nconst QuestionSchema = Type.Object({\n\tquestion: Type.String({\n\t\tminLength: 1,\n\t\tmaxLength: QUESTION_LIMITS.questionChars,\n\t\tdescription:\n\t\t\t\"One clear, specific decision or preference question ending in a question mark. Ask only what is needed to proceed.\",\n\t}),\n\theader: Type.String({\n\t\tminLength: 1,\n\t\tmaxLength: QUESTION_LIMITS.headerChars,\n\t\tdescription: \"Very short decision label shown as a chip; aim for ~12 chars, max 32. E.g. 'Auth method'.\",\n\t}),\n\toptions: Type.Array(OptionSchema, {\n\t\tminItems: MIN_OPTIONS,\n\t\tmaxItems: MAX_OPTIONS,\n\t\tdescription: \"Mutually distinct options for this decision; do not include reserved custom-answer labels.\",\n\t}),\n\tmultiSelect: Type.Optional(\n\t\tType.Boolean({\n\t\t\tdescription:\n\t\t\t\t\"Allow selecting multiple options only when choices can be combined. Defaults to false for mutually exclusive decisions.\",\n\t\t}),\n\t),\n});\n\nexport const QuestionParams = Type.Object({\n\tquestions: Type.Array(QuestionSchema, {\n\t\tminItems: 1,\n\t\tmaxItems: MAX_QUESTIONS,\n\t\tdescription: \"Related decisions to ask now (1-4); ask only what is needed to unblock progress.\",\n\t}),\n});\n\n/**\n * Checks visible non-whitespace text and cross-field rules the schema cannot\n * express. Length and count limits remain enforced by `QuestionParams` first.\n */\nexport function validateQuestions(\n\tquestions: Question[],\n): { ok: true } | { ok: false; error: QuestionToolError; message: string } {\n\tconst requireNonBlank = (\n\t\tvalue: unknown,\n\t\tpath: string,\n\t): { ok: false; error: QuestionToolError; message: string } | undefined => {\n\t\tif (typeof value === \"string\" && value.trim().length > 0) return undefined;\n\t\treturn { ok: false, error: \"blank_text\", message: `${path} must contain visible text` };\n\t};\n\n\tconst seenQuestions = new Set<string>();\n\tfor (let questionIndex = 0; questionIndex < questions.length; questionIndex++) {\n\t\tconst q = questions[questionIndex];\n\t\tconst questionError = requireNonBlank(q.question, `questions[${questionIndex}].question`);\n\t\tif (questionError) return questionError;\n\t\tconst headerError = requireNonBlank(q.header, `questions[${questionIndex}].header`);\n\t\tif (headerError) return headerError;\n\n\t\tconst questionKey = normalizeLabel(q.question);\n\t\tif (seenQuestions.has(questionKey)) {\n\t\t\treturn { ok: false, error: \"duplicate_question\", message: \"Question text must be unique\" };\n\t\t}\n\t\tseenQuestions.add(questionKey);\n\n\t\tconst seenLabels = new Set<string>();\n\t\tfor (let optionIndex = 0; optionIndex < q.options.length; optionIndex++) {\n\t\t\tconst option = q.options[optionIndex] as QuestionOption;\n\t\t\tconst optionPath = `questions[${questionIndex}].options[${optionIndex}]`;\n\t\t\tconst labelError = requireNonBlank(option.label, `${optionPath}.label`);\n\t\t\tif (labelError) return labelError;\n\t\t\tconst descriptionError = requireNonBlank(option.description, `${optionPath}.description`);\n\t\t\tif (descriptionError) return descriptionError;\n\t\t\tif (option.preview !== undefined) {\n\t\t\t\tconst previewError = requireNonBlank(option.preview, `${optionPath}.preview`);\n\t\t\t\tif (previewError) return previewError;\n\t\t\t}\n\n\t\t\tconst labelKey = normalizeLabel(option.label);\n\t\t\tif (RESERVED_NORMALIZED.has(labelKey)) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\terror: \"reserved_label\",\n\t\t\t\t\tmessage: `Option label is reserved (${RESERVED_LABELS.join(\", \")})`,\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (seenLabels.has(labelKey)) {\n\t\t\t\treturn {\n\t\t\t\t\tok: false,\n\t\t\t\t\terror: \"duplicate_option_label\",\n\t\t\t\t\tmessage: \"Option labels must be unique within a question\",\n\t\t\t\t};\n\t\t\t}\n\t\t\tseenLabels.add(labelKey);\n\t\t}\n\n\t\tif (q.multiSelect && q.options.some((option) => option.preview)) {\n\t\t\treturn {\n\t\t\t\tok: false,\n\t\t\t\terror: \"preview_multiselect\",\n\t\t\t\tmessage:\n\t\t\t\t\t\"Option previews are not supported on multiSelect questions; drop the previews or make the question single-select\",\n\t\t\t};\n\t\t}\n\t}\n\n\treturn { ok: true };\n}\n"]}
@@ -10,7 +10,7 @@ export interface Question {
10
10
  multiSelect?: boolean;
11
11
  }
12
12
  export type InputMode = "custom" | "notes" | undefined;
13
- export type QuestionToolError = "no_ui" | "duplicate_question" | "duplicate_option_label" | "reserved_label" | "preview_multiselect";
13
+ export type QuestionToolError = "no_ui" | "blank_text" | "duplicate_question" | "duplicate_option_label" | "reserved_label" | "preview_multiselect";
14
14
  export interface AnswerNote {
15
15
  option: string;
16
16
  text: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/extensions/question/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,WAAW,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC;AAEvD,MAAM,MAAM,iBAAiB,GAC1B,OAAO,GACP,oBAAoB,GACpB,wBAAwB,GACxB,gBAAgB,GAChB,qBAAqB,CAAC;AAEzB,MAAM,WAAW,UAAU;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,cAAc;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;IACpC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,WAAW,GAAG,qBAAqB,GAAG,OAAO,CAAC;AAEzF,MAAM,WAAW,mBAAmB;IACnC,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,OAAO,EAAE,eAAe,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,cAAc,CAAC;IAC9B,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC5B,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,OAAO,EAAE,eAAe,CAAC;CACzB;AAED,MAAM,MAAM,aAAa,GACtB,CAAC,cAAc,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC,GAC1D;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpC,eAAO,MAAM,YAAY,EAAE,aAA0D,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/extensions/question/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,WAAW,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC;AAEvD,MAAM,MAAM,iBAAiB,GAC1B,OAAO,GACP,YAAY,GACZ,oBAAoB,GACpB,wBAAwB,GACxB,gBAAgB,GAChB,qBAAqB,CAAC;AAEzB,MAAM,WAAW,UAAU;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,cAAc;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;IACpC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,WAAW,GAAG,qBAAqB,GAAG,OAAO,CAAC;AAEzF,MAAM,WAAW,mBAAmB;IACnC,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,OAAO,EAAE,eAAe,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,cAAc,CAAC;IAC9B,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC5B,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,OAAO,EAAE,eAAe,CAAC;CACzB;AAED,MAAM,MAAM,aAAa,GACtB,CAAC,cAAc,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC,GAC1D;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpC,eAAO,MAAM,YAAY,EAAE,aAA0D,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/extensions/question/types.ts"],"names":[],"mappings":"AAuEA,MAAM,CAAC,MAAM,YAAY,GAAkB,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC","sourcesContent":["export interface QuestionOption {\n\tlabel: string;\n\tdescription: string;\n\tpreview?: string;\n}\n\nexport interface Question {\n\tquestion: string;\n\theader: string;\n\toptions: QuestionOption[];\n\tmultiSelect?: boolean;\n}\n\nexport type InputMode = \"custom\" | \"notes\" | undefined;\n\nexport type QuestionToolError =\n\t| \"no_ui\"\n\t| \"duplicate_question\"\n\t| \"duplicate_option_label\"\n\t| \"reserved_label\"\n\t| \"preview_multiselect\";\n\nexport interface AnswerNote {\n\toption: string;\n\ttext: string;\n}\n\nexport interface QuestionAnswer {\n\tquestionIndex: number;\n\tquestion: string;\n\theader: string;\n\tkind: \"option\" | \"custom\" | \"multi\";\n\tanswer: string | null;\n\tselected?: string[];\n\tnotes?: AnswerNote[];\n\tpreview?: string;\n}\n\nexport type QuestionOutcome = \"answered\" | \"cancelled\" | \"needs_clarification\" | \"error\";\n\nexport interface QuestionToolDetails {\n\tanswers: QuestionAnswer[];\n\toutcome: QuestionOutcome;\n\tcancelled: boolean;\n\terror?: QuestionToolError;\n\tmessage?: string;\n}\n\nexport interface CustomAnswer {\n\ttext: string;\n\tselected: boolean;\n}\n\nexport interface QuestionState {\n\toptionIndex: number;\n\tsingleAnswer?: QuestionAnswer;\n\tmultiSelected: Set<number>;\n\tcustomAnswer?: CustomAnswer;\n\tnotesByOption: Map<string, string>;\n\twarning?: string;\n}\n\nexport interface DialogResult {\n\tanswers: QuestionAnswer[];\n\toutcome: QuestionOutcome;\n}\n\nexport type DisplayOption =\n\t| (QuestionOption & { kind: \"option\"; optionIndex: number })\n\t| { kind: \"other\"; label: string };\n\nexport const OTHER_OPTION: DisplayOption = { kind: \"other\", label: \"Type something\" };\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/extensions/question/types.ts"],"names":[],"mappings":"AAwEA,MAAM,CAAC,MAAM,YAAY,GAAkB,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC","sourcesContent":["export interface QuestionOption {\n\tlabel: string;\n\tdescription: string;\n\tpreview?: string;\n}\n\nexport interface Question {\n\tquestion: string;\n\theader: string;\n\toptions: QuestionOption[];\n\tmultiSelect?: boolean;\n}\n\nexport type InputMode = \"custom\" | \"notes\" | undefined;\n\nexport type QuestionToolError =\n\t| \"no_ui\"\n\t| \"blank_text\"\n\t| \"duplicate_question\"\n\t| \"duplicate_option_label\"\n\t| \"reserved_label\"\n\t| \"preview_multiselect\";\n\nexport interface AnswerNote {\n\toption: string;\n\ttext: string;\n}\n\nexport interface QuestionAnswer {\n\tquestionIndex: number;\n\tquestion: string;\n\theader: string;\n\tkind: \"option\" | \"custom\" | \"multi\";\n\tanswer: string | null;\n\tselected?: string[];\n\tnotes?: AnswerNote[];\n\tpreview?: string;\n}\n\nexport type QuestionOutcome = \"answered\" | \"cancelled\" | \"needs_clarification\" | \"error\";\n\nexport interface QuestionToolDetails {\n\tanswers: QuestionAnswer[];\n\toutcome: QuestionOutcome;\n\tcancelled: boolean;\n\terror?: QuestionToolError;\n\tmessage?: string;\n}\n\nexport interface CustomAnswer {\n\ttext: string;\n\tselected: boolean;\n}\n\nexport interface QuestionState {\n\toptionIndex: number;\n\tsingleAnswer?: QuestionAnswer;\n\tmultiSelected: Set<number>;\n\tcustomAnswer?: CustomAnswer;\n\tnotesByOption: Map<string, string>;\n\twarning?: string;\n}\n\nexport interface DialogResult {\n\tanswers: QuestionAnswer[];\n\toutcome: QuestionOutcome;\n}\n\nexport type DisplayOption =\n\t| (QuestionOption & { kind: \"option\"; optionIndex: number })\n\t| { kind: \"other\"; label: string };\n\nexport const OTHER_OPTION: DisplayOption = { kind: \"other\", label: \"Type something\" };\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../../../src/extensions/router/probe.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAIlE,MAAM,WAAW,UAAU;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,WAAW,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,UAAU,EAAE,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,WAAW,CAAC,CA0DvB"}
1
+ {"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../../../src/extensions/router/probe.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAKlE,MAAM,WAAW,UAAU;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,WAAW,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,UAAU,EAAE,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,WAAW,CAAC,CAiEvB"}
@@ -1,4 +1,5 @@
1
1
  /** Fetch OpenAI-compatible model catalog from a relay baseUrl. */
2
+ import { readResponseTextBounded } from "../../utils/http-response.js";
2
3
  import { DEFAULTS, formatError } from "./constants.js";
3
4
  export async function probeRelayModels(opts) {
4
5
  let baseUrl;
@@ -25,13 +26,20 @@ export async function probeRelayModels(opts) {
25
26
  headers.Authorization = `Bearer ${opts.apiKey}`;
26
27
  const response = await fetch(url, { headers, signal: controller.signal });
27
28
  if (!response.ok) {
28
- const body = await readTextBounded(response, 4_096);
29
+ const body = await readResponseTextBounded(response, {
30
+ maxBytes: 4_096,
31
+ signal: controller.signal,
32
+ });
29
33
  return {
30
34
  ok: false,
31
35
  error: `HTTP ${response.status}${body ? `: ${body.slice(0, 400)}` : ""}`,
32
36
  };
33
37
  }
34
- const text = await readTextBounded(response, DEFAULTS.probeBodyBytes);
38
+ const text = await readResponseTextBounded(response, {
39
+ maxBytes: DEFAULTS.probeBodyBytes,
40
+ overflowMessage: `Response exceeds ${DEFAULTS.probeBodyBytes} bytes.`,
41
+ signal: controller.signal,
42
+ });
35
43
  let json;
36
44
  try {
37
45
  json = JSON.parse(text);
@@ -97,31 +105,4 @@ function dedupeSort(models) {
97
105
  }
98
106
  return [...map.values()].sort((a, b) => a.id.localeCompare(b.id));
99
107
  }
100
- async function readTextBounded(response, maxBytes) {
101
- const reader = response.body?.getReader();
102
- if (!reader)
103
- return await response.text();
104
- const chunks = [];
105
- let total = 0;
106
- while (true) {
107
- const { done, value } = await reader.read();
108
- if (done)
109
- break;
110
- if (!value)
111
- continue;
112
- total += value.byteLength;
113
- if (total > maxBytes) {
114
- await reader.cancel();
115
- throw new Error(`Response exceeds ${maxBytes} bytes.`);
116
- }
117
- chunks.push(value);
118
- }
119
- const merged = new Uint8Array(total);
120
- let offset = 0;
121
- for (const chunk of chunks) {
122
- merged.set(chunk, offset);
123
- offset += chunk.byteLength;
124
- }
125
- return new TextDecoder().decode(merged);
126
- }
127
108
  //# sourceMappingURL=probe.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"probe.js","sourceRoot":"","sources":["../../../src/extensions/router/probe.ts"],"names":[],"mappings":"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","sourcesContent":["/** Fetch OpenAI-compatible model catalog from a relay baseUrl. */\n\nimport { DEFAULTS, formatError } from \"./constants.ts\";\n\nexport interface ProbeModel {\n\tid: string;\n\tname?: string;\n}\n\nexport type ProbeResult = { ok: true; models: ProbeModel[]; truncated: boolean } | { ok: false; error: string };\n\nexport async function probeRelayModels(opts: {\n\tbaseUrl: string;\n\tapiKey?: string;\n\tsignal?: AbortSignal;\n\ttimeoutMs?: number;\n}): Promise<ProbeResult> {\n\tlet baseUrl: URL;\n\ttry {\n\t\tbaseUrl = new URL(opts.baseUrl);\n\t} catch {\n\t\treturn { ok: false, error: \"Base URL is not a valid URL.\" };\n\t}\n\tif (baseUrl.protocol !== \"http:\" && baseUrl.protocol !== \"https:\") {\n\t\treturn { ok: false, error: `Unsupported protocol: ${baseUrl.protocol}` };\n\t}\n\n\tif (opts.signal?.aborted) return { ok: false, error: \"Cancelled.\" };\n\tconst controller = new AbortController();\n\tconst timeoutMs = opts.timeoutMs ?? DEFAULTS.probeTimeoutMs;\n\tconst timer = setTimeout(() => controller.abort(), timeoutMs);\n\tconst onOuterAbort = () => controller.abort();\n\topts.signal?.addEventListener(\"abort\", onOuterAbort, { once: true });\n\n\ttry {\n\t\tconst url = appendPath(baseUrl, \"models\");\n\t\tconst headers: Record<string, string> = { Accept: \"application/json\" };\n\t\tif (opts.apiKey) headers.Authorization = `Bearer ${opts.apiKey}`;\n\n\t\tconst response = await fetch(url, { headers, signal: controller.signal });\n\t\tif (!response.ok) {\n\t\t\tconst body = await readTextBounded(response, 4_096);\n\t\t\treturn {\n\t\t\t\tok: false,\n\t\t\t\terror: `HTTP ${response.status}${body ? `: ${body.slice(0, 400)}` : \"\"}`,\n\t\t\t};\n\t\t}\n\t\tconst text = await readTextBounded(response, DEFAULTS.probeBodyBytes);\n\t\tlet json: unknown;\n\t\ttry {\n\t\t\tjson = JSON.parse(text);\n\t\t} catch {\n\t\t\treturn { ok: false, error: \"Model catalog response is not JSON.\" };\n\t\t}\n\t\tconst models = parseOpenAIModels(json);\n\t\tif (models === null) {\n\t\t\treturn { ok: false, error: \"JSON has no OpenAI-style `data` array of model ids.\" };\n\t\t}\n\t\tconst sorted = dedupeSort(models);\n\t\tconst truncated = sorted.length > DEFAULTS.probeMaxModels;\n\t\treturn {\n\t\t\tok: true,\n\t\t\tmodels: sorted.slice(0, DEFAULTS.probeMaxModels),\n\t\t\ttruncated,\n\t\t};\n\t} catch (error) {\n\t\tif (controller.signal.aborted) {\n\t\t\treturn { ok: false, error: opts.signal?.aborted ? \"Cancelled.\" : `Timed out after ${timeoutMs}ms.` };\n\t\t}\n\t\treturn { ok: false, error: formatError(error) };\n\t} finally {\n\t\tclearTimeout(timer);\n\t\topts.signal?.removeEventListener(\"abort\", onOuterAbort);\n\t}\n}\n\nfunction appendPath(base: URL, segment: string): URL {\n\tconst url = new URL(base.href);\n\tconst path = url.pathname.replace(/\\/+$/, \"\");\n\turl.pathname = `${path}/${segment}`;\n\treturn url;\n}\n\nfunction parseOpenAIModels(json: unknown): ProbeModel[] | null {\n\tif (!json || typeof json !== \"object\") return null;\n\tconst data = (json as { data?: unknown }).data;\n\tif (!Array.isArray(data)) return null;\n\tconst models: ProbeModel[] = [];\n\tfor (const item of data) {\n\t\tif (!item || typeof item !== \"object\") continue;\n\t\tconst record = item as Record<string, unknown>;\n\t\tconst id = typeof record.id === \"string\" ? record.id.trim() : \"\";\n\t\tif (!id) continue;\n\t\tconst name =\n\t\t\ttypeof record.name === \"string\" && record.name.trim() && record.name.trim() !== id\n\t\t\t\t? record.name.trim()\n\t\t\t\t: undefined;\n\t\tmodels.push(name ? { id, name } : { id });\n\t}\n\treturn models;\n}\n\nfunction dedupeSort(models: ProbeModel[]): ProbeModel[] {\n\tconst map = new Map<string, ProbeModel>();\n\tfor (const model of models) {\n\t\tif (!map.has(model.id)) map.set(model.id, model);\n\t}\n\treturn [...map.values()].sort((a, b) => a.id.localeCompare(b.id));\n}\n\nasync function readTextBounded(response: Response, maxBytes: number): Promise<string> {\n\tconst reader = response.body?.getReader();\n\tif (!reader) return await response.text();\n\tconst chunks: Uint8Array[] = [];\n\tlet total = 0;\n\twhile (true) {\n\t\tconst { done, value } = await reader.read();\n\t\tif (done) break;\n\t\tif (!value) continue;\n\t\ttotal += value.byteLength;\n\t\tif (total > maxBytes) {\n\t\t\tawait reader.cancel();\n\t\t\tthrow new Error(`Response exceeds ${maxBytes} bytes.`);\n\t\t}\n\t\tchunks.push(value);\n\t}\n\tconst merged = new Uint8Array(total);\n\tlet offset = 0;\n\tfor (const chunk of chunks) {\n\t\tmerged.set(chunk, offset);\n\t\toffset += chunk.byteLength;\n\t}\n\treturn new TextDecoder().decode(merged);\n}\n"]}
1
+ {"version":3,"file":"probe.js","sourceRoot":"","sources":["../../../src/extensions/router/probe.ts"],"names":[],"mappings":"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","sourcesContent":["/** Fetch OpenAI-compatible model catalog from a relay baseUrl. */\n\nimport { readResponseTextBounded } from \"../../utils/http-response.ts\";\nimport { DEFAULTS, formatError } from \"./constants.ts\";\n\nexport interface ProbeModel {\n\tid: string;\n\tname?: string;\n}\n\nexport type ProbeResult = { ok: true; models: ProbeModel[]; truncated: boolean } | { ok: false; error: string };\n\nexport async function probeRelayModels(opts: {\n\tbaseUrl: string;\n\tapiKey?: string;\n\tsignal?: AbortSignal;\n\ttimeoutMs?: number;\n}): Promise<ProbeResult> {\n\tlet baseUrl: URL;\n\ttry {\n\t\tbaseUrl = new URL(opts.baseUrl);\n\t} catch {\n\t\treturn { ok: false, error: \"Base URL is not a valid URL.\" };\n\t}\n\tif (baseUrl.protocol !== \"http:\" && baseUrl.protocol !== \"https:\") {\n\t\treturn { ok: false, error: `Unsupported protocol: ${baseUrl.protocol}` };\n\t}\n\n\tif (opts.signal?.aborted) return { ok: false, error: \"Cancelled.\" };\n\tconst controller = new AbortController();\n\tconst timeoutMs = opts.timeoutMs ?? DEFAULTS.probeTimeoutMs;\n\tconst timer = setTimeout(() => controller.abort(), timeoutMs);\n\tconst onOuterAbort = () => controller.abort();\n\topts.signal?.addEventListener(\"abort\", onOuterAbort, { once: true });\n\n\ttry {\n\t\tconst url = appendPath(baseUrl, \"models\");\n\t\tconst headers: Record<string, string> = { Accept: \"application/json\" };\n\t\tif (opts.apiKey) headers.Authorization = `Bearer ${opts.apiKey}`;\n\n\t\tconst response = await fetch(url, { headers, signal: controller.signal });\n\t\tif (!response.ok) {\n\t\t\tconst body = await readResponseTextBounded(response, {\n\t\t\t\tmaxBytes: 4_096,\n\t\t\t\tsignal: controller.signal,\n\t\t\t});\n\t\t\treturn {\n\t\t\t\tok: false,\n\t\t\t\terror: `HTTP ${response.status}${body ? `: ${body.slice(0, 400)}` : \"\"}`,\n\t\t\t};\n\t\t}\n\t\tconst text = await readResponseTextBounded(response, {\n\t\t\tmaxBytes: DEFAULTS.probeBodyBytes,\n\t\t\toverflowMessage: `Response exceeds ${DEFAULTS.probeBodyBytes} bytes.`,\n\t\t\tsignal: controller.signal,\n\t\t});\n\t\tlet json: unknown;\n\t\ttry {\n\t\t\tjson = JSON.parse(text);\n\t\t} catch {\n\t\t\treturn { ok: false, error: \"Model catalog response is not JSON.\" };\n\t\t}\n\t\tconst models = parseOpenAIModels(json);\n\t\tif (models === null) {\n\t\t\treturn { ok: false, error: \"JSON has no OpenAI-style `data` array of model ids.\" };\n\t\t}\n\t\tconst sorted = dedupeSort(models);\n\t\tconst truncated = sorted.length > DEFAULTS.probeMaxModels;\n\t\treturn {\n\t\t\tok: true,\n\t\t\tmodels: sorted.slice(0, DEFAULTS.probeMaxModels),\n\t\t\ttruncated,\n\t\t};\n\t} catch (error) {\n\t\tif (controller.signal.aborted) {\n\t\t\treturn { ok: false, error: opts.signal?.aborted ? \"Cancelled.\" : `Timed out after ${timeoutMs}ms.` };\n\t\t}\n\t\treturn { ok: false, error: formatError(error) };\n\t} finally {\n\t\tclearTimeout(timer);\n\t\topts.signal?.removeEventListener(\"abort\", onOuterAbort);\n\t}\n}\n\nfunction appendPath(base: URL, segment: string): URL {\n\tconst url = new URL(base.href);\n\tconst path = url.pathname.replace(/\\/+$/, \"\");\n\turl.pathname = `${path}/${segment}`;\n\treturn url;\n}\n\nfunction parseOpenAIModels(json: unknown): ProbeModel[] | null {\n\tif (!json || typeof json !== \"object\") return null;\n\tconst data = (json as { data?: unknown }).data;\n\tif (!Array.isArray(data)) return null;\n\tconst models: ProbeModel[] = [];\n\tfor (const item of data) {\n\t\tif (!item || typeof item !== \"object\") continue;\n\t\tconst record = item as Record<string, unknown>;\n\t\tconst id = typeof record.id === \"string\" ? record.id.trim() : \"\";\n\t\tif (!id) continue;\n\t\tconst name =\n\t\t\ttypeof record.name === \"string\" && record.name.trim() && record.name.trim() !== id\n\t\t\t\t? record.name.trim()\n\t\t\t\t: undefined;\n\t\tmodels.push(name ? { id, name } : { id });\n\t}\n\treturn models;\n}\n\nfunction dedupeSort(models: ProbeModel[]): ProbeModel[] {\n\tconst map = new Map<string, ProbeModel>();\n\tfor (const model of models) {\n\t\tif (!map.has(model.id)) map.set(model.id, model);\n\t}\n\treturn [...map.values()].sort((a, b) => a.id.localeCompare(b.id));\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../../../src/extensions/router/register.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAGnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAK1D,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW;;;;;;;;;;;;;;;;;;;;;;EAYlD;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,UAAU,GAAG,IAAI,CA8BxE;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAc3E;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAOrE"}
1
+ {"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../../../src/extensions/router/register.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAGnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAc1D,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW;;;;;;;;;;;;;;;;;;;;;;EAYlD;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,UAAU,GAAG,IAAI,CA+BxE;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAe3E;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAOrE"}
@@ -4,8 +4,16 @@
4
4
  import { ROUTER_API } from "./constants.js";
5
5
  import { toRegisterModel } from "./presets.js";
6
6
  import { streamRouterCodex } from "./stream.js";
7
- /** Provider ids currently registered by this extension in the process. */
8
- const registeredIds = new Set();
7
+ /** Provider ids registered by this extension, isolated to each SDK host. */
8
+ const registeredIdsByApi = new WeakMap();
9
+ function registeredIdsFor(pi) {
10
+ let ids = registeredIdsByApi.get(pi);
11
+ if (!ids) {
12
+ ids = new Set();
13
+ registeredIdsByApi.set(pi, ids);
14
+ }
15
+ return ids;
16
+ }
9
17
  export function toProviderConfig(relay) {
10
18
  return {
11
19
  name: relay.id,
@@ -20,6 +28,7 @@ export function toProviderConfig(relay) {
20
28
  };
21
29
  }
22
30
  export function applyRouterFile(pi, file) {
31
+ const registeredIds = registeredIdsFor(pi);
23
32
  const nextIds = new Set(file.relays.map((relay) => relay.id));
24
33
  for (const id of [...registeredIds]) {
25
34
  if (!nextIds.has(id)) {
@@ -40,7 +49,7 @@ export function applyRouterFile(pi, file) {
40
49
  pi.unregisterProvider(relay.id);
41
50
  }
42
51
  catch {
43
- // ignore
52
+ // Ignore stale registrations after /reload.
44
53
  }
45
54
  registeredIds.delete(relay.id);
46
55
  }
@@ -51,13 +60,14 @@ export function applyRouterFile(pi, file) {
51
60
  }
52
61
  }
53
62
  export function registerOneRelay(pi, relay) {
63
+ const registeredIds = registeredIdsFor(pi);
54
64
  if (relay.models.length === 0) {
55
65
  if (registeredIds.has(relay.id)) {
56
66
  try {
57
67
  pi.unregisterProvider(relay.id);
58
68
  }
59
69
  catch {
60
- // ignore
70
+ // Ignore stale registrations after /reload.
61
71
  }
62
72
  registeredIds.delete(relay.id);
63
73
  }
@@ -71,8 +81,8 @@ export function unregisterOneRelay(pi, id) {
71
81
  pi.unregisterProvider(id);
72
82
  }
73
83
  catch {
74
- // ignore
84
+ // Ignore stale registrations after /reload.
75
85
  }
76
- registeredIds.delete(id);
86
+ registeredIdsFor(pi).delete(id);
77
87
  }
78
88
  //# sourceMappingURL=register.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"register.js","sourceRoot":"","sources":["../../../src/extensions/router/register.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAGhD,0EAA0E;AAC1E,MAAM,aAAa,GAAG,IAAI,GAAG,EAAU,CAAC;AAExC,MAAM,UAAU,gBAAgB,CAAC,KAAkB;IAClD,OAAO;QACN,IAAI,EAAE,KAAK,CAAC,EAAE;QACd,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;QAC1C,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,GAAG,EAAE,UAAU;QACf,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YACpC,GAAG,eAAe,CAAC,KAAK,CAAC;YACzB,GAAG,EAAE,UAAU;SACf,CAAC,CAAC;QACH,YAAY,EAAE,iBAAiB;KAC/B,CAAC;AACH,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,EAAgB,EAAE,IAAgB;IACjE,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;IAE9D,KAAK,MAAM,EAAE,IAAI,CAAC,GAAG,aAAa,CAAC,EAAE,CAAC;QACrC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;YACtB,IAAI,CAAC;gBACJ,EAAE,CAAC,kBAAkB,CAAC,EAAE,CAAC,CAAC;YAC3B,CAAC;YAAC,MAAM,CAAC;gBACR,8CAA8C;YAC/C,CAAC;YACD,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC1B,CAAC;IACF,CAAC;IAED,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QACjC,sFAAsF;QACtF,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/B,IAAI,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC;gBACjC,IAAI,CAAC;oBACJ,EAAE,CAAC,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;gBACjC,CAAC;gBAAC,MAAM,CAAC;oBACR,SAAS;gBACV,CAAC;gBACD,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YAChC,CAAC;YACD,SAAS;QACV,CAAC;QACD,EAAE,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAE,EAAE,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC;QACvD,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC7B,CAAC;AACF,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,EAAgB,EAAE,KAAkB;IACpE,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/B,IAAI,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC;YACjC,IAAI,CAAC;gBACJ,EAAE,CAAC,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACjC,CAAC;YAAC,MAAM,CAAC;gBACR,SAAS;YACV,CAAC;YACD,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAChC,CAAC;QACD,OAAO;IACR,CAAC;IACD,EAAE,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAE,EAAE,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC;IACvD,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,EAAgB,EAAE,EAAU;IAC9D,IAAI,CAAC;QACJ,EAAE,CAAC,kBAAkB,CAAC,EAAE,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACR,SAAS;IACV,CAAC;IACD,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AAC1B,CAAC","sourcesContent":["/**\n * Apply router.json relays to Pi via registerProvider (config form + streamSimple).\n */\n\nimport type { ExtensionAPI } from \"../../core/extensions/types.ts\";\nimport { ROUTER_API } from \"./constants.ts\";\nimport { toRegisterModel } from \"./presets.ts\";\nimport { streamRouterCodex } from \"./stream.ts\";\nimport type { RelayConfig, RouterFile } from \"./types.ts\";\n\n/** Provider ids currently registered by this extension in the process. */\nconst registeredIds = new Set<string>();\n\nexport function toProviderConfig(relay: RelayConfig) {\n\treturn {\n\t\tname: relay.id,\n\t\tbaseUrl: relay.baseUrl.replace(/\\/+$/, \"\"),\n\t\tapiKey: relay.apiKey,\n\t\tapi: ROUTER_API,\n\t\tmodels: relay.models.map((model) => ({\n\t\t\t...toRegisterModel(model),\n\t\t\tapi: ROUTER_API,\n\t\t})),\n\t\tstreamSimple: streamRouterCodex,\n\t};\n}\n\nexport function applyRouterFile(pi: ExtensionAPI, file: RouterFile): void {\n\tconst nextIds = new Set(file.relays.map((relay) => relay.id));\n\n\tfor (const id of [...registeredIds]) {\n\t\tif (!nextIds.has(id)) {\n\t\t\ttry {\n\t\t\t\tpi.unregisterProvider(id);\n\t\t\t} catch {\n\t\t\t\t// Provider may already be gone after /reload.\n\t\t\t}\n\t\t\tregisteredIds.delete(id);\n\t\t}\n\t}\n\n\tfor (const relay of file.relays) {\n\t\t// Only register relays that have at least one model; empty catalog is not selectable.\n\t\tif (relay.models.length === 0) {\n\t\t\tif (registeredIds.has(relay.id)) {\n\t\t\t\ttry {\n\t\t\t\t\tpi.unregisterProvider(relay.id);\n\t\t\t\t} catch {\n\t\t\t\t\t// ignore\n\t\t\t\t}\n\t\t\t\tregisteredIds.delete(relay.id);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tpi.registerProvider(relay.id, toProviderConfig(relay));\n\t\tregisteredIds.add(relay.id);\n\t}\n}\n\nexport function registerOneRelay(pi: ExtensionAPI, relay: RelayConfig): void {\n\tif (relay.models.length === 0) {\n\t\tif (registeredIds.has(relay.id)) {\n\t\t\ttry {\n\t\t\t\tpi.unregisterProvider(relay.id);\n\t\t\t} catch {\n\t\t\t\t// ignore\n\t\t\t}\n\t\t\tregisteredIds.delete(relay.id);\n\t\t}\n\t\treturn;\n\t}\n\tpi.registerProvider(relay.id, toProviderConfig(relay));\n\tregisteredIds.add(relay.id);\n}\n\nexport function unregisterOneRelay(pi: ExtensionAPI, id: string): void {\n\ttry {\n\t\tpi.unregisterProvider(id);\n\t} catch {\n\t\t// ignore\n\t}\n\tregisteredIds.delete(id);\n}\n"]}
1
+ {"version":3,"file":"register.js","sourceRoot":"","sources":["../../../src/extensions/router/register.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Apply router.json relays to Pi via registerProvider (config form + streamSimple).\n */\n\nimport type { ExtensionAPI } from \"../../core/extensions/types.ts\";\nimport { ROUTER_API } from \"./constants.ts\";\nimport { toRegisterModel } from \"./presets.ts\";\nimport { streamRouterCodex } from \"./stream.ts\";\nimport type { RelayConfig, RouterFile } from \"./types.ts\";\n\n/** Provider ids registered by this extension, isolated to each SDK host. */\nconst registeredIdsByApi = new WeakMap<ExtensionAPI, Set<string>>();\n\nfunction registeredIdsFor(pi: ExtensionAPI): Set<string> {\n\tlet ids = registeredIdsByApi.get(pi);\n\tif (!ids) {\n\t\tids = new Set<string>();\n\t\tregisteredIdsByApi.set(pi, ids);\n\t}\n\treturn ids;\n}\n\nexport function toProviderConfig(relay: RelayConfig) {\n\treturn {\n\t\tname: relay.id,\n\t\tbaseUrl: relay.baseUrl.replace(/\\/+$/, \"\"),\n\t\tapiKey: relay.apiKey,\n\t\tapi: ROUTER_API,\n\t\tmodels: relay.models.map((model) => ({\n\t\t\t...toRegisterModel(model),\n\t\t\tapi: ROUTER_API,\n\t\t})),\n\t\tstreamSimple: streamRouterCodex,\n\t};\n}\n\nexport function applyRouterFile(pi: ExtensionAPI, file: RouterFile): void {\n\tconst registeredIds = registeredIdsFor(pi);\n\tconst nextIds = new Set(file.relays.map((relay) => relay.id));\n\n\tfor (const id of [...registeredIds]) {\n\t\tif (!nextIds.has(id)) {\n\t\t\ttry {\n\t\t\t\tpi.unregisterProvider(id);\n\t\t\t} catch {\n\t\t\t\t// Provider may already be gone after /reload.\n\t\t\t}\n\t\t\tregisteredIds.delete(id);\n\t\t}\n\t}\n\n\tfor (const relay of file.relays) {\n\t\t// Only register relays that have at least one model; empty catalog is not selectable.\n\t\tif (relay.models.length === 0) {\n\t\t\tif (registeredIds.has(relay.id)) {\n\t\t\t\ttry {\n\t\t\t\t\tpi.unregisterProvider(relay.id);\n\t\t\t\t} catch {\n\t\t\t\t\t// Ignore stale registrations after /reload.\n\t\t\t\t}\n\t\t\t\tregisteredIds.delete(relay.id);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tpi.registerProvider(relay.id, toProviderConfig(relay));\n\t\tregisteredIds.add(relay.id);\n\t}\n}\n\nexport function registerOneRelay(pi: ExtensionAPI, relay: RelayConfig): void {\n\tconst registeredIds = registeredIdsFor(pi);\n\tif (relay.models.length === 0) {\n\t\tif (registeredIds.has(relay.id)) {\n\t\t\ttry {\n\t\t\t\tpi.unregisterProvider(relay.id);\n\t\t\t} catch {\n\t\t\t\t// Ignore stale registrations after /reload.\n\t\t\t}\n\t\t\tregisteredIds.delete(relay.id);\n\t\t}\n\t\treturn;\n\t}\n\tpi.registerProvider(relay.id, toProviderConfig(relay));\n\tregisteredIds.add(relay.id);\n}\n\nexport function unregisterOneRelay(pi: ExtensionAPI, id: string): void {\n\ttry {\n\t\tpi.unregisterProvider(id);\n\t} catch {\n\t\t// Ignore stale registrations after /reload.\n\t}\n\tregisteredIdsFor(pi).delete(id);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../src/extensions/router/store.ts"],"names":[],"mappings":"AAAA;;GAEG;AAOH,OAAO,KAAK,EAAE,WAAW,EAAoB,UAAU,EAAoB,MAAM,YAAY,CAAC;AAE9F,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED,wBAAgB,eAAe,IAAI,UAAU,CAE5C;AAED,wBAAsB,cAAc,IAAI,OAAO,CAAC,UAAU,CAAC,CAU1D;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAqBvD;AA+CD,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA2BpE;AAeD,wBAAsB,WAAW,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAOzE;AAED,wBAAsB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAKjE"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../src/extensions/router/store.ts"],"names":[],"mappings":"AAAA;;GAEG;AAOH,OAAO,KAAK,EAAE,WAAW,EAAoB,UAAU,EAAoB,MAAM,YAAY,CAAC;AAM9F,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED,wBAAgB,eAAe,IAAI,UAAU,CAE5C;AAED,wBAAsB,cAAc,IAAI,OAAO,CAAC,UAAU,CAAC,CAU1D;AA4BD,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CA0BvD;AA4ED,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA2BpE;AAeD,wBAAsB,WAAW,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAOzE;AAED,wBAAsB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAKjE"}
@@ -5,7 +5,10 @@ import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
5
5
  import { dirname, join } from "node:path";
6
6
  import { getAgentDir } from "../../config.js";
7
7
  import { withFileMutationQueue } from "../../core/tools/file-mutation-queue.js";
8
- import { CONFIG_VERSION, isValidRelayId } from "./constants.js";
8
+ import { CONFIG_VERSION, isValidRelayId, THINKING_LEVELS } from "./constants.js";
9
+ const ROOT_FIELDS = ["version", "relays"];
10
+ const RELAY_FIELDS = ["id", "baseUrl", "apiKey", "models"];
11
+ const MODEL_FIELDS = ["id", "name", "reasoning", "input", "contextWindow", "maxTokens", "thinkingLevelMap"];
9
12
  export function getRouterConfigPath() {
10
13
  return join(getAgentDir(), "router.json");
11
14
  }
@@ -25,6 +28,31 @@ export async function loadRouterFile() {
25
28
  }
26
29
  return parseRouterFile(raw);
27
30
  }
31
+ function expectObject(value, path) {
32
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
33
+ throw new Error(`${path} must be an object.`);
34
+ }
35
+ return value;
36
+ }
37
+ function rejectUnknownFields(record, allowed, path) {
38
+ const unknown = Object.keys(record).filter((key) => !allowed.includes(key));
39
+ if (unknown.length > 0)
40
+ throw new Error(`${path} has unsupported field(s): ${unknown.join(", ")}.`);
41
+ }
42
+ function expectString(value, path, allowEmpty = false) {
43
+ if (typeof value !== "string")
44
+ throw new Error(`${path} must be a string.`);
45
+ const trimmed = value.trim();
46
+ if (!allowEmpty && !trimmed)
47
+ throw new Error(`${path} must not be empty.`);
48
+ return allowEmpty ? value : trimmed;
49
+ }
50
+ function expectPositiveInteger(value, path) {
51
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
52
+ throw new Error(`${path} must be a positive integer.`);
53
+ }
54
+ return value;
55
+ }
28
56
  export function parseRouterFile(raw) {
29
57
  let parsed;
30
58
  try {
@@ -33,71 +61,100 @@ export function parseRouterFile(raw) {
33
61
  catch {
34
62
  throw new Error("router.json is not valid JSON.");
35
63
  }
36
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
37
- throw new Error("router.json must be a JSON object.");
64
+ const root = expectObject(parsed, "router.json");
65
+ rejectUnknownFields(root, ROOT_FIELDS, "router.json");
66
+ const version = root.version ?? CONFIG_VERSION;
67
+ if (version !== CONFIG_VERSION) {
68
+ throw new Error(`router.json has unsupported version ${String(version)}.`);
38
69
  }
39
- const root = parsed;
40
- const relaysRaw = root.relays;
41
- if (relaysRaw !== undefined && !Array.isArray(relaysRaw)) {
70
+ if (root.relays !== undefined && !Array.isArray(root.relays)) {
42
71
  throw new Error("router.json relays must be an array.");
43
72
  }
44
- const relays = [];
45
- for (const item of relaysRaw ?? []) {
46
- const relay = normalizeRelay(item);
47
- if (relay)
48
- relays.push(relay);
73
+ const relays = (root.relays ?? []).map((value, index) => parseRelay(value, `router.json relays[${index}]`));
74
+ const relayIds = new Set();
75
+ for (const relay of relays) {
76
+ if (relayIds.has(relay.id))
77
+ throw new Error(`router.json contains duplicate relay id "${relay.id}".`);
78
+ relayIds.add(relay.id);
49
79
  }
50
80
  return { version: CONFIG_VERSION, relays };
51
81
  }
52
- function normalizeRelay(value) {
53
- if (!value || typeof value !== "object" || Array.isArray(value))
54
- return undefined;
55
- const record = value;
56
- const id = typeof record.id === "string" ? record.id.trim() : "";
57
- const baseUrl = typeof record.baseUrl === "string" ? record.baseUrl.trim() : "";
58
- const apiKey = typeof record.apiKey === "string" ? record.apiKey : "";
59
- if (!id || !isValidRelayId(id) || !baseUrl)
60
- return undefined;
61
- const models = [];
62
- if (Array.isArray(record.models)) {
63
- for (const model of record.models) {
64
- const normalized = normalizeModel(model);
65
- if (normalized)
66
- models.push(normalized);
67
- }
82
+ function parseRelay(value, path) {
83
+ const record = expectObject(value, path);
84
+ rejectUnknownFields(record, RELAY_FIELDS, path);
85
+ const id = expectString(record.id, `${path}.id`);
86
+ if (!isValidRelayId(id))
87
+ throw new Error(`${path}.id must be a valid provider id without '/'.`);
88
+ const baseUrl = expectString(record.baseUrl, `${path}.baseUrl`);
89
+ try {
90
+ const url = new URL(baseUrl);
91
+ if (url.protocol !== "http:" && url.protocol !== "https:")
92
+ throw new Error("unsupported protocol");
93
+ }
94
+ catch {
95
+ throw new Error(`${path}.baseUrl must be an http or https URL.`);
96
+ }
97
+ const apiKey = expectString(record.apiKey, `${path}.apiKey`, true);
98
+ if (record.models !== undefined && !Array.isArray(record.models)) {
99
+ throw new Error(`${path}.models must be an array.`);
100
+ }
101
+ const models = (record.models ?? []).map((model, index) => parseModel(model, `${path}.models[${index}]`));
102
+ const modelIds = new Set();
103
+ for (const model of models) {
104
+ if (modelIds.has(model.id))
105
+ throw new Error(`${path}.models contains duplicate id "${model.id}".`);
106
+ modelIds.add(model.id);
68
107
  }
69
108
  return { id, baseUrl, apiKey, models };
70
109
  }
71
- function normalizeModel(value) {
72
- if (!value || typeof value !== "object" || Array.isArray(value))
73
- return undefined;
74
- const record = value;
75
- const id = typeof record.id === "string" ? record.id.trim() : "";
76
- if (!id)
77
- return undefined;
78
- const model = { id };
79
- if (typeof record.name === "string" && record.name.trim())
80
- model.name = record.name.trim();
81
- if (typeof record.reasoning === "boolean")
110
+ function parseModel(value, path) {
111
+ const record = expectObject(value, path);
112
+ rejectUnknownFields(record, MODEL_FIELDS, path);
113
+ const model = { id: expectString(record.id, `${path}.id`) };
114
+ if (record.name !== undefined) {
115
+ const name = expectString(record.name, `${path}.name`, true).trim();
116
+ if (name)
117
+ model.name = name;
118
+ }
119
+ if (record.reasoning !== undefined) {
120
+ if (typeof record.reasoning !== "boolean")
121
+ throw new Error(`${path}.reasoning must be a boolean.`);
82
122
  model.reasoning = record.reasoning;
83
- if (Array.isArray(record.input)) {
84
- const input = record.input.filter((item) => item === "text" || item === "image");
85
- if (input.includes("text"))
86
- model.input = input.includes("image") ? ["text", "image"] : ["text"];
87
123
  }
88
- if (typeof record.contextWindow === "number" && Number.isFinite(record.contextWindow) && record.contextWindow > 0) {
89
- model.contextWindow = Math.floor(record.contextWindow);
124
+ if (record.input !== undefined) {
125
+ if (!Array.isArray(record.input) || record.input.some((item) => item !== "text" && item !== "image")) {
126
+ throw new Error(`${path}.input must contain only "text" and "image".`);
127
+ }
128
+ const input = new Set(record.input);
129
+ if (!input.has("text"))
130
+ throw new Error(`${path}.input must include "text".`);
131
+ model.input = input.has("image") ? ["text", "image"] : ["text"];
132
+ }
133
+ if (record.contextWindow !== undefined) {
134
+ model.contextWindow = expectPositiveInteger(record.contextWindow, `${path}.contextWindow`);
90
135
  }
91
- if (typeof record.maxTokens === "number" && Number.isFinite(record.maxTokens) && record.maxTokens > 0) {
92
- model.maxTokens = Math.floor(record.maxTokens);
136
+ if (record.maxTokens !== undefined) {
137
+ model.maxTokens = expectPositiveInteger(record.maxTokens, `${path}.maxTokens`);
93
138
  }
94
- if (record.thinkingLevelMap &&
95
- typeof record.thinkingLevelMap === "object" &&
96
- !Array.isArray(record.thinkingLevelMap)) {
97
- model.thinkingLevelMap = record.thinkingLevelMap;
139
+ if (record.thinkingLevelMap !== undefined) {
140
+ model.thinkingLevelMap = parseThinkingLevelMap(record.thinkingLevelMap, `${path}.thinkingLevelMap`);
98
141
  }
99
142
  return model;
100
143
  }
144
+ function parseThinkingLevelMap(value, path) {
145
+ const record = expectObject(value, path);
146
+ const map = {};
147
+ for (const [level, target] of Object.entries(record)) {
148
+ if (!THINKING_LEVELS.includes(level)) {
149
+ throw new Error(`${path} has unsupported thinking level "${level}".`);
150
+ }
151
+ if (target !== null && (typeof target !== "string" || target.trim().length === 0)) {
152
+ throw new Error(`${path}.${level} must be a non-empty string or null.`);
153
+ }
154
+ map[level] = target === null ? null : target.trim();
155
+ }
156
+ return map;
157
+ }
101
158
  export async function saveRouterFile(file) {
102
159
  const filePath = getRouterConfigPath();
103
160
  const payload = {
@@ -122,7 +179,7 @@ export async function saveRouterFile(file) {
122
179
  await unlink(tempPath);
123
180
  }
124
181
  catch {
125
- // ignore
182
+ // Ignore cleanup errors and preserve the original failure.
126
183
  }
127
184
  throw error;
128
185
  }
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","sourceRoot":"","sources":["../../../src/extensions/router/store.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC9E,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AAChF,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAGhE,MAAM,UAAU,mBAAmB;IAClC,OAAO,IAAI,CAAC,WAAW,EAAE,EAAE,aAAa,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,eAAe;IAC9B,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;AAChD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc;IACnC,MAAM,QAAQ,GAAG,mBAAmB,EAAE,CAAC;IACvC,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACJ,GAAG,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,eAAe,EAAE,CAAC;QACjF,MAAM,KAAK,CAAC;IACb,CAAC;IACD,OAAO,eAAe,CAAC,GAAG,CAAC,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,GAAW;IAC1C,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACJ,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACR,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;IACnD,CAAC;IACD,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACpE,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;IACvD,CAAC;IACD,MAAM,IAAI,GAAG,MAAiC,CAAC;IAC/C,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC;IAC9B,IAAI,SAAS,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QAC1D,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IACzD,CAAC;IACD,MAAM,MAAM,GAAkB,EAAE,CAAC;IACjC,KAAK,MAAM,IAAI,IAAI,SAAS,IAAI,EAAE,EAAE,CAAC;QACpC,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,KAAK;YAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,CAAC;AAC5C,CAAC;AAED,SAAS,cAAc,CAAC,KAAc;IACrC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAClF,MAAM,MAAM,GAAG,KAAgC,CAAC;IAChD,MAAM,EAAE,GAAG,OAAO,MAAM,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACjE,MAAM,OAAO,GAAG,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAChF,MAAM,MAAM,GAAG,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IACtE,IAAI,CAAC,EAAE,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO;QAAE,OAAO,SAAS,CAAC;IAC7D,MAAM,MAAM,GAAuB,EAAE,CAAC;IACtC,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;QAClC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YACnC,MAAM,UAAU,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;YACzC,IAAI,UAAU;gBAAE,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACzC,CAAC;IACF,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AACxC,CAAC;AAED,SAAS,cAAc,CAAC,KAAc;IACrC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAClF,MAAM,MAAM,GAAG,KAAgC,CAAC;IAChD,MAAM,EAAE,GAAG,OAAO,MAAM,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACjE,IAAI,CAAC,EAAE;QAAE,OAAO,SAAS,CAAC;IAC1B,MAAM,KAAK,GAAqB,EAAE,EAAE,EAAE,CAAC;IACvC,IAAI,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE;QAAE,KAAK,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;IAC3F,IAAI,OAAO,MAAM,CAAC,SAAS,KAAK,SAAS;QAAE,KAAK,CAAC,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC;IAC9E,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QACjC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAA4B,EAAE,CAAC,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,OAAO,CAAC,CAAC;QAC3G,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IAClG,CAAC;IACD,IAAI,OAAO,MAAM,CAAC,aAAa,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,MAAM,CAAC,aAAa,GAAG,CAAC,EAAE,CAAC;QACnH,KAAK,CAAC,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IACxD,CAAC;IACD,IAAI,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,MAAM,CAAC,SAAS,GAAG,CAAC,EAAE,CAAC;QACvG,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAChD,CAAC;IACD,IACC,MAAM,CAAC,gBAAgB;QACvB,OAAO,MAAM,CAAC,gBAAgB,KAAK,QAAQ;QAC3C,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,gBAAgB,CAAC,EACtC,CAAC;QACF,KAAK,CAAC,gBAAgB,GAAG,MAAM,CAAC,gBAAoC,CAAC;IACtE,CAAC;IACD,OAAO,KAAK,CAAC;AACd,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,IAAgB;IACpD,MAAM,QAAQ,GAAG,mBAAmB,EAAE,CAAC;IACvC,MAAM,OAAO,GAAe;QAC3B,OAAO,EAAE,cAAc;QACvB,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YACnC,EAAE,EAAE,KAAK,CAAC,EAAE;YACZ,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;SAC1D,CAAC,CAAC;KACH,CAAC;IACF,MAAM,UAAU,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;IAC3D,MAAM,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,MAAM,qBAAqB,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE;QAChD,MAAM,QAAQ,GAAG,GAAG,QAAQ,IAAI,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC;QAChE,IAAI,CAAC;YACJ,MAAM,SAAS,CAAC,QAAQ,EAAE,UAAU,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YACzE,MAAM,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAClC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC;gBACJ,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;YACxB,CAAC;YAAC,MAAM,CAAC;gBACR,SAAS;YACV,CAAC;YACD,MAAM,KAAK,CAAC;QACb,CAAC;IACF,CAAC,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CAAC,KAAuB;IAC9C,MAAM,GAAG,GAAqB,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC;IAC/C,IAAI,KAAK,CAAC,IAAI;QAAE,GAAG,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;IACtC,IAAI,KAAK,CAAC,SAAS,KAAK,SAAS;QAAE,GAAG,CAAC,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC;IACnE,IAAI,KAAK,CAAC,KAAK;QAAE,GAAG,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;IACzC,IAAI,KAAK,CAAC,aAAa,KAAK,SAAS;QAAE,GAAG,CAAC,aAAa,GAAG,KAAK,CAAC,aAAa,CAAC;IAC/E,IAAI,KAAK,CAAC,SAAS,KAAK,SAAS;QAAE,GAAG,CAAC,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC;IACnE,IAAI,KAAK,CAAC,gBAAgB,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9E,GAAG,CAAC,gBAAgB,GAAG,KAAK,CAAC,gBAAgB,CAAC;IAC/C,CAAC;IACD,OAAO,GAAG,CAAC;AACZ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,KAAkB;IACnD,MAAM,IAAI,GAAG,MAAM,cAAc,EAAE,CAAC;IACpC,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,CAAC,EAAE,CAAC,CAAC;IACtE,IAAI,KAAK,IAAI,CAAC;QAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC;;QACtC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7B,MAAM,cAAc,CAAC,IAAI,CAAC,CAAC;IAC3B,OAAO,IAAI,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,EAAU;IAC3C,MAAM,IAAI,GAAG,MAAM,cAAc,EAAE,CAAC;IACpC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IAC7D,MAAM,cAAc,CAAC,IAAI,CAAC,CAAC;IAC3B,OAAO,IAAI,CAAC;AACb,CAAC","sourcesContent":["/**\n * Persistence for ~/.pi/agent/router.json\n */\n\nimport { mkdir, readFile, rename, unlink, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { getAgentDir } from \"../../config.ts\";\nimport { withFileMutationQueue } from \"../../core/tools/file-mutation-queue.ts\";\nimport { CONFIG_VERSION, isValidRelayId } from \"./constants.ts\";\nimport type { RelayConfig, RelayModelConfig, RouterFile, ThinkingLevelMap } from \"./types.ts\";\n\nexport function getRouterConfigPath(): string {\n\treturn join(getAgentDir(), \"router.json\");\n}\n\nexport function emptyRouterFile(): RouterFile {\n\treturn { version: CONFIG_VERSION, relays: [] };\n}\n\nexport async function loadRouterFile(): Promise<RouterFile> {\n\tconst filePath = getRouterConfigPath();\n\tlet raw: string;\n\ttry {\n\t\traw = await readFile(filePath, \"utf8\");\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") return emptyRouterFile();\n\t\tthrow error;\n\t}\n\treturn parseRouterFile(raw);\n}\n\nexport function parseRouterFile(raw: string): RouterFile {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(raw);\n\t} catch {\n\t\tthrow new Error(\"router.json is not valid JSON.\");\n\t}\n\tif (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n\t\tthrow new Error(\"router.json must be a JSON object.\");\n\t}\n\tconst root = parsed as Record<string, unknown>;\n\tconst relaysRaw = root.relays;\n\tif (relaysRaw !== undefined && !Array.isArray(relaysRaw)) {\n\t\tthrow new Error(\"router.json relays must be an array.\");\n\t}\n\tconst relays: RelayConfig[] = [];\n\tfor (const item of relaysRaw ?? []) {\n\t\tconst relay = normalizeRelay(item);\n\t\tif (relay) relays.push(relay);\n\t}\n\treturn { version: CONFIG_VERSION, relays };\n}\n\nfunction normalizeRelay(value: unknown): RelayConfig | undefined {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n\tconst record = value as Record<string, unknown>;\n\tconst id = typeof record.id === \"string\" ? record.id.trim() : \"\";\n\tconst baseUrl = typeof record.baseUrl === \"string\" ? record.baseUrl.trim() : \"\";\n\tconst apiKey = typeof record.apiKey === \"string\" ? record.apiKey : \"\";\n\tif (!id || !isValidRelayId(id) || !baseUrl) return undefined;\n\tconst models: RelayModelConfig[] = [];\n\tif (Array.isArray(record.models)) {\n\t\tfor (const model of record.models) {\n\t\t\tconst normalized = normalizeModel(model);\n\t\t\tif (normalized) models.push(normalized);\n\t\t}\n\t}\n\treturn { id, baseUrl, apiKey, models };\n}\n\nfunction normalizeModel(value: unknown): RelayModelConfig | undefined {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n\tconst record = value as Record<string, unknown>;\n\tconst id = typeof record.id === \"string\" ? record.id.trim() : \"\";\n\tif (!id) return undefined;\n\tconst model: RelayModelConfig = { id };\n\tif (typeof record.name === \"string\" && record.name.trim()) model.name = record.name.trim();\n\tif (typeof record.reasoning === \"boolean\") model.reasoning = record.reasoning;\n\tif (Array.isArray(record.input)) {\n\t\tconst input = record.input.filter((item): item is \"text\" | \"image\" => item === \"text\" || item === \"image\");\n\t\tif (input.includes(\"text\")) model.input = input.includes(\"image\") ? [\"text\", \"image\"] : [\"text\"];\n\t}\n\tif (typeof record.contextWindow === \"number\" && Number.isFinite(record.contextWindow) && record.contextWindow > 0) {\n\t\tmodel.contextWindow = Math.floor(record.contextWindow);\n\t}\n\tif (typeof record.maxTokens === \"number\" && Number.isFinite(record.maxTokens) && record.maxTokens > 0) {\n\t\tmodel.maxTokens = Math.floor(record.maxTokens);\n\t}\n\tif (\n\t\trecord.thinkingLevelMap &&\n\t\ttypeof record.thinkingLevelMap === \"object\" &&\n\t\t!Array.isArray(record.thinkingLevelMap)\n\t) {\n\t\tmodel.thinkingLevelMap = record.thinkingLevelMap as ThinkingLevelMap;\n\t}\n\treturn model;\n}\n\nexport async function saveRouterFile(file: RouterFile): Promise<void> {\n\tconst filePath = getRouterConfigPath();\n\tconst payload: RouterFile = {\n\t\tversion: CONFIG_VERSION,\n\t\trelays: file.relays.map((relay) => ({\n\t\t\tid: relay.id,\n\t\t\tbaseUrl: relay.baseUrl,\n\t\t\tapiKey: relay.apiKey,\n\t\t\tmodels: relay.models.map((model) => serializeModel(model)),\n\t\t})),\n\t};\n\tconst serialized = `${JSON.stringify(payload, null, 2)}\\n`;\n\tawait mkdir(dirname(filePath), { recursive: true });\n\tawait withFileMutationQueue(filePath, async () => {\n\t\tconst tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;\n\t\ttry {\n\t\t\tawait writeFile(tempPath, serialized, { encoding: \"utf8\", mode: 0o600 });\n\t\t\tawait rename(tempPath, filePath);\n\t\t} catch (error) {\n\t\t\ttry {\n\t\t\t\tawait unlink(tempPath);\n\t\t\t} catch {\n\t\t\t\t// ignore\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t});\n}\n\nfunction serializeModel(model: RelayModelConfig): RelayModelConfig {\n\tconst out: RelayModelConfig = { id: model.id };\n\tif (model.name) out.name = model.name;\n\tif (model.reasoning !== undefined) out.reasoning = model.reasoning;\n\tif (model.input) out.input = model.input;\n\tif (model.contextWindow !== undefined) out.contextWindow = model.contextWindow;\n\tif (model.maxTokens !== undefined) out.maxTokens = model.maxTokens;\n\tif (model.thinkingLevelMap && Object.keys(model.thinkingLevelMap).length > 0) {\n\t\tout.thinkingLevelMap = model.thinkingLevelMap;\n\t}\n\treturn out;\n}\n\nexport async function upsertRelay(relay: RelayConfig): Promise<RouterFile> {\n\tconst file = await loadRouterFile();\n\tconst index = file.relays.findIndex((entry) => entry.id === relay.id);\n\tif (index >= 0) file.relays[index] = relay;\n\telse file.relays.push(relay);\n\tawait saveRouterFile(file);\n\treturn file;\n}\n\nexport async function removeRelay(id: string): Promise<RouterFile> {\n\tconst file = await loadRouterFile();\n\tfile.relays = file.relays.filter((entry) => entry.id !== id);\n\tawait saveRouterFile(file);\n\treturn file;\n}\n"]}
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../../src/extensions/router/store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Persistence for ~/.pi/agent/router.json\n */\n\nimport { mkdir, readFile, rename, unlink, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { getAgentDir } from \"../../config.ts\";\nimport { withFileMutationQueue } from \"../../core/tools/file-mutation-queue.ts\";\nimport { CONFIG_VERSION, isValidRelayId, THINKING_LEVELS } from \"./constants.ts\";\nimport type { RelayConfig, RelayModelConfig, RouterFile, ThinkingLevelMap } from \"./types.ts\";\n\nconst ROOT_FIELDS = [\"version\", \"relays\"] as const;\nconst RELAY_FIELDS = [\"id\", \"baseUrl\", \"apiKey\", \"models\"] as const;\nconst MODEL_FIELDS = [\"id\", \"name\", \"reasoning\", \"input\", \"contextWindow\", \"maxTokens\", \"thinkingLevelMap\"] as const;\n\nexport function getRouterConfigPath(): string {\n\treturn join(getAgentDir(), \"router.json\");\n}\n\nexport function emptyRouterFile(): RouterFile {\n\treturn { version: CONFIG_VERSION, relays: [] };\n}\n\nexport async function loadRouterFile(): Promise<RouterFile> {\n\tconst filePath = getRouterConfigPath();\n\tlet raw: string;\n\ttry {\n\t\traw = await readFile(filePath, \"utf8\");\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") return emptyRouterFile();\n\t\tthrow error;\n\t}\n\treturn parseRouterFile(raw);\n}\n\nfunction expectObject(value: unknown, path: string): Record<string, unknown> {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) {\n\t\tthrow new Error(`${path} must be an object.`);\n\t}\n\treturn value as Record<string, unknown>;\n}\n\nfunction rejectUnknownFields(record: Record<string, unknown>, allowed: readonly string[], path: string): void {\n\tconst unknown = Object.keys(record).filter((key) => !allowed.includes(key));\n\tif (unknown.length > 0) throw new Error(`${path} has unsupported field(s): ${unknown.join(\", \")}.`);\n}\n\nfunction expectString(value: unknown, path: string, allowEmpty = false): string {\n\tif (typeof value !== \"string\") throw new Error(`${path} must be a string.`);\n\tconst trimmed = value.trim();\n\tif (!allowEmpty && !trimmed) throw new Error(`${path} must not be empty.`);\n\treturn allowEmpty ? value : trimmed;\n}\n\nfunction expectPositiveInteger(value: unknown, path: string): number {\n\tif (typeof value !== \"number\" || !Number.isInteger(value) || value <= 0) {\n\t\tthrow new Error(`${path} must be a positive integer.`);\n\t}\n\treturn value;\n}\n\nexport function parseRouterFile(raw: string): RouterFile {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(raw);\n\t} catch {\n\t\tthrow new Error(\"router.json is not valid JSON.\");\n\t}\n\tconst root = expectObject(parsed, \"router.json\");\n\trejectUnknownFields(root, ROOT_FIELDS, \"router.json\");\n\tconst version = root.version ?? CONFIG_VERSION;\n\tif (version !== CONFIG_VERSION) {\n\t\tthrow new Error(`router.json has unsupported version ${String(version)}.`);\n\t}\n\tif (root.relays !== undefined && !Array.isArray(root.relays)) {\n\t\tthrow new Error(\"router.json relays must be an array.\");\n\t}\n\n\tconst relays = ((root.relays as unknown[] | undefined) ?? []).map((value, index) =>\n\t\tparseRelay(value, `router.json relays[${index}]`),\n\t);\n\tconst relayIds = new Set<string>();\n\tfor (const relay of relays) {\n\t\tif (relayIds.has(relay.id)) throw new Error(`router.json contains duplicate relay id \"${relay.id}\".`);\n\t\trelayIds.add(relay.id);\n\t}\n\treturn { version: CONFIG_VERSION, relays };\n}\n\nfunction parseRelay(value: unknown, path: string): RelayConfig {\n\tconst record = expectObject(value, path);\n\trejectUnknownFields(record, RELAY_FIELDS, path);\n\tconst id = expectString(record.id, `${path}.id`);\n\tif (!isValidRelayId(id)) throw new Error(`${path}.id must be a valid provider id without '/'.`);\n\tconst baseUrl = expectString(record.baseUrl, `${path}.baseUrl`);\n\ttry {\n\t\tconst url = new URL(baseUrl);\n\t\tif (url.protocol !== \"http:\" && url.protocol !== \"https:\") throw new Error(\"unsupported protocol\");\n\t} catch {\n\t\tthrow new Error(`${path}.baseUrl must be an http or https URL.`);\n\t}\n\tconst apiKey = expectString(record.apiKey, `${path}.apiKey`, true);\n\tif (record.models !== undefined && !Array.isArray(record.models)) {\n\t\tthrow new Error(`${path}.models must be an array.`);\n\t}\n\tconst models = ((record.models as unknown[] | undefined) ?? []).map((model, index) =>\n\t\tparseModel(model, `${path}.models[${index}]`),\n\t);\n\tconst modelIds = new Set<string>();\n\tfor (const model of models) {\n\t\tif (modelIds.has(model.id)) throw new Error(`${path}.models contains duplicate id \"${model.id}\".`);\n\t\tmodelIds.add(model.id);\n\t}\n\treturn { id, baseUrl, apiKey, models };\n}\n\nfunction parseModel(value: unknown, path: string): RelayModelConfig {\n\tconst record = expectObject(value, path);\n\trejectUnknownFields(record, MODEL_FIELDS, path);\n\tconst model: RelayModelConfig = { id: expectString(record.id, `${path}.id`) };\n\tif (record.name !== undefined) {\n\t\tconst name = expectString(record.name, `${path}.name`, true).trim();\n\t\tif (name) model.name = name;\n\t}\n\tif (record.reasoning !== undefined) {\n\t\tif (typeof record.reasoning !== \"boolean\") throw new Error(`${path}.reasoning must be a boolean.`);\n\t\tmodel.reasoning = record.reasoning;\n\t}\n\tif (record.input !== undefined) {\n\t\tif (!Array.isArray(record.input) || record.input.some((item) => item !== \"text\" && item !== \"image\")) {\n\t\t\tthrow new Error(`${path}.input must contain only \"text\" and \"image\".`);\n\t\t}\n\t\tconst input = new Set(record.input as Array<\"text\" | \"image\">);\n\t\tif (!input.has(\"text\")) throw new Error(`${path}.input must include \"text\".`);\n\t\tmodel.input = input.has(\"image\") ? [\"text\", \"image\"] : [\"text\"];\n\t}\n\tif (record.contextWindow !== undefined) {\n\t\tmodel.contextWindow = expectPositiveInteger(record.contextWindow, `${path}.contextWindow`);\n\t}\n\tif (record.maxTokens !== undefined) {\n\t\tmodel.maxTokens = expectPositiveInteger(record.maxTokens, `${path}.maxTokens`);\n\t}\n\tif (record.thinkingLevelMap !== undefined) {\n\t\tmodel.thinkingLevelMap = parseThinkingLevelMap(record.thinkingLevelMap, `${path}.thinkingLevelMap`);\n\t}\n\treturn model;\n}\n\nfunction parseThinkingLevelMap(value: unknown, path: string): ThinkingLevelMap {\n\tconst record = expectObject(value, path);\n\tconst map: ThinkingLevelMap = {};\n\tfor (const [level, target] of Object.entries(record)) {\n\t\tif (!(THINKING_LEVELS as readonly string[]).includes(level)) {\n\t\t\tthrow new Error(`${path} has unsupported thinking level \"${level}\".`);\n\t\t}\n\t\tif (target !== null && (typeof target !== \"string\" || target.trim().length === 0)) {\n\t\t\tthrow new Error(`${path}.${level} must be a non-empty string or null.`);\n\t\t}\n\t\tmap[level as keyof ThinkingLevelMap] = target === null ? null : target.trim();\n\t}\n\treturn map;\n}\n\nexport async function saveRouterFile(file: RouterFile): Promise<void> {\n\tconst filePath = getRouterConfigPath();\n\tconst payload: RouterFile = {\n\t\tversion: CONFIG_VERSION,\n\t\trelays: file.relays.map((relay) => ({\n\t\t\tid: relay.id,\n\t\t\tbaseUrl: relay.baseUrl,\n\t\t\tapiKey: relay.apiKey,\n\t\t\tmodels: relay.models.map((model) => serializeModel(model)),\n\t\t})),\n\t};\n\tconst serialized = `${JSON.stringify(payload, null, 2)}\\n`;\n\tawait mkdir(dirname(filePath), { recursive: true });\n\tawait withFileMutationQueue(filePath, async () => {\n\t\tconst tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;\n\t\ttry {\n\t\t\tawait writeFile(tempPath, serialized, { encoding: \"utf8\", mode: 0o600 });\n\t\t\tawait rename(tempPath, filePath);\n\t\t} catch (error) {\n\t\t\ttry {\n\t\t\t\tawait unlink(tempPath);\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors and preserve the original failure.\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t});\n}\n\nfunction serializeModel(model: RelayModelConfig): RelayModelConfig {\n\tconst out: RelayModelConfig = { id: model.id };\n\tif (model.name) out.name = model.name;\n\tif (model.reasoning !== undefined) out.reasoning = model.reasoning;\n\tif (model.input) out.input = model.input;\n\tif (model.contextWindow !== undefined) out.contextWindow = model.contextWindow;\n\tif (model.maxTokens !== undefined) out.maxTokens = model.maxTokens;\n\tif (model.thinkingLevelMap && Object.keys(model.thinkingLevelMap).length > 0) {\n\t\tout.thinkingLevelMap = model.thinkingLevelMap;\n\t}\n\treturn out;\n}\n\nexport async function upsertRelay(relay: RelayConfig): Promise<RouterFile> {\n\tconst file = await loadRouterFile();\n\tconst index = file.relays.findIndex((entry) => entry.id === relay.id);\n\tif (index >= 0) file.relays[index] = relay;\n\telse file.relays.push(relay);\n\tawait saveRouterFile(file);\n\treturn file;\n}\n\nexport async function removeRelay(id: string): Promise<RouterFile> {\n\tconst file = await loadRouterFile();\n\tfile.relays = file.relays.filter((entry) => entry.id !== id);\n\tawait saveRouterFile(file);\n\treturn file;\n}\n"]}
@@ -5,11 +5,17 @@
5
5
  * Searchable pickers follow Pi's native /model interaction pattern.
6
6
  */
7
7
  import type { ExtensionAPI, ExtensionCommandContext } from "../../core/extensions/types.ts";
8
+ import { type ModelChecklistItem } from "./dialog.ts";
9
+ import type { RelayModelConfig } from "./types.ts";
8
10
  export declare function isCurrentRouterModel(current: {
9
11
  provider: string;
10
12
  id: string;
11
13
  } | undefined, relayId: string, modelId: string): boolean;
12
14
  export declare function runRouterCommand(args: string, ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void>;
15
+ export declare function mergeCatalogWithConfigured(configured: ReadonlyArray<RelayModelConfig>, catalog: ReadonlyArray<{
16
+ id: string;
17
+ name?: string;
18
+ }>, activeModelId?: string): ModelChecklistItem[];
13
19
  /** Resolve only keys that can be safely used by the catalog probe. */
14
20
  export declare function resolveProbeApiKey(apiKey: string): {
15
21
  value?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../../../src/extensions/router/ui.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AA6F5F,wBAAgB,oBAAoB,CACnC,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,EACrD,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GACb,OAAO,CAET;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,uBAAuB,EAAE,EAAE,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBlH;AAqtBD,sEAAsE;AACtE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAiBrF"}
1
+ {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../../../src/extensions/router/ui.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAK5F,OAAO,EAIN,KAAK,kBAAkB,EACvB,MAAM,aAAa,CAAC;AAiBrB,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAkEhE,wBAAgB,oBAAoB,CACnC,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,EACrD,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GACb,OAAO,CAET;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,uBAAuB,EAAE,EAAE,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBlH;AAmoBD,wBAAgB,0BAA0B,CACzC,UAAU,EAAE,aAAa,CAAC,gBAAgB,CAAC,EAC3C,OAAO,EAAE,aAAa,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,EACrD,aAAa,CAAC,EAAE,MAAM,GACpB,kBAAkB,EAAE,CAuBtB;AAkED,sEAAsE;AACtE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAiBrF"}