@akon-labs/nexus-cli 0.1.3 → 0.1.4

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 (182) hide show
  1. package/CHANGELOG.md +16 -5327
  2. package/README.md +29 -24
  3. package/dist/cli/require-login.d.ts.map +1 -1
  4. package/dist/cli/require-login.js +1 -1
  5. package/dist/cli/require-login.js.map +1 -1
  6. package/dist/cli.js +107 -150
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +1 -1
  9. package/dist/config.js.map +1 -1
  10. package/dist/core/auth-guidance.d.ts.map +1 -1
  11. package/dist/core/auth-guidance.js +2 -3
  12. package/dist/core/auth-guidance.js.map +1 -1
  13. package/dist/core/extensions/loader.d.ts.map +1 -1
  14. package/dist/core/extensions/loader.js +18 -63
  15. package/dist/core/extensions/loader.js.map +1 -1
  16. package/dist/core/models-dev-catalog.d.ts.map +1 -1
  17. package/dist/core/models-dev-catalog.js +7 -11
  18. package/dist/core/models-dev-catalog.js.map +1 -1
  19. package/dist/core/sdk.d.ts +1 -1
  20. package/dist/core/sdk.d.ts.map +1 -1
  21. package/dist/core/sdk.js +1 -1
  22. package/dist/core/sdk.js.map +1 -1
  23. package/dist/core/tools/websearch.d.ts.map +1 -1
  24. package/dist/core/tools/websearch.js +37 -52
  25. package/dist/core/tools/websearch.js.map +1 -1
  26. package/dist/extensions/gitnexus/device-auth.d.ts.map +1 -1
  27. package/dist/extensions/gitnexus/device-auth.js +1 -2
  28. package/dist/extensions/gitnexus/device-auth.js.map +1 -1
  29. package/dist/extensions/gitnexus/gateway.d.ts.map +1 -1
  30. package/dist/extensions/gitnexus/gateway.js +1 -9
  31. package/dist/extensions/gitnexus/gateway.js.map +1 -1
  32. package/dist/extensions/gitnexus/index.d.ts.map +1 -1
  33. package/dist/extensions/gitnexus/index.js +4 -10
  34. package/dist/extensions/gitnexus/index.js.map +1 -1
  35. package/dist/extensions/llama/index.d.ts.map +1 -1
  36. package/dist/extensions/llama/index.js +1 -1
  37. package/dist/extensions/llama/index.js.map +1 -1
  38. package/dist/extensions/web-access.d.ts +8 -1
  39. package/dist/extensions/web-access.d.ts.map +1 -1
  40. package/dist/extensions/web-access.js +48 -2
  41. package/dist/extensions/web-access.js.map +1 -1
  42. package/dist/index.d.ts +1 -1
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +110 -155
  45. package/dist/index.js.map +1 -1
  46. package/dist/main.d.ts.map +1 -1
  47. package/dist/main.js +1 -1
  48. package/dist/main.js.map +1 -1
  49. package/dist/migrations.d.ts.map +1 -1
  50. package/dist/migrations.js +2 -2
  51. package/dist/migrations.js.map +1 -1
  52. package/dist/modes/interactive/components/command-palette.d.ts.map +1 -1
  53. package/dist/modes/interactive/components/command-palette.js +2 -2
  54. package/dist/modes/interactive/components/command-palette.js.map +1 -1
  55. package/dist/rpc-entry.js +107 -150
  56. package/dist/vendor/agent-core.js +6805 -0
  57. package/dist/vendor/ai.js +12407 -0
  58. package/dist/vendor/tui.js +11645 -0
  59. package/dist/vendor/web-access.js +93103 -0
  60. package/docs/compaction.md +14 -14
  61. package/docs/containerization.md +1 -1
  62. package/docs/custom-provider.md +17 -17
  63. package/docs/development.md +2 -2
  64. package/docs/environment-variables.md +1 -1
  65. package/docs/extensions.md +44 -44
  66. package/docs/index.md +5 -11
  67. package/docs/json.md +4 -4
  68. package/docs/llama-cpp.md +2 -2
  69. package/docs/models.md +3 -3
  70. package/docs/packages.md +6 -6
  71. package/docs/providers.md +13 -13
  72. package/docs/quickstart.md +19 -13
  73. package/docs/rpc.md +1 -1
  74. package/docs/sdk.md +28 -28
  75. package/docs/security.md +1 -1
  76. package/docs/session-format.md +6 -6
  77. package/docs/settings.md +2 -2
  78. package/docs/termux.md +1 -1
  79. package/docs/themes.md +2 -2
  80. package/docs/tui.md +20 -20
  81. package/docs/usage.md +5 -4
  82. package/examples/extensions/README.md +2 -2
  83. package/examples/extensions/auto-commit-on-exit.ts +1 -1
  84. package/examples/extensions/bash-spawn-hook.ts +2 -2
  85. package/examples/extensions/bookmark.ts +1 -1
  86. package/examples/extensions/border-status-editor.ts +3 -3
  87. package/examples/extensions/built-in-tool-renderer.ts +3 -3
  88. package/examples/extensions/claude-rules.ts +1 -1
  89. package/examples/extensions/commands.ts +1 -1
  90. package/examples/extensions/confirm-destructive.ts +1 -1
  91. package/examples/extensions/custom-compaction.ts +3 -3
  92. package/examples/extensions/custom-footer.ts +3 -3
  93. package/examples/extensions/custom-header.ts +2 -2
  94. package/examples/extensions/custom-provider-anthropic/index.ts +2 -2
  95. package/examples/extensions/custom-provider-gitlab-duo/index.ts +2 -2
  96. package/examples/extensions/custom-provider-gitlab-duo/test.ts +1 -1
  97. package/examples/extensions/dirty-repo-guard.ts +1 -1
  98. package/examples/extensions/doom-overlay/doom-component.ts +2 -2
  99. package/examples/extensions/doom-overlay/doom-keys.ts +1 -1
  100. package/examples/extensions/doom-overlay/index.ts +1 -1
  101. package/examples/extensions/dynamic-resources/dynamic.json +1 -1
  102. package/examples/extensions/dynamic-resources/index.ts +1 -1
  103. package/examples/extensions/dynamic-tools.ts +1 -1
  104. package/examples/extensions/entry-renderer.ts +2 -2
  105. package/examples/extensions/event-bus.ts +1 -1
  106. package/examples/extensions/file-trigger.ts +1 -1
  107. package/examples/extensions/git-checkpoint.ts +1 -1
  108. package/examples/extensions/git-merge-and-resolve.ts +1 -1
  109. package/examples/extensions/github-issue-autocomplete.ts +2 -2
  110. package/examples/extensions/gondolin/index.ts +2 -2
  111. package/examples/extensions/handoff.ts +4 -4
  112. package/examples/extensions/hello.ts +2 -2
  113. package/examples/extensions/hidden-thinking-label.ts +1 -1
  114. package/examples/extensions/inline-bash.ts +1 -1
  115. package/examples/extensions/input-transform-streaming.ts +1 -1
  116. package/examples/extensions/input-transform.ts +1 -1
  117. package/examples/extensions/interactive-shell.ts +1 -1
  118. package/examples/extensions/kimi-deferred-tools.ts +1 -1
  119. package/examples/extensions/mac-system-theme.ts +1 -1
  120. package/examples/extensions/message-renderer.ts +2 -2
  121. package/examples/extensions/minimal-mode.ts +3 -3
  122. package/examples/extensions/modal-editor.ts +2 -2
  123. package/examples/extensions/model-status.ts +1 -1
  124. package/examples/extensions/notify.ts +1 -1
  125. package/examples/extensions/overlay-qa-tests.ts +5 -5
  126. package/examples/extensions/overlay-test.ts +2 -2
  127. package/examples/extensions/permission-gate.ts +1 -1
  128. package/examples/extensions/pirate.ts +1 -1
  129. package/examples/extensions/plan-mode/index.ts +4 -4
  130. package/examples/extensions/preset.ts +4 -4
  131. package/examples/extensions/project-trust.ts +1 -1
  132. package/examples/extensions/prompt-customizer.ts +1 -1
  133. package/examples/extensions/protected-paths.ts +1 -1
  134. package/examples/extensions/provider-payload.ts +1 -1
  135. package/examples/extensions/qna.ts +3 -3
  136. package/examples/extensions/question.ts +2 -2
  137. package/examples/extensions/questionnaire.ts +2 -2
  138. package/examples/extensions/rainbow-editor.ts +1 -1
  139. package/examples/extensions/reload-runtime.ts +1 -1
  140. package/examples/extensions/rpc-demo.ts +1 -1
  141. package/examples/extensions/sandbox/index.ts +2 -2
  142. package/examples/extensions/send-user-message.ts +1 -1
  143. package/examples/extensions/session-name.ts +1 -1
  144. package/examples/extensions/shutdown-command.ts +1 -1
  145. package/examples/extensions/snake.ts +2 -2
  146. package/examples/extensions/space-invaders.ts +2 -2
  147. package/examples/extensions/ssh.ts +2 -2
  148. package/examples/extensions/status-line.ts +1 -1
  149. package/examples/extensions/structured-output.ts +2 -2
  150. package/examples/extensions/subagent/agents.ts +1 -1
  151. package/examples/extensions/subagent/index.ts +5 -5
  152. package/examples/extensions/summarize.ts +4 -4
  153. package/examples/extensions/system-prompt-header.ts +1 -1
  154. package/examples/extensions/tic-tac-toe.ts +3 -3
  155. package/examples/extensions/timed-confirm.ts +1 -1
  156. package/examples/extensions/titlebar-spinner.ts +1 -1
  157. package/examples/extensions/todo.ts +3 -3
  158. package/examples/extensions/tool-override.ts +2 -2
  159. package/examples/extensions/tools.ts +3 -3
  160. package/examples/extensions/trigger-compact.ts +1 -1
  161. package/examples/extensions/truncated-tool.ts +3 -3
  162. package/examples/extensions/widget-placement.ts +1 -1
  163. package/examples/extensions/with-deps/index.ts +1 -1
  164. package/examples/extensions/working-indicator.ts +1 -1
  165. package/examples/extensions/working-message-test.ts +1 -1
  166. package/examples/rpc-extension-ui.ts +1 -1
  167. package/examples/sdk/01-minimal.ts +1 -1
  168. package/examples/sdk/02-custom-model.ts +1 -1
  169. package/examples/sdk/03-custom-prompt.ts +1 -6
  170. package/examples/sdk/04-skills.ts +1 -1
  171. package/examples/sdk/05-tools.ts +1 -1
  172. package/examples/sdk/06-extensions.ts +2 -7
  173. package/examples/sdk/07-context-files.ts +1 -6
  174. package/examples/sdk/08-prompt-templates.ts +1 -1
  175. package/examples/sdk/09-api-keys-and-oauth.ts +1 -1
  176. package/examples/sdk/10-settings.ts +1 -1
  177. package/examples/sdk/11-sessions.ts +1 -1
  178. package/examples/sdk/12-full-control.ts +2 -2
  179. package/examples/sdk/13-session-runtime.ts +1 -1
  180. package/examples/sdk/README.md +2 -2
  181. package/npm-shrinkwrap.json +2 -26
  182. package/package.json +10 -25
@@ -1 +1 @@
1
- {"version":3,"file":"sdk.js","sourceRoot":"","sources":["../../src/core/sdk.ts"],"names":[],"mappings":"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","sourcesContent":["import { join } from \"node:path\";\nimport { Agent, type AgentMessage, setDefaultStreamFn, type ThinkingLevel } from \"@earendil-works/pi-agent-core\";\nimport { clampThinkingLevel, type Message, type Model, streamSimple } from \"@earendil-works/pi-ai/compat\";\nimport { getAgentDir } from \"../config.ts\";\nimport { resolvePath } from \"../utils/paths.ts\";\nimport { AgentSession } from \"./agent-session.ts\";\nimport { formatNoModelsAvailableMessage } from \"./auth-guidance.ts\";\nimport { DEFAULT_THINKING_LEVEL } from \"./defaults.ts\";\nimport type { ExtensionRunner, LoadExtensionsResult, SessionStartEvent, ToolDefinition } from \"./extensions/index.ts\";\nimport { convertToLlm } from \"./messages.ts\";\nimport { findInitialModel } from \"./model-resolver.ts\";\nimport { ModelRuntime } from \"./model-runtime.ts\";\nimport { mergeProviderAttributionHeaders } from \"./provider-attribution.ts\";\nimport type { ResourceLoader } from \"./resource-loader.ts\";\nimport { DefaultResourceLoader } from \"./resource-loader.ts\";\nimport { getDefaultSessionDir, SessionManager } from \"./session-manager.ts\";\nimport { SettingsManager } from \"./settings-manager.ts\";\nimport { time } from \"./timings.ts\";\nimport {\n\tcreateBashTool,\n\tcreateCodingTools,\n\tcreateEditTool,\n\tcreateFindTool,\n\tcreateGrepTool,\n\tcreateLsTool,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateWriteTool,\n\ttype ToolName,\n\twithFileMutationQueue,\n} from \"./tools/index.ts\";\n\n// Preserve the pre-0.81 fallback for extensions that construct Agent instances\n// or invoke low-level agent loops without supplying streamFn. Agent core remains\n// provider-agnostic and does not import pi-ai/compat itself.\nsetDefaultStreamFn(streamSimple);\n\nexport interface CreateAgentSessionOptions {\n\t/** Working directory for project-local discovery. Default: process.cwd() */\n\tcwd?: string;\n\t/** Global config directory. Default: ~/.pi/agent */\n\tagentDir?: string;\n\n\t/** Canonical model/auth runtime. Defaults to a runtime using agentDir/auth.json and models.json. */\n\tmodelRuntime?: ModelRuntime;\n\n\t/** Model to use. Default: exact saved setting; otherwise entitled gitnexus/auto when available. */\n\tmodel?: Model<any>;\n\t/** Thinking level. Default: from settings, else 'medium' (clamped to model capabilities) */\n\tthinkingLevel?: ThinkingLevel;\n\t/** Models available for cycling (Ctrl+P in interactive mode) */\n\tscopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;\n\n\t/**\n\t * Optional default tool suppression mode when no explicit allowlist is provided.\n\t *\n\t * - \"all\": start with no tools enabled\n\t * - \"builtin\": disable the default built-in tools (read, bash, edit, write)\n\t * but keep extension/custom tools enabled\n\t */\n\tnoTools?: \"all\" | \"builtin\";\n\t/**\n\t * Optional allowlist of tool names.\n\t *\n\t * When omitted, pi enables the default built-in tools (read, bash, edit, write)\n\t * and leaves extension/custom tools enabled unless `noTools` changes that default.\n\t * When provided, only the listed tool names are enabled.\n\t */\n\ttools?: string[];\n\t/** Optional denylist of tool names to disable. Applies after `tools` when both are provided. */\n\texcludeTools?: string[];\n\t/** Custom tools to register (in addition to built-in tools). */\n\tcustomTools?: ToolDefinition[];\n\n\t/** Resource loader. When omitted, DefaultResourceLoader is used. */\n\tresourceLoader?: ResourceLoader;\n\n\t/** Session manager. Default: SessionManager.create(cwd) */\n\tsessionManager?: SessionManager;\n\n\t/** Settings manager. Default: SettingsManager.create(cwd, agentDir) */\n\tsettingsManager?: SettingsManager;\n\t/** Session start event metadata for extension runtime startup. */\n\tsessionStartEvent?: SessionStartEvent;\n}\n\n/** Result from createAgentSession */\nexport interface CreateAgentSessionResult {\n\t/** The created session */\n\tsession: AgentSession;\n\t/** Extensions result (for UI context setup in interactive mode) */\n\textensionsResult: LoadExtensionsResult;\n\t/** Warning if session was restored with a different model than saved */\n\tmodelFallbackMessage?: string;\n}\n\n// Re-exports\n\nexport * from \"./agent-session-runtime.ts\";\nexport type {\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tExtensionFactory,\n\tInlineExtension,\n\tSlashCommandInfo,\n\tSlashCommandSource,\n\tToolDefinition,\n} from \"./extensions/index.ts\";\nexport type { PromptTemplate } from \"./prompt-templates.ts\";\nexport type { Skill } from \"./skills.ts\";\nexport type { Tool } from \"./tools/index.ts\";\n\nexport {\n\twithFileMutationQueue,\n\t// Tool factories (for custom cwd)\n\tcreateCodingTools,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateBashTool,\n\tcreateEditTool,\n\tcreateWriteTool,\n\tcreateGrepTool,\n\tcreateFindTool,\n\tcreateLsTool,\n};\n\n// Helper Functions\n\nfunction getDefaultAgentDir(): string {\n\treturn getAgentDir();\n}\n\n/**\n * Create an AgentSession with the specified options.\n *\n * @example\n * ```typescript\n * // Minimal - uses defaults\n * const { session } = await createAgentSession();\n *\n * // With explicit model\n * import { getModel } from '@earendil-works/pi-ai';\n * const { session } = await createAgentSession({\n * model: getModel('anthropic', 'claude-opus-4-5'),\n * thinkingLevel: 'high',\n * });\n *\n * // Continue previous session\n * const { session, modelFallbackMessage } = await createAgentSession({\n * continueSession: true,\n * });\n *\n * // Full control\n * const loader = new DefaultResourceLoader({\n * cwd: process.cwd(),\n * agentDir: getAgentDir(),\n * settingsManager: SettingsManager.create(),\n * });\n * await loader.reload();\n * const { session } = await createAgentSession({\n * model: myModel,\n * tools: [\"read\", \"bash\"],\n * resourceLoader: loader,\n * sessionManager: SessionManager.inMemory(),\n * });\n * ```\n */\nexport async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {\n\tconst cwd = resolvePath(options.cwd ?? options.sessionManager?.getCwd() ?? process.cwd());\n\tconst agentDir = options.agentDir ? resolvePath(options.agentDir) : getDefaultAgentDir();\n\tlet resourceLoader = options.resourceLoader;\n\n\tconst authPath = options.agentDir ? join(agentDir, \"auth.json\") : undefined;\n\tconst modelsPath = options.agentDir ? join(agentDir, \"models.json\") : undefined;\n\tconst modelRuntime = options.modelRuntime ?? (await ModelRuntime.create({ authPath, modelsPath }));\n\n\tconst settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);\n\tconst sessionManager = options.sessionManager ?? SessionManager.create(cwd, getDefaultSessionDir(cwd, agentDir));\n\n\tif (!resourceLoader) {\n\t\tresourceLoader = new DefaultResourceLoader({ cwd, agentDir, settingsManager });\n\t\tawait resourceLoader.reload();\n\t\ttime(\"resourceLoader.reload\");\n\t}\n\n\t// Check if session has existing data to restore\n\tconst existingSession = sessionManager.buildSessionContext();\n\tconst hasExistingSession = existingSession.messages.length > 0;\n\tconst hasThinkingEntry = sessionManager.getBranch().some((entry) => entry.type === \"thinking_level_change\");\n\n\tlet model = options.model;\n\tlet modelFallbackMessage: string | undefined;\n\n\t// If session has data, try to restore model from it\n\tif (!model && hasExistingSession && existingSession.model) {\n\t\tconst restoredModel = modelRuntime.getModel(existingSession.model.provider, existingSession.model.modelId);\n\t\tif (restoredModel) {\n\t\t\tmodel = restoredModel;\n\t\t}\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = `Could not restore model ${existingSession.model.provider}/${existingSession.model.modelId}`;\n\t\t}\n\t}\n\n\t// If still no model, use the exact saved default or entitled hosted Auto.\n\tif (!model) {\n\t\tconst result = await findInitialModel({\n\t\t\tscopedModels: [],\n\t\t\tisContinuing: hasExistingSession,\n\t\t\tdefaultProvider: settingsManager.getDefaultProvider(),\n\t\t\tdefaultModelId: settingsManager.getDefaultModel(),\n\t\t\tdefaultThinkingLevel: settingsManager.getDefaultThinkingLevel(),\n\t\t\tmodelRuntime,\n\t\t});\n\t\tmodel = result.model;\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = formatNoModelsAvailableMessage();\n\t\t} else if (modelFallbackMessage) {\n\t\t\tmodelFallbackMessage += `. Using ${model.provider}/${model.id}`;\n\t\t}\n\t}\n\n\tlet thinkingLevel = options.thinkingLevel;\n\n\t// If session has data, restore thinking level from it\n\tif (thinkingLevel === undefined && hasExistingSession) {\n\t\tthinkingLevel = hasThinkingEntry\n\t\t\t? (existingSession.thinkingLevel as ThinkingLevel)\n\t\t\t: (settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL);\n\t}\n\n\t// Fall back to settings default\n\tif (thinkingLevel === undefined) {\n\t\tthinkingLevel = settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;\n\t}\n\n\t// Clamp to model capabilities\n\tif (!model) {\n\t\tthinkingLevel = \"off\";\n\t} else {\n\t\tthinkingLevel = clampThinkingLevel(model, thinkingLevel) as ThinkingLevel;\n\t}\n\n\tconst defaultActiveToolNames: ToolName[] = [\"read\", \"bash\", \"edit\", \"write\", \"websearch\"];\n\tconst allowedToolNames = options.tools ?? (options.noTools === \"all\" ? [] : undefined);\n\tconst excludedToolNames = options.excludeTools;\n\tconst excludedToolNameSet = excludedToolNames ? new Set(excludedToolNames) : undefined;\n\tconst initialActiveToolNames: string[] = (\n\t\toptions.tools ? [...options.tools] : options.noTools ? [] : defaultActiveToolNames\n\t).filter((name) => !excludedToolNameSet?.has(name));\n\n\tlet agent: Agent;\n\n\t// Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)\n\tconst convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {\n\t\tconst converted = convertToLlm(messages);\n\t\t// Check setting dynamically so mid-session changes take effect\n\t\tif (!settingsManager.getBlockImages()) {\n\t\t\treturn converted;\n\t\t}\n\t\t// Filter out ImageContent from all messages, replacing with text placeholder\n\t\treturn converted.map((msg) => {\n\t\t\tif (msg.role === \"user\" || msg.role === \"toolResult\") {\n\t\t\t\tconst content = msg.content;\n\t\t\t\tif (Array.isArray(content)) {\n\t\t\t\t\tconst hasImages = content.some((c) => c.type === \"image\");\n\t\t\t\t\tif (hasImages) {\n\t\t\t\t\t\tconst filteredContent = content\n\t\t\t\t\t\t\t.map((c) =>\n\t\t\t\t\t\t\t\tc.type === \"image\" ? { type: \"text\" as const, text: \"Image reading is disabled.\" } : c,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t.filter(\n\t\t\t\t\t\t\t\t(c, i, arr) =>\n\t\t\t\t\t\t\t\t\t// Dedupe consecutive \"Image reading is disabled.\" texts\n\t\t\t\t\t\t\t\t\t!(\n\t\t\t\t\t\t\t\t\t\tc.type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\tc.text === \"Image reading is disabled.\" &&\n\t\t\t\t\t\t\t\t\t\ti > 0 &&\n\t\t\t\t\t\t\t\t\t\tarr[i - 1].type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\t(arr[i - 1] as { type: \"text\"; text: string }).text === \"Image reading is disabled.\"\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\treturn { ...msg, content: filteredContent };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn msg;\n\t\t});\n\t};\n\n\tconst extensionRunnerRef: { current?: ExtensionRunner } = {};\n\n\tagent = new Agent({\n\t\tinitialState: {\n\t\t\tsystemPrompt: \"\",\n\t\t\tmodel,\n\t\t\tthinkingLevel,\n\t\t\ttools: [],\n\t\t},\n\t\tconvertToLlm: convertToLlmWithBlockImages,\n\t\tstreamFn: async (model, context, options) => {\n\t\t\tconst providerRetrySettings = settingsManager.getProviderRetrySettings();\n\t\t\tconst httpIdleTimeoutMs = settingsManager.getHttpIdleTimeoutMs();\n\t\t\t// SDKs treat timeout=0 as 0ms (immediate timeout), not \"no timeout\".\n\t\t\t// Use max int32 to effectively disable the timeout.\n\t\t\tconst effectiveTimeoutMs = httpIdleTimeoutMs === 0 ? 2147483647 : httpIdleTimeoutMs;\n\t\t\tconst timeoutMs = options?.timeoutMs ?? providerRetrySettings.timeoutMs ?? effectiveTimeoutMs;\n\t\t\tconst websocketConnectTimeoutMs =\n\t\t\t\toptions?.websocketConnectTimeoutMs ?? settingsManager.getWebSocketConnectTimeoutMs();\n\t\t\tconst headerRunner = extensionRunnerRef.current;\n\t\t\treturn modelRuntime.streamSimple(model, context, {\n\t\t\t\t...options,\n\t\t\t\ttimeoutMs,\n\t\t\t\twebsocketConnectTimeoutMs,\n\t\t\t\tmaxRetries: options?.maxRetries ?? providerRetrySettings.maxRetries,\n\t\t\t\tmaxRetryDelayMs: options?.maxRetryDelayMs ?? providerRetrySettings.maxRetryDelayMs,\n\t\t\t\ttransformHeaders: async (requestHeaders) => {\n\t\t\t\t\tconst headers = mergeProviderAttributionHeaders(\n\t\t\t\t\t\tmodel,\n\t\t\t\t\t\tsettingsManager,\n\t\t\t\t\t\toptions?.sessionId,\n\t\t\t\t\t\trequestHeaders,\n\t\t\t\t\t);\n\t\t\t\t\treturn headerRunner?.hasHandlers(\"before_provider_headers\")\n\t\t\t\t\t\t? headerRunner.emitBeforeProviderHeaders(headers ?? {})\n\t\t\t\t\t\t: (headers ?? {});\n\t\t\t\t},\n\t\t\t});\n\t\t},\n\t\tonPayload: async (payload, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"before_provider_request\")) {\n\t\t\t\treturn payload;\n\t\t\t}\n\t\t\treturn runner.emitBeforeProviderRequest(payload);\n\t\t},\n\t\tonResponse: async (response, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"after_provider_response\")) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait runner.emit({\n\t\t\t\ttype: \"after_provider_response\",\n\t\t\t\tstatus: response.status,\n\t\t\t\theaders: response.headers,\n\t\t\t});\n\t\t},\n\t\tsessionId: sessionManager.getSessionId(),\n\t\ttransformContext: async (messages) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner) return messages;\n\t\t\treturn runner.emitContext(messages);\n\t\t},\n\t\tsteeringMode: settingsManager.getSteeringMode(),\n\t\tfollowUpMode: settingsManager.getFollowUpMode(),\n\t\ttransport: settingsManager.getTransport(),\n\t\tthinkingBudgets: settingsManager.getThinkingBudgets(),\n\t\tmaxRetryDelayMs: settingsManager.getProviderRetrySettings().maxRetryDelayMs,\n\t});\n\n\t// Restore messages if session has existing data\n\tif (hasExistingSession) {\n\t\tagent.state.messages = existingSession.messages;\n\t\tif (!hasThinkingEntry) {\n\t\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t\t}\n\t} else {\n\t\t// Save initial model and thinking level for new sessions so they can be restored on resume\n\t\tif (model) {\n\t\t\tsessionManager.appendModelChange(model.provider, model.id);\n\t\t}\n\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t}\n\n\tconst session = new AgentSession({\n\t\tagent,\n\t\tsessionManager,\n\t\tsettingsManager,\n\t\tcwd,\n\t\tscopedModels: options.scopedModels,\n\t\tresourceLoader,\n\t\tcustomTools: options.customTools,\n\t\tmodelRuntime,\n\t\tinitialActiveToolNames,\n\t\tallowedToolNames,\n\t\texcludedToolNames,\n\t\textensionRunnerRef,\n\t\tsessionStartEvent: options.sessionStartEvent,\n\t});\n\tconst extensionsResult = resourceLoader.getExtensions();\n\n\treturn {\n\t\tsession,\n\t\textensionsResult,\n\t\tmodelFallbackMessage,\n\t};\n}\n"]}
1
+ {"version":3,"file":"sdk.js","sourceRoot":"","sources":["../../src/core/sdk.ts"],"names":[],"mappings":"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","sourcesContent":["import { join } from \"node:path\";\nimport { Agent, type AgentMessage, setDefaultStreamFn, type ThinkingLevel } from \"@earendil-works/pi-agent-core\";\nimport { clampThinkingLevel, type Message, type Model, streamSimple } from \"@earendil-works/pi-ai/compat\";\nimport { getAgentDir } from \"../config.ts\";\nimport { resolvePath } from \"../utils/paths.ts\";\nimport { AgentSession } from \"./agent-session.ts\";\nimport { formatNoModelsAvailableMessage } from \"./auth-guidance.ts\";\nimport { DEFAULT_THINKING_LEVEL } from \"./defaults.ts\";\nimport type { ExtensionRunner, LoadExtensionsResult, SessionStartEvent, ToolDefinition } from \"./extensions/index.ts\";\nimport { convertToLlm } from \"./messages.ts\";\nimport { findInitialModel } from \"./model-resolver.ts\";\nimport { ModelRuntime } from \"./model-runtime.ts\";\nimport { mergeProviderAttributionHeaders } from \"./provider-attribution.ts\";\nimport type { ResourceLoader } from \"./resource-loader.ts\";\nimport { DefaultResourceLoader } from \"./resource-loader.ts\";\nimport { getDefaultSessionDir, SessionManager } from \"./session-manager.ts\";\nimport { SettingsManager } from \"./settings-manager.ts\";\nimport { time } from \"./timings.ts\";\nimport {\n\tcreateBashTool,\n\tcreateCodingTools,\n\tcreateEditTool,\n\tcreateFindTool,\n\tcreateGrepTool,\n\tcreateLsTool,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateWriteTool,\n\ttype ToolName,\n\twithFileMutationQueue,\n} from \"./tools/index.ts\";\n\n// Preserve the pre-0.81 fallback for extensions that construct Agent instances\n// or invoke low-level agent loops without supplying streamFn. Agent core remains\n// provider-agnostic and does not import pi-ai/compat itself.\nsetDefaultStreamFn(streamSimple);\n\nexport interface CreateAgentSessionOptions {\n\t/** Working directory for project-local discovery. Default: process.cwd() */\n\tcwd?: string;\n\t/** Global config directory. Default: ~/.pi/agent */\n\tagentDir?: string;\n\n\t/** Canonical model/auth runtime. Defaults to a runtime using agentDir/auth.json and models.json. */\n\tmodelRuntime?: ModelRuntime;\n\n\t/** Model to use. Default: exact saved setting; never another provider. */\n\tmodel?: Model<any>;\n\t/** Thinking level. Default: from settings, else 'medium' (clamped to model capabilities) */\n\tthinkingLevel?: ThinkingLevel;\n\t/** Models available for cycling (Ctrl+P in interactive mode) */\n\tscopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;\n\n\t/**\n\t * Optional default tool suppression mode when no explicit allowlist is provided.\n\t *\n\t * - \"all\": start with no tools enabled\n\t * - \"builtin\": disable the default built-in tools (read, bash, edit, write)\n\t * but keep extension/custom tools enabled\n\t */\n\tnoTools?: \"all\" | \"builtin\";\n\t/**\n\t * Optional allowlist of tool names.\n\t *\n\t * When omitted, pi enables the default built-in tools (read, bash, edit, write)\n\t * and leaves extension/custom tools enabled unless `noTools` changes that default.\n\t * When provided, only the listed tool names are enabled.\n\t */\n\ttools?: string[];\n\t/** Optional denylist of tool names to disable. Applies after `tools` when both are provided. */\n\texcludeTools?: string[];\n\t/** Custom tools to register (in addition to built-in tools). */\n\tcustomTools?: ToolDefinition[];\n\n\t/** Resource loader. When omitted, DefaultResourceLoader is used. */\n\tresourceLoader?: ResourceLoader;\n\n\t/** Session manager. Default: SessionManager.create(cwd) */\n\tsessionManager?: SessionManager;\n\n\t/** Settings manager. Default: SettingsManager.create(cwd, agentDir) */\n\tsettingsManager?: SettingsManager;\n\t/** Session start event metadata for extension runtime startup. */\n\tsessionStartEvent?: SessionStartEvent;\n}\n\n/** Result from createAgentSession */\nexport interface CreateAgentSessionResult {\n\t/** The created session */\n\tsession: AgentSession;\n\t/** Extensions result (for UI context setup in interactive mode) */\n\textensionsResult: LoadExtensionsResult;\n\t/** Warning if session was restored with a different model than saved */\n\tmodelFallbackMessage?: string;\n}\n\n// Re-exports\n\nexport * from \"./agent-session-runtime.ts\";\nexport type {\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tExtensionFactory,\n\tInlineExtension,\n\tSlashCommandInfo,\n\tSlashCommandSource,\n\tToolDefinition,\n} from \"./extensions/index.ts\";\nexport type { PromptTemplate } from \"./prompt-templates.ts\";\nexport type { Skill } from \"./skills.ts\";\nexport type { Tool } from \"./tools/index.ts\";\n\nexport {\n\twithFileMutationQueue,\n\t// Tool factories (for custom cwd)\n\tcreateCodingTools,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateBashTool,\n\tcreateEditTool,\n\tcreateWriteTool,\n\tcreateGrepTool,\n\tcreateFindTool,\n\tcreateLsTool,\n};\n\n// Helper Functions\n\nfunction getDefaultAgentDir(): string {\n\treturn getAgentDir();\n}\n\n/**\n * Create an AgentSession with the specified options.\n *\n * @example\n * ```typescript\n * // Minimal - uses defaults\n * const { session } = await createAgentSession();\n *\n * // With explicit model\n * import { getModel } from '@earendil-works/pi-ai';\n * const { session } = await createAgentSession({\n * model: getModel('anthropic', 'claude-opus-4-5'),\n * thinkingLevel: 'high',\n * });\n *\n * // Continue previous session\n * const { session, modelFallbackMessage } = await createAgentSession({\n * continueSession: true,\n * });\n *\n * // Full control\n * const loader = new DefaultResourceLoader({\n * cwd: process.cwd(),\n * agentDir: getAgentDir(),\n * settingsManager: SettingsManager.create(),\n * });\n * await loader.reload();\n * const { session } = await createAgentSession({\n * model: myModel,\n * tools: [\"read\", \"bash\"],\n * resourceLoader: loader,\n * sessionManager: SessionManager.inMemory(),\n * });\n * ```\n */\nexport async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {\n\tconst cwd = resolvePath(options.cwd ?? options.sessionManager?.getCwd() ?? process.cwd());\n\tconst agentDir = options.agentDir ? resolvePath(options.agentDir) : getDefaultAgentDir();\n\tlet resourceLoader = options.resourceLoader;\n\n\tconst authPath = options.agentDir ? join(agentDir, \"auth.json\") : undefined;\n\tconst modelsPath = options.agentDir ? join(agentDir, \"models.json\") : undefined;\n\tconst modelRuntime = options.modelRuntime ?? (await ModelRuntime.create({ authPath, modelsPath }));\n\n\tconst settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);\n\tconst sessionManager = options.sessionManager ?? SessionManager.create(cwd, getDefaultSessionDir(cwd, agentDir));\n\n\tif (!resourceLoader) {\n\t\tresourceLoader = new DefaultResourceLoader({ cwd, agentDir, settingsManager });\n\t\tawait resourceLoader.reload();\n\t\ttime(\"resourceLoader.reload\");\n\t}\n\n\t// Check if session has existing data to restore\n\tconst existingSession = sessionManager.buildSessionContext();\n\tconst hasExistingSession = existingSession.messages.length > 0;\n\tconst hasThinkingEntry = sessionManager.getBranch().some((entry) => entry.type === \"thinking_level_change\");\n\n\tlet model = options.model;\n\tlet modelFallbackMessage: string | undefined;\n\n\t// If session has data, try to restore model from it\n\tif (!model && hasExistingSession && existingSession.model) {\n\t\tconst restoredModel = modelRuntime.getModel(existingSession.model.provider, existingSession.model.modelId);\n\t\tif (restoredModel) {\n\t\t\tmodel = restoredModel;\n\t\t}\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = `Could not restore model ${existingSession.model.provider}/${existingSession.model.modelId}`;\n\t\t}\n\t}\n\n\t// If still no model, use only the exact saved default. Nexus never switches providers automatically.\n\tif (!model) {\n\t\tconst result = await findInitialModel({\n\t\t\tscopedModels: [],\n\t\t\tisContinuing: hasExistingSession,\n\t\t\tdefaultProvider: settingsManager.getDefaultProvider(),\n\t\t\tdefaultModelId: settingsManager.getDefaultModel(),\n\t\t\tdefaultThinkingLevel: settingsManager.getDefaultThinkingLevel(),\n\t\t\tmodelRuntime,\n\t\t});\n\t\tmodel = result.model;\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = formatNoModelsAvailableMessage();\n\t\t} else if (modelFallbackMessage) {\n\t\t\tmodelFallbackMessage += `. Using ${model.provider}/${model.id}`;\n\t\t}\n\t}\n\n\tlet thinkingLevel = options.thinkingLevel;\n\n\t// If session has data, restore thinking level from it\n\tif (thinkingLevel === undefined && hasExistingSession) {\n\t\tthinkingLevel = hasThinkingEntry\n\t\t\t? (existingSession.thinkingLevel as ThinkingLevel)\n\t\t\t: (settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL);\n\t}\n\n\t// Fall back to settings default\n\tif (thinkingLevel === undefined) {\n\t\tthinkingLevel = settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;\n\t}\n\n\t// Clamp to model capabilities\n\tif (!model) {\n\t\tthinkingLevel = \"off\";\n\t} else {\n\t\tthinkingLevel = clampThinkingLevel(model, thinkingLevel) as ThinkingLevel;\n\t}\n\n\tconst defaultActiveToolNames: ToolName[] = [\"read\", \"bash\", \"edit\", \"write\", \"websearch\"];\n\tconst allowedToolNames = options.tools ?? (options.noTools === \"all\" ? [] : undefined);\n\tconst excludedToolNames = options.excludeTools;\n\tconst excludedToolNameSet = excludedToolNames ? new Set(excludedToolNames) : undefined;\n\tconst initialActiveToolNames: string[] = (\n\t\toptions.tools ? [...options.tools] : options.noTools ? [] : defaultActiveToolNames\n\t).filter((name) => !excludedToolNameSet?.has(name));\n\n\tlet agent: Agent;\n\n\t// Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)\n\tconst convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {\n\t\tconst converted = convertToLlm(messages);\n\t\t// Check setting dynamically so mid-session changes take effect\n\t\tif (!settingsManager.getBlockImages()) {\n\t\t\treturn converted;\n\t\t}\n\t\t// Filter out ImageContent from all messages, replacing with text placeholder\n\t\treturn converted.map((msg) => {\n\t\t\tif (msg.role === \"user\" || msg.role === \"toolResult\") {\n\t\t\t\tconst content = msg.content;\n\t\t\t\tif (Array.isArray(content)) {\n\t\t\t\t\tconst hasImages = content.some((c) => c.type === \"image\");\n\t\t\t\t\tif (hasImages) {\n\t\t\t\t\t\tconst filteredContent = content\n\t\t\t\t\t\t\t.map((c) =>\n\t\t\t\t\t\t\t\tc.type === \"image\" ? { type: \"text\" as const, text: \"Image reading is disabled.\" } : c,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t.filter(\n\t\t\t\t\t\t\t\t(c, i, arr) =>\n\t\t\t\t\t\t\t\t\t// Dedupe consecutive \"Image reading is disabled.\" texts\n\t\t\t\t\t\t\t\t\t!(\n\t\t\t\t\t\t\t\t\t\tc.type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\tc.text === \"Image reading is disabled.\" &&\n\t\t\t\t\t\t\t\t\t\ti > 0 &&\n\t\t\t\t\t\t\t\t\t\tarr[i - 1].type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\t(arr[i - 1] as { type: \"text\"; text: string }).text === \"Image reading is disabled.\"\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\treturn { ...msg, content: filteredContent };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn msg;\n\t\t});\n\t};\n\n\tconst extensionRunnerRef: { current?: ExtensionRunner } = {};\n\n\tagent = new Agent({\n\t\tinitialState: {\n\t\t\tsystemPrompt: \"\",\n\t\t\tmodel,\n\t\t\tthinkingLevel,\n\t\t\ttools: [],\n\t\t},\n\t\tconvertToLlm: convertToLlmWithBlockImages,\n\t\tstreamFn: async (model, context, options) => {\n\t\t\tconst providerRetrySettings = settingsManager.getProviderRetrySettings();\n\t\t\tconst httpIdleTimeoutMs = settingsManager.getHttpIdleTimeoutMs();\n\t\t\t// SDKs treat timeout=0 as 0ms (immediate timeout), not \"no timeout\".\n\t\t\t// Use max int32 to effectively disable the timeout.\n\t\t\tconst effectiveTimeoutMs = httpIdleTimeoutMs === 0 ? 2147483647 : httpIdleTimeoutMs;\n\t\t\tconst timeoutMs = options?.timeoutMs ?? providerRetrySettings.timeoutMs ?? effectiveTimeoutMs;\n\t\t\tconst websocketConnectTimeoutMs =\n\t\t\t\toptions?.websocketConnectTimeoutMs ?? settingsManager.getWebSocketConnectTimeoutMs();\n\t\t\tconst headerRunner = extensionRunnerRef.current;\n\t\t\treturn modelRuntime.streamSimple(model, context, {\n\t\t\t\t...options,\n\t\t\t\ttimeoutMs,\n\t\t\t\twebsocketConnectTimeoutMs,\n\t\t\t\tmaxRetries: options?.maxRetries ?? providerRetrySettings.maxRetries,\n\t\t\t\tmaxRetryDelayMs: options?.maxRetryDelayMs ?? providerRetrySettings.maxRetryDelayMs,\n\t\t\t\ttransformHeaders: async (requestHeaders) => {\n\t\t\t\t\tconst headers = mergeProviderAttributionHeaders(\n\t\t\t\t\t\tmodel,\n\t\t\t\t\t\tsettingsManager,\n\t\t\t\t\t\toptions?.sessionId,\n\t\t\t\t\t\trequestHeaders,\n\t\t\t\t\t);\n\t\t\t\t\treturn headerRunner?.hasHandlers(\"before_provider_headers\")\n\t\t\t\t\t\t? headerRunner.emitBeforeProviderHeaders(headers ?? {})\n\t\t\t\t\t\t: (headers ?? {});\n\t\t\t\t},\n\t\t\t});\n\t\t},\n\t\tonPayload: async (payload, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"before_provider_request\")) {\n\t\t\t\treturn payload;\n\t\t\t}\n\t\t\treturn runner.emitBeforeProviderRequest(payload);\n\t\t},\n\t\tonResponse: async (response, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"after_provider_response\")) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait runner.emit({\n\t\t\t\ttype: \"after_provider_response\",\n\t\t\t\tstatus: response.status,\n\t\t\t\theaders: response.headers,\n\t\t\t});\n\t\t},\n\t\tsessionId: sessionManager.getSessionId(),\n\t\ttransformContext: async (messages) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner) return messages;\n\t\t\treturn runner.emitContext(messages);\n\t\t},\n\t\tsteeringMode: settingsManager.getSteeringMode(),\n\t\tfollowUpMode: settingsManager.getFollowUpMode(),\n\t\ttransport: settingsManager.getTransport(),\n\t\tthinkingBudgets: settingsManager.getThinkingBudgets(),\n\t\tmaxRetryDelayMs: settingsManager.getProviderRetrySettings().maxRetryDelayMs,\n\t});\n\n\t// Restore messages if session has existing data\n\tif (hasExistingSession) {\n\t\tagent.state.messages = existingSession.messages;\n\t\tif (!hasThinkingEntry) {\n\t\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t\t}\n\t} else {\n\t\t// Save initial model and thinking level for new sessions so they can be restored on resume\n\t\tif (model) {\n\t\t\tsessionManager.appendModelChange(model.provider, model.id);\n\t\t}\n\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t}\n\n\tconst session = new AgentSession({\n\t\tagent,\n\t\tsessionManager,\n\t\tsettingsManager,\n\t\tcwd,\n\t\tscopedModels: options.scopedModels,\n\t\tresourceLoader,\n\t\tcustomTools: options.customTools,\n\t\tmodelRuntime,\n\t\tinitialActiveToolNames,\n\t\tallowedToolNames,\n\t\texcludedToolNames,\n\t\textensionRunnerRef,\n\t\tsessionStartEvent: options.sessionStartEvent,\n\t});\n\tconst extensionsResult = resourceLoader.getExtensions();\n\n\treturn {\n\t\tsession,\n\t\textensionsResult,\n\t\tmodelFallbackMessage,\n\t};\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"websearch.d.ts","sourceRoot":"","sources":["../../../src/core/tools/websearch.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAsBtF,QAAA,MAAM,eAAe;;;EAKnB,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,OAAO,eAAe,CAAC,CAAC;AAEhE,MAAM,WAAW,oBAAoB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,aAAa,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACpC,oDAAoD;IACpD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAOD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAcrD;AAED,iEAAiE;AACjE,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAoB1D;AAED,wBAAgB,6BAA6B,CAC5C,OAAO,CAAC,EAAE,oBAAoB,GAC5B,cAAc,CAAC,OAAO,eAAe,EAAE,oBAAoB,GAAG,SAAS,CAAC,CA4F1E;AAED,eAAO,MAAM,mBAAmB,0IAC2B,CAAC","sourcesContent":["import { Text } from \"@earendil-works/pi-tui\";\nimport { type Static, Type } from \"typebox\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.ts\";\nimport { getTextOutput, str } from \"./render-utils.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\n\n/**\n * Web search through Exa's hosted MCP endpoint.\n *\n * The endpoint answers unauthenticated requests on a free plan, so search works\n * with no configuration at all. EXA_API_KEY lifts the rate limit and is what a\n * deployment should use — Exa's own guidance is to authenticate for sustained\n * use, and the anonymous pool is shared with every other unauthenticated caller\n * on the internet.\n *\n * Only the transport is spoken here rather than pulling in an MCP client: this\n * is one JSON-RPC method against one known endpoint, and the response is a\n * text block either way.\n */\n\nconst EXA_MCP_URL = \"https://mcp.exa.ai/mcp\";\nconst DEFAULT_RESULTS = 8;\nconst REQUEST_TIMEOUT_MS = 25_000;\n\nconst websearchSchema = Type.Object({\n\tquery: Type.String({ description: \"What to search the web for\" }),\n\tnumResults: Type.Optional(\n\t\tType.Number({ description: `Maximum number of results to return (default: ${DEFAULT_RESULTS})` }),\n\t),\n});\n\nexport type WebSearchToolInput = Static<typeof websearchSchema>;\n\nexport interface WebSearchToolDetails {\n\tquery: string;\n\t/** True when a key was used, so the UI can explain a rate limit accurately. */\n\tauthenticated: boolean;\n}\n\nexport interface WebSearchToolOptions {\n\t/** Overridable so tests never reach the network. */\n\tfetchImpl?: typeof fetch;\n\tapiKey?: string | undefined;\n}\n\n/** Exa reads the key from a query parameter on the hosted endpoint. */\nfunction endpointFor(apiKey: string | undefined): string {\n\treturn apiKey ? `${EXA_MCP_URL}?exaApiKey=${encodeURIComponent(apiKey)}` : EXA_MCP_URL;\n}\n\n/**\n * The endpoint answers `application/json` or an SSE stream depending on how it\n * feels; both carry the same JSON-RPC envelope, so pull the payload out of\n * whichever arrived.\n */\nexport function parseMcpPayload(body: string): unknown {\n\tconst trimmed = body.trim();\n\tif (!trimmed) throw new Error(\"Search returned an empty response\");\n\n\tif (trimmed.startsWith(\"{\")) return JSON.parse(trimmed);\n\n\t// SSE: one or more `data:` lines, last one wins.\n\tconst dataLines = trimmed\n\t\t.split(\"\\n\")\n\t\t.filter((line) => line.startsWith(\"data:\"))\n\t\t.map((line) => line.slice(5).trim());\n\tconst last = dataLines.at(-1);\n\tif (!last) throw new Error(\"Search returned an unrecognized response\");\n\treturn JSON.parse(last);\n}\n\n/** Flatten the MCP tool result into the text the model reads. */\nexport function extractResultText(payload: unknown): string {\n\tconst envelope = payload as {\n\t\terror?: { message?: string };\n\t\tresult?: { content?: Array<{ type?: string; text?: string }>; isError?: boolean };\n\t};\n\n\tif (envelope?.error?.message) throw new Error(envelope.error.message);\n\n\tconst content = envelope?.result?.content;\n\tif (!Array.isArray(content)) throw new Error(\"Search returned no results\");\n\n\tconst text = content\n\t\t.filter((part) => part?.type === \"text\" && typeof part.text === \"string\")\n\t\t.map((part) => part.text)\n\t\t.join(\"\\n\")\n\t\t.trim();\n\n\tif (!text) throw new Error(\"Search returned no results\");\n\tif (envelope.result?.isError) throw new Error(text);\n\treturn text;\n}\n\nexport function createWebSearchToolDefinition(\n\toptions?: WebSearchToolOptions,\n): ToolDefinition<typeof websearchSchema, WebSearchToolDetails | undefined> {\n\treturn {\n\t\tname: \"websearch\",\n\t\tlabel: \"websearch\",\n\t\tdescription:\n\t\t\t\"Search the web and return ranked results with excerpts. Use for current information, documentation, error messages, and anything outside the repository.\",\n\t\tpromptSnippet: \"Search the web for current information\",\n\t\tpromptGuidelines: [\"Use websearch for anything outside the repository; do not guess at current facts.\"],\n\t\tparameters: websearchSchema,\n\t\tasync execute(_toolCallId, { query, numResults }, signal?: AbortSignal) {\n\t\t\tconst doFetch = options?.fetchImpl ?? fetch;\n\t\t\tconst apiKey = options?.apiKey ?? process.env.EXA_API_KEY;\n\n\t\t\t// Bound the request ourselves and honour an outer abort, so a hung\n\t\t\t// endpoint cannot wedge the turn.\n\t\t\tconst controller = new AbortController();\n\t\t\tconst timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);\n\t\t\tconst onAbort = () => controller.abort();\n\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\ttry {\n\t\t\t\tlet response: Response;\n\t\t\t\ttry {\n\t\t\t\t\tresponse = await doFetch(endpointFor(apiKey), {\n\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\theaders: { \"content-type\": \"application/json\", accept: \"application/json, text/event-stream\" },\n\t\t\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\t\t\tjsonrpc: \"2.0\",\n\t\t\t\t\t\t\tid: 1,\n\t\t\t\t\t\t\tmethod: \"tools/call\",\n\t\t\t\t\t\t\tparams: {\n\t\t\t\t\t\t\t\tname: \"web_search_exa\",\n\t\t\t\t\t\t\t\targuments: { query, numResults: numResults ?? DEFAULT_RESULTS, type: \"auto\" },\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}),\n\t\t\t\t\t\tsignal: controller.signal,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\t\t\tif (controller.signal.aborted) {\n\t\t\t\t\t\tthrow new Error(`Search timed out after ${REQUEST_TIMEOUT_MS / 1000}s`);\n\t\t\t\t\t}\n\t\t\t\t\tthrow new Error(`Search request failed: ${error instanceof Error ? error.message : String(error)}`);\n\t\t\t\t}\n\n\t\t\t\t// A rate limit is the one failure worth naming: it is expected on the\n\t\t\t\t// free plan, and the fix is specific rather than \"try again\".\n\t\t\t\tif (response.status === 429) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\tapiKey\n\t\t\t\t\t\t\t? \"Search rate limit reached for this Exa API key.\"\n\t\t\t\t\t\t\t: \"Search rate limit reached on Exa's free plan. Set EXA_API_KEY to raise it.\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tthrow new Error(`Search failed with HTTP ${response.status}`);\n\t\t\t\t}\n\n\t\t\t\tlet body: string;\n\t\t\t\ttry {\n\t\t\t\t\tbody = await response.text();\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\t\t\tif (controller.signal.aborted) {\n\t\t\t\t\t\tthrow new Error(`Search timed out after ${REQUEST_TIMEOUT_MS / 1000}s`);\n\t\t\t\t\t}\n\t\t\t\t\tthrow new Error(`Search response failed: ${error instanceof Error ? error.message : String(error)}`);\n\t\t\t\t}\n\n\t\t\t\tconst text = extractResultText(parseMcpPayload(body));\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\t\tdetails: { query, authenticated: apiKey !== undefined },\n\t\t\t\t};\n\t\t\t} finally {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t}\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\tconst query = str(args?.query);\n\t\t\ttext.setText(query ? theme.fg(\"muted\", query) : \"\");\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, _options: ToolRenderResultOptions, theme: Theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\t// Results are text only; images never appear in a search payload.\n\t\t\ttext.setText(theme.fg(\"muted\", getTextOutput(result as never, false)));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport const createWebSearchTool = (options?: WebSearchToolOptions) =>\n\twrapToolDefinition(createWebSearchToolDefinition(options));\n"]}
1
+ {"version":3,"file":"websearch.d.ts","sourceRoot":"","sources":["../../../src/core/tools/websearch.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAsBtF,QAAA,MAAM,eAAe;;;EAKnB,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,OAAO,eAAe,CAAC,CAAC;AAEhE,MAAM,WAAW,oBAAoB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,aAAa,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACpC,oDAAoD;IACpD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAOD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAcrD;AAED,iEAAiE;AACjE,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAoB1D;AAED,wBAAgB,6BAA6B,CAC5C,OAAO,CAAC,EAAE,oBAAoB,GAC5B,cAAc,CAAC,OAAO,eAAe,EAAE,oBAAoB,GAAG,SAAS,CAAC,CA6E1E;AAED,eAAO,MAAM,mBAAmB,0IAC2B,CAAC","sourcesContent":["import { Text } from \"@earendil-works/pi-tui\";\nimport { type Static, Type } from \"typebox\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.ts\";\nimport { getTextOutput, str } from \"./render-utils.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\n\n/**\n * Web search through Exa's hosted MCP endpoint.\n *\n * The endpoint answers unauthenticated requests on a free plan, so search works\n * with no configuration at all. EXA_API_KEY lifts the rate limit and is what a\n * deployment should use — Exa's own guidance is to authenticate for sustained\n * use, and the anonymous pool is shared with every other unauthenticated caller\n * on the internet.\n *\n * Only the transport is spoken here rather than pulling in an MCP client: this\n * is one JSON-RPC method against one known endpoint, and the response is a\n * text block either way.\n */\n\nconst EXA_MCP_URL = \"https://mcp.exa.ai/mcp\";\nconst DEFAULT_RESULTS = 8;\nconst REQUEST_TIMEOUT_MS = 25_000;\n\nconst websearchSchema = Type.Object({\n\tquery: Type.String({ description: \"What to search the web for\" }),\n\tnumResults: Type.Optional(\n\t\tType.Number({ description: `Maximum number of results to return (default: ${DEFAULT_RESULTS})` }),\n\t),\n});\n\nexport type WebSearchToolInput = Static<typeof websearchSchema>;\n\nexport interface WebSearchToolDetails {\n\tquery: string;\n\t/** True when a key was used, so the UI can explain a rate limit accurately. */\n\tauthenticated: boolean;\n}\n\nexport interface WebSearchToolOptions {\n\t/** Overridable so tests never reach the network. */\n\tfetchImpl?: typeof fetch;\n\tapiKey?: string | undefined;\n}\n\n/** Exa reads the key from a query parameter on the hosted endpoint. */\nfunction endpointFor(apiKey: string | undefined): string {\n\treturn apiKey ? `${EXA_MCP_URL}?exaApiKey=${encodeURIComponent(apiKey)}` : EXA_MCP_URL;\n}\n\n/**\n * The endpoint answers `application/json` or an SSE stream depending on how it\n * feels; both carry the same JSON-RPC envelope, so pull the payload out of\n * whichever arrived.\n */\nexport function parseMcpPayload(body: string): unknown {\n\tconst trimmed = body.trim();\n\tif (!trimmed) throw new Error(\"Search returned an empty response\");\n\n\tif (trimmed.startsWith(\"{\")) return JSON.parse(trimmed);\n\n\t// SSE: one or more `data:` lines, last one wins.\n\tconst dataLines = trimmed\n\t\t.split(\"\\n\")\n\t\t.filter((line) => line.startsWith(\"data:\"))\n\t\t.map((line) => line.slice(5).trim());\n\tconst last = dataLines.at(-1);\n\tif (!last) throw new Error(\"Search returned an unrecognized response\");\n\treturn JSON.parse(last);\n}\n\n/** Flatten the MCP tool result into the text the model reads. */\nexport function extractResultText(payload: unknown): string {\n\tconst envelope = payload as {\n\t\terror?: { message?: string };\n\t\tresult?: { content?: Array<{ type?: string; text?: string }>; isError?: boolean };\n\t};\n\n\tif (envelope?.error?.message) throw new Error(envelope.error.message);\n\n\tconst content = envelope?.result?.content;\n\tif (!Array.isArray(content)) throw new Error(\"Search returned no results\");\n\n\tconst text = content\n\t\t.filter((part) => part?.type === \"text\" && typeof part.text === \"string\")\n\t\t.map((part) => part.text)\n\t\t.join(\"\\n\")\n\t\t.trim();\n\n\tif (!text) throw new Error(\"Search returned no results\");\n\tif (envelope.result?.isError) throw new Error(text);\n\treturn text;\n}\n\nexport function createWebSearchToolDefinition(\n\toptions?: WebSearchToolOptions,\n): ToolDefinition<typeof websearchSchema, WebSearchToolDetails | undefined> {\n\treturn {\n\t\tname: \"websearch\",\n\t\tlabel: \"websearch\",\n\t\tdescription:\n\t\t\t\"Search the web and return ranked results with excerpts. Use for current information, documentation, error messages, and anything outside the repository.\",\n\t\tpromptSnippet: \"Search the web for current information\",\n\t\tpromptGuidelines: [\"Use websearch for anything outside the repository; do not guess at current facts.\"],\n\t\tparameters: websearchSchema,\n\t\tasync execute(_toolCallId, { query, numResults }, signal?: AbortSignal) {\n\t\t\tconst doFetch = options?.fetchImpl ?? fetch;\n\t\t\tconst apiKey = options?.apiKey ?? process.env.EXA_API_KEY;\n\n\t\t\t// Bound the request ourselves and honour an outer abort, so a hung\n\t\t\t// endpoint cannot wedge the turn.\n\t\t\tconst controller = new AbortController();\n\t\t\tconst timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);\n\t\t\tconst onAbort = () => controller.abort();\n\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\tlet response: Response;\n\t\t\ttry {\n\t\t\t\tresponse = await doFetch(endpointFor(apiKey), {\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\theaders: { \"content-type\": \"application/json\", accept: \"application/json, text/event-stream\" },\n\t\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\t\tjsonrpc: \"2.0\",\n\t\t\t\t\t\tid: 1,\n\t\t\t\t\t\tmethod: \"tools/call\",\n\t\t\t\t\t\tparams: {\n\t\t\t\t\t\t\tname: \"web_search_exa\",\n\t\t\t\t\t\t\targuments: { query, numResults: numResults ?? DEFAULT_RESULTS, type: \"auto\" },\n\t\t\t\t\t\t},\n\t\t\t\t\t}),\n\t\t\t\t\tsignal: controller.signal,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\t\tif (controller.signal.aborted) throw new Error(`Search timed out after ${REQUEST_TIMEOUT_MS / 1000}s`);\n\t\t\t\tthrow new Error(`Search request failed: ${error instanceof Error ? error.message : String(error)}`);\n\t\t\t} finally {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t}\n\n\t\t\t// A rate limit is the one failure worth naming: it is expected on the\n\t\t\t// free plan, and the fix is specific rather than \"try again\".\n\t\t\tif (response.status === 429) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\tapiKey\n\t\t\t\t\t\t? \"Search rate limit reached for this Exa API key.\"\n\t\t\t\t\t\t: \"Search rate limit reached on Exa's free plan. Set EXA_API_KEY to raise it.\",\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new Error(`Search failed with HTTP ${response.status}`);\n\t\t\t}\n\n\t\t\tconst text = extractResultText(parseMcpPayload(await response.text()));\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\tdetails: { query, authenticated: apiKey !== undefined },\n\t\t\t};\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\tconst query = str(args?.query);\n\t\t\ttext.setText(query ? theme.fg(\"muted\", query) : \"\");\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, _options: ToolRenderResultOptions, theme: Theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\t// Results are text only; images never appear in a search payload.\n\t\t\ttext.setText(theme.fg(\"muted\", getTextOutput(result as never, false)));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport const createWebSearchTool = (options?: WebSearchToolOptions) =>\n\twrapToolDefinition(createWebSearchToolDefinition(options));\n"]}
@@ -83,64 +83,49 @@ export function createWebSearchToolDefinition(options) {
83
83
  const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
84
84
  const onAbort = () => controller.abort();
85
85
  signal?.addEventListener("abort", onAbort, { once: true });
86
+ let response;
86
87
  try {
87
- let response;
88
- try {
89
- response = await doFetch(endpointFor(apiKey), {
90
- method: "POST",
91
- headers: { "content-type": "application/json", accept: "application/json, text/event-stream" },
92
- body: JSON.stringify({
93
- jsonrpc: "2.0",
94
- id: 1,
95
- method: "tools/call",
96
- params: {
97
- name: "web_search_exa",
98
- arguments: { query, numResults: numResults ?? DEFAULT_RESULTS, type: "auto" },
99
- },
100
- }),
101
- signal: controller.signal,
102
- });
103
- }
104
- catch (error) {
105
- if (signal?.aborted)
106
- throw new Error("Operation aborted");
107
- if (controller.signal.aborted) {
108
- throw new Error(`Search timed out after ${REQUEST_TIMEOUT_MS / 1000}s`);
109
- }
110
- throw new Error(`Search request failed: ${error instanceof Error ? error.message : String(error)}`);
111
- }
112
- // A rate limit is the one failure worth naming: it is expected on the
113
- // free plan, and the fix is specific rather than "try again".
114
- if (response.status === 429) {
115
- throw new Error(apiKey
116
- ? "Search rate limit reached for this Exa API key."
117
- : "Search rate limit reached on Exa's free plan. Set EXA_API_KEY to raise it.");
118
- }
119
- if (!response.ok) {
120
- throw new Error(`Search failed with HTTP ${response.status}`);
121
- }
122
- let body;
123
- try {
124
- body = await response.text();
125
- }
126
- catch (error) {
127
- if (signal?.aborted)
128
- throw new Error("Operation aborted");
129
- if (controller.signal.aborted) {
130
- throw new Error(`Search timed out after ${REQUEST_TIMEOUT_MS / 1000}s`);
131
- }
132
- throw new Error(`Search response failed: ${error instanceof Error ? error.message : String(error)}`);
133
- }
134
- const text = extractResultText(parseMcpPayload(body));
135
- return {
136
- content: [{ type: "text", text }],
137
- details: { query, authenticated: apiKey !== undefined },
138
- };
88
+ response = await doFetch(endpointFor(apiKey), {
89
+ method: "POST",
90
+ headers: { "content-type": "application/json", accept: "application/json, text/event-stream" },
91
+ body: JSON.stringify({
92
+ jsonrpc: "2.0",
93
+ id: 1,
94
+ method: "tools/call",
95
+ params: {
96
+ name: "web_search_exa",
97
+ arguments: { query, numResults: numResults ?? DEFAULT_RESULTS, type: "auto" },
98
+ },
99
+ }),
100
+ signal: controller.signal,
101
+ });
102
+ }
103
+ catch (error) {
104
+ if (signal?.aborted)
105
+ throw new Error("Operation aborted");
106
+ if (controller.signal.aborted)
107
+ throw new Error(`Search timed out after ${REQUEST_TIMEOUT_MS / 1000}s`);
108
+ throw new Error(`Search request failed: ${error instanceof Error ? error.message : String(error)}`);
139
109
  }
140
110
  finally {
141
111
  clearTimeout(timer);
142
112
  signal?.removeEventListener("abort", onAbort);
143
113
  }
114
+ // A rate limit is the one failure worth naming: it is expected on the
115
+ // free plan, and the fix is specific rather than "try again".
116
+ if (response.status === 429) {
117
+ throw new Error(apiKey
118
+ ? "Search rate limit reached for this Exa API key."
119
+ : "Search rate limit reached on Exa's free plan. Set EXA_API_KEY to raise it.");
120
+ }
121
+ if (!response.ok) {
122
+ throw new Error(`Search failed with HTTP ${response.status}`);
123
+ }
124
+ const text = extractResultText(parseMcpPayload(await response.text()));
125
+ return {
126
+ content: [{ type: "text", text }],
127
+ details: { query, authenticated: apiKey !== undefined },
128
+ };
144
129
  },
145
130
  renderCall(args, theme, context) {
146
131
  const text = context.lastComponent ?? new Text("", 0, 0);
@@ -1 +1 @@
1
- {"version":3,"file":"websearch.js","sourceRoot":"","sources":["../../../src/core/tools/websearch.ts"],"names":[],"mappings":"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","sourcesContent":["import { Text } from \"@earendil-works/pi-tui\";\nimport { type Static, Type } from \"typebox\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.ts\";\nimport { getTextOutput, str } from \"./render-utils.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\n\n/**\n * Web search through Exa's hosted MCP endpoint.\n *\n * The endpoint answers unauthenticated requests on a free plan, so search works\n * with no configuration at all. EXA_API_KEY lifts the rate limit and is what a\n * deployment should use — Exa's own guidance is to authenticate for sustained\n * use, and the anonymous pool is shared with every other unauthenticated caller\n * on the internet.\n *\n * Only the transport is spoken here rather than pulling in an MCP client: this\n * is one JSON-RPC method against one known endpoint, and the response is a\n * text block either way.\n */\n\nconst EXA_MCP_URL = \"https://mcp.exa.ai/mcp\";\nconst DEFAULT_RESULTS = 8;\nconst REQUEST_TIMEOUT_MS = 25_000;\n\nconst websearchSchema = Type.Object({\n\tquery: Type.String({ description: \"What to search the web for\" }),\n\tnumResults: Type.Optional(\n\t\tType.Number({ description: `Maximum number of results to return (default: ${DEFAULT_RESULTS})` }),\n\t),\n});\n\nexport type WebSearchToolInput = Static<typeof websearchSchema>;\n\nexport interface WebSearchToolDetails {\n\tquery: string;\n\t/** True when a key was used, so the UI can explain a rate limit accurately. */\n\tauthenticated: boolean;\n}\n\nexport interface WebSearchToolOptions {\n\t/** Overridable so tests never reach the network. */\n\tfetchImpl?: typeof fetch;\n\tapiKey?: string | undefined;\n}\n\n/** Exa reads the key from a query parameter on the hosted endpoint. */\nfunction endpointFor(apiKey: string | undefined): string {\n\treturn apiKey ? `${EXA_MCP_URL}?exaApiKey=${encodeURIComponent(apiKey)}` : EXA_MCP_URL;\n}\n\n/**\n * The endpoint answers `application/json` or an SSE stream depending on how it\n * feels; both carry the same JSON-RPC envelope, so pull the payload out of\n * whichever arrived.\n */\nexport function parseMcpPayload(body: string): unknown {\n\tconst trimmed = body.trim();\n\tif (!trimmed) throw new Error(\"Search returned an empty response\");\n\n\tif (trimmed.startsWith(\"{\")) return JSON.parse(trimmed);\n\n\t// SSE: one or more `data:` lines, last one wins.\n\tconst dataLines = trimmed\n\t\t.split(\"\\n\")\n\t\t.filter((line) => line.startsWith(\"data:\"))\n\t\t.map((line) => line.slice(5).trim());\n\tconst last = dataLines.at(-1);\n\tif (!last) throw new Error(\"Search returned an unrecognized response\");\n\treturn JSON.parse(last);\n}\n\n/** Flatten the MCP tool result into the text the model reads. */\nexport function extractResultText(payload: unknown): string {\n\tconst envelope = payload as {\n\t\terror?: { message?: string };\n\t\tresult?: { content?: Array<{ type?: string; text?: string }>; isError?: boolean };\n\t};\n\n\tif (envelope?.error?.message) throw new Error(envelope.error.message);\n\n\tconst content = envelope?.result?.content;\n\tif (!Array.isArray(content)) throw new Error(\"Search returned no results\");\n\n\tconst text = content\n\t\t.filter((part) => part?.type === \"text\" && typeof part.text === \"string\")\n\t\t.map((part) => part.text)\n\t\t.join(\"\\n\")\n\t\t.trim();\n\n\tif (!text) throw new Error(\"Search returned no results\");\n\tif (envelope.result?.isError) throw new Error(text);\n\treturn text;\n}\n\nexport function createWebSearchToolDefinition(\n\toptions?: WebSearchToolOptions,\n): ToolDefinition<typeof websearchSchema, WebSearchToolDetails | undefined> {\n\treturn {\n\t\tname: \"websearch\",\n\t\tlabel: \"websearch\",\n\t\tdescription:\n\t\t\t\"Search the web and return ranked results with excerpts. Use for current information, documentation, error messages, and anything outside the repository.\",\n\t\tpromptSnippet: \"Search the web for current information\",\n\t\tpromptGuidelines: [\"Use websearch for anything outside the repository; do not guess at current facts.\"],\n\t\tparameters: websearchSchema,\n\t\tasync execute(_toolCallId, { query, numResults }, signal?: AbortSignal) {\n\t\t\tconst doFetch = options?.fetchImpl ?? fetch;\n\t\t\tconst apiKey = options?.apiKey ?? process.env.EXA_API_KEY;\n\n\t\t\t// Bound the request ourselves and honour an outer abort, so a hung\n\t\t\t// endpoint cannot wedge the turn.\n\t\t\tconst controller = new AbortController();\n\t\t\tconst timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);\n\t\t\tconst onAbort = () => controller.abort();\n\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\ttry {\n\t\t\t\tlet response: Response;\n\t\t\t\ttry {\n\t\t\t\t\tresponse = await doFetch(endpointFor(apiKey), {\n\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\theaders: { \"content-type\": \"application/json\", accept: \"application/json, text/event-stream\" },\n\t\t\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\t\t\tjsonrpc: \"2.0\",\n\t\t\t\t\t\t\tid: 1,\n\t\t\t\t\t\t\tmethod: \"tools/call\",\n\t\t\t\t\t\t\tparams: {\n\t\t\t\t\t\t\t\tname: \"web_search_exa\",\n\t\t\t\t\t\t\t\targuments: { query, numResults: numResults ?? DEFAULT_RESULTS, type: \"auto\" },\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}),\n\t\t\t\t\t\tsignal: controller.signal,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\t\t\tif (controller.signal.aborted) {\n\t\t\t\t\t\tthrow new Error(`Search timed out after ${REQUEST_TIMEOUT_MS / 1000}s`);\n\t\t\t\t\t}\n\t\t\t\t\tthrow new Error(`Search request failed: ${error instanceof Error ? error.message : String(error)}`);\n\t\t\t\t}\n\n\t\t\t\t// A rate limit is the one failure worth naming: it is expected on the\n\t\t\t\t// free plan, and the fix is specific rather than \"try again\".\n\t\t\t\tif (response.status === 429) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\tapiKey\n\t\t\t\t\t\t\t? \"Search rate limit reached for this Exa API key.\"\n\t\t\t\t\t\t\t: \"Search rate limit reached on Exa's free plan. Set EXA_API_KEY to raise it.\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tthrow new Error(`Search failed with HTTP ${response.status}`);\n\t\t\t\t}\n\n\t\t\t\tlet body: string;\n\t\t\t\ttry {\n\t\t\t\t\tbody = await response.text();\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\t\t\tif (controller.signal.aborted) {\n\t\t\t\t\t\tthrow new Error(`Search timed out after ${REQUEST_TIMEOUT_MS / 1000}s`);\n\t\t\t\t\t}\n\t\t\t\t\tthrow new Error(`Search response failed: ${error instanceof Error ? error.message : String(error)}`);\n\t\t\t\t}\n\n\t\t\t\tconst text = extractResultText(parseMcpPayload(body));\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\t\tdetails: { query, authenticated: apiKey !== undefined },\n\t\t\t\t};\n\t\t\t} finally {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t}\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\tconst query = str(args?.query);\n\t\t\ttext.setText(query ? theme.fg(\"muted\", query) : \"\");\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, _options: ToolRenderResultOptions, theme: Theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\t// Results are text only; images never appear in a search payload.\n\t\t\ttext.setText(theme.fg(\"muted\", getTextOutput(result as never, false)));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport const createWebSearchTool = (options?: WebSearchToolOptions) =>\n\twrapToolDefinition(createWebSearchToolDefinition(options));\n"]}
1
+ {"version":3,"file":"websearch.js","sourceRoot":"","sources":["../../../src/core/tools/websearch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAC9C,OAAO,EAAe,IAAI,EAAE,MAAM,SAAS,CAAC;AAG5C,OAAO,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAElE;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,GAAG,wBAAwB,CAAC;AAC7C,MAAM,eAAe,GAAG,CAAC,CAAC;AAC1B,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAElC,MAAM,eAAe,GAAG,IAAI,CAAC,MAAM,CAAC;IACnC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,4BAA4B,EAAE,CAAC;IACjE,UAAU,EAAE,IAAI,CAAC,QAAQ,CACxB,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,iDAAiD,eAAe,GAAG,EAAE,CAAC,CACjG;CACD,CAAC,CAAC;AAgBH,uEAAuE;AACvE,SAAS,WAAW,CAAC,MAA0B,EAAU;IACxD,OAAO,MAAM,CAAC,CAAC,CAAC,GAAG,WAAW,cAAc,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC;AAAA,CACvF;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAW;IACtD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC5B,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;IAEnE,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAExD,iDAAiD;IACjD,MAAM,SAAS,GAAG,OAAO;SACvB,KAAK,CAAC,IAAI,CAAC;SACX,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;SAC1C,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACtC,MAAM,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9B,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;IACvE,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AAAA,CACxB;AAED,iEAAiE;AACjE,MAAM,UAAU,iBAAiB,CAAC,OAAgB,EAAU;IAC3D,MAAM,QAAQ,GAAG,OAGhB,CAAC;IAEF,IAAI,QAAQ,EAAE,KAAK,EAAE,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAEtE,MAAM,OAAO,GAAG,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC;IAC1C,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;IAE3E,MAAM,IAAI,GAAG,OAAO;SAClB,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC;SACxE,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC;SACxB,IAAI,CAAC,IAAI,CAAC;SACV,IAAI,EAAE,CAAC;IAET,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;IACzD,IAAI,QAAQ,CAAC,MAAM,EAAE,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC;IACpD,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,MAAM,UAAU,6BAA6B,CAC5C,OAA8B,EAC6C;IAC3E,OAAO;QACN,IAAI,EAAE,WAAW;QACjB,KAAK,EAAE,WAAW;QAClB,WAAW,EACV,0JAA0J;QAC3J,aAAa,EAAE,wCAAwC;QACvD,gBAAgB,EAAE,CAAC,mFAAmF,CAAC;QACvG,UAAU,EAAE,eAAe;QAC3B,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,MAAoB,EAAE;YACvE,MAAM,OAAO,GAAG,OAAO,EAAE,SAAS,IAAI,KAAK,CAAC;YAC5C,MAAM,MAAM,GAAG,OAAO,EAAE,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC;YAE1D,mEAAmE;YACnE,kCAAkC;YAClC,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;YACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,kBAAkB,CAAC,CAAC;YACvE,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;YACzC,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YAE3D,IAAI,QAAkB,CAAC;YACvB,IAAI,CAAC;gBACJ,QAAQ,GAAG,MAAM,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE;oBAC7C,MAAM,EAAE,MAAM;oBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,EAAE,qCAAqC,EAAE;oBAC9F,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;wBACpB,OAAO,EAAE,KAAK;wBACd,EAAE,EAAE,CAAC;wBACL,MAAM,EAAE,YAAY;wBACpB,MAAM,EAAE;4BACP,IAAI,EAAE,gBAAgB;4BACtB,SAAS,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,IAAI,eAAe,EAAE,IAAI,EAAE,MAAM,EAAE;yBAC7E;qBACD,CAAC;oBACF,MAAM,EAAE,UAAU,CAAC,MAAM;iBACzB,CAAC,CAAC;YACJ,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,IAAI,MAAM,EAAE,OAAO;oBAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;gBAC1D,IAAI,UAAU,CAAC,MAAM,CAAC,OAAO;oBAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,kBAAkB,GAAG,IAAI,GAAG,CAAC,CAAC;gBACvG,MAAM,IAAI,KAAK,CAAC,0BAA0B,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACrG,CAAC;oBAAS,CAAC;gBACV,YAAY,CAAC,KAAK,CAAC,CAAC;gBACpB,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC/C,CAAC;YAED,sEAAsE;YACtE,8DAA8D;YAC9D,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;gBAC7B,MAAM,IAAI,KAAK,CACd,MAAM;oBACL,CAAC,CAAC,iDAAiD;oBACnD,CAAC,CAAC,4EAA4E,CAC/E,CAAC;YACH,CAAC;YACD,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBAClB,MAAM,IAAI,KAAK,CAAC,2BAA2B,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;YAC/D,CAAC;YAED,MAAM,IAAI,GAAG,iBAAiB,CAAC,eAAe,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;YACvE,OAAO;gBACN,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;gBACjC,OAAO,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,KAAK,SAAS,EAAE;aACvD,CAAC;QAAA,CACF;QACD,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE;YAChC,MAAM,IAAI,GAAI,OAAO,CAAC,aAAkC,IAAI,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YAC/E,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YAC/B,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACpD,OAAO,IAAI,CAAC;QAAA,CACZ;QACD,YAAY,CAAC,MAAM,EAAE,QAAiC,EAAE,KAAY,EAAE,OAAO,EAAE;YAC9E,MAAM,IAAI,GAAI,OAAO,CAAC,aAAkC,IAAI,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YAC/E,kEAAkE;YAClE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,CAAC,MAAe,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;YACvE,OAAO,IAAI,CAAC;QAAA,CACZ;KACD,CAAC;AAAA,CACF;AAED,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,OAA8B,EAAE,EAAE,CACrE,kBAAkB,CAAC,6BAA6B,CAAC,OAAO,CAAC,CAAC,CAAC","sourcesContent":["import { Text } from \"@earendil-works/pi-tui\";\nimport { type Static, Type } from \"typebox\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.ts\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.ts\";\nimport { getTextOutput, str } from \"./render-utils.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\n\n/**\n * Web search through Exa's hosted MCP endpoint.\n *\n * The endpoint answers unauthenticated requests on a free plan, so search works\n * with no configuration at all. EXA_API_KEY lifts the rate limit and is what a\n * deployment should use — Exa's own guidance is to authenticate for sustained\n * use, and the anonymous pool is shared with every other unauthenticated caller\n * on the internet.\n *\n * Only the transport is spoken here rather than pulling in an MCP client: this\n * is one JSON-RPC method against one known endpoint, and the response is a\n * text block either way.\n */\n\nconst EXA_MCP_URL = \"https://mcp.exa.ai/mcp\";\nconst DEFAULT_RESULTS = 8;\nconst REQUEST_TIMEOUT_MS = 25_000;\n\nconst websearchSchema = Type.Object({\n\tquery: Type.String({ description: \"What to search the web for\" }),\n\tnumResults: Type.Optional(\n\t\tType.Number({ description: `Maximum number of results to return (default: ${DEFAULT_RESULTS})` }),\n\t),\n});\n\nexport type WebSearchToolInput = Static<typeof websearchSchema>;\n\nexport interface WebSearchToolDetails {\n\tquery: string;\n\t/** True when a key was used, so the UI can explain a rate limit accurately. */\n\tauthenticated: boolean;\n}\n\nexport interface WebSearchToolOptions {\n\t/** Overridable so tests never reach the network. */\n\tfetchImpl?: typeof fetch;\n\tapiKey?: string | undefined;\n}\n\n/** Exa reads the key from a query parameter on the hosted endpoint. */\nfunction endpointFor(apiKey: string | undefined): string {\n\treturn apiKey ? `${EXA_MCP_URL}?exaApiKey=${encodeURIComponent(apiKey)}` : EXA_MCP_URL;\n}\n\n/**\n * The endpoint answers `application/json` or an SSE stream depending on how it\n * feels; both carry the same JSON-RPC envelope, so pull the payload out of\n * whichever arrived.\n */\nexport function parseMcpPayload(body: string): unknown {\n\tconst trimmed = body.trim();\n\tif (!trimmed) throw new Error(\"Search returned an empty response\");\n\n\tif (trimmed.startsWith(\"{\")) return JSON.parse(trimmed);\n\n\t// SSE: one or more `data:` lines, last one wins.\n\tconst dataLines = trimmed\n\t\t.split(\"\\n\")\n\t\t.filter((line) => line.startsWith(\"data:\"))\n\t\t.map((line) => line.slice(5).trim());\n\tconst last = dataLines.at(-1);\n\tif (!last) throw new Error(\"Search returned an unrecognized response\");\n\treturn JSON.parse(last);\n}\n\n/** Flatten the MCP tool result into the text the model reads. */\nexport function extractResultText(payload: unknown): string {\n\tconst envelope = payload as {\n\t\terror?: { message?: string };\n\t\tresult?: { content?: Array<{ type?: string; text?: string }>; isError?: boolean };\n\t};\n\n\tif (envelope?.error?.message) throw new Error(envelope.error.message);\n\n\tconst content = envelope?.result?.content;\n\tif (!Array.isArray(content)) throw new Error(\"Search returned no results\");\n\n\tconst text = content\n\t\t.filter((part) => part?.type === \"text\" && typeof part.text === \"string\")\n\t\t.map((part) => part.text)\n\t\t.join(\"\\n\")\n\t\t.trim();\n\n\tif (!text) throw new Error(\"Search returned no results\");\n\tif (envelope.result?.isError) throw new Error(text);\n\treturn text;\n}\n\nexport function createWebSearchToolDefinition(\n\toptions?: WebSearchToolOptions,\n): ToolDefinition<typeof websearchSchema, WebSearchToolDetails | undefined> {\n\treturn {\n\t\tname: \"websearch\",\n\t\tlabel: \"websearch\",\n\t\tdescription:\n\t\t\t\"Search the web and return ranked results with excerpts. Use for current information, documentation, error messages, and anything outside the repository.\",\n\t\tpromptSnippet: \"Search the web for current information\",\n\t\tpromptGuidelines: [\"Use websearch for anything outside the repository; do not guess at current facts.\"],\n\t\tparameters: websearchSchema,\n\t\tasync execute(_toolCallId, { query, numResults }, signal?: AbortSignal) {\n\t\t\tconst doFetch = options?.fetchImpl ?? fetch;\n\t\t\tconst apiKey = options?.apiKey ?? process.env.EXA_API_KEY;\n\n\t\t\t// Bound the request ourselves and honour an outer abort, so a hung\n\t\t\t// endpoint cannot wedge the turn.\n\t\t\tconst controller = new AbortController();\n\t\t\tconst timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);\n\t\t\tconst onAbort = () => controller.abort();\n\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\tlet response: Response;\n\t\t\ttry {\n\t\t\t\tresponse = await doFetch(endpointFor(apiKey), {\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\theaders: { \"content-type\": \"application/json\", accept: \"application/json, text/event-stream\" },\n\t\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\t\tjsonrpc: \"2.0\",\n\t\t\t\t\t\tid: 1,\n\t\t\t\t\t\tmethod: \"tools/call\",\n\t\t\t\t\t\tparams: {\n\t\t\t\t\t\t\tname: \"web_search_exa\",\n\t\t\t\t\t\t\targuments: { query, numResults: numResults ?? DEFAULT_RESULTS, type: \"auto\" },\n\t\t\t\t\t\t},\n\t\t\t\t\t}),\n\t\t\t\t\tsignal: controller.signal,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\t\tif (controller.signal.aborted) throw new Error(`Search timed out after ${REQUEST_TIMEOUT_MS / 1000}s`);\n\t\t\t\tthrow new Error(`Search request failed: ${error instanceof Error ? error.message : String(error)}`);\n\t\t\t} finally {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t}\n\n\t\t\t// A rate limit is the one failure worth naming: it is expected on the\n\t\t\t// free plan, and the fix is specific rather than \"try again\".\n\t\t\tif (response.status === 429) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\tapiKey\n\t\t\t\t\t\t? \"Search rate limit reached for this Exa API key.\"\n\t\t\t\t\t\t: \"Search rate limit reached on Exa's free plan. Set EXA_API_KEY to raise it.\",\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new Error(`Search failed with HTTP ${response.status}`);\n\t\t\t}\n\n\t\t\tconst text = extractResultText(parseMcpPayload(await response.text()));\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\tdetails: { query, authenticated: apiKey !== undefined },\n\t\t\t};\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\tconst query = str(args?.query);\n\t\t\ttext.setText(query ? theme.fg(\"muted\", query) : \"\");\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, _options: ToolRenderResultOptions, theme: Theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\t// Results are text only; images never appear in a search payload.\n\t\t\ttext.setText(theme.fg(\"muted\", getTextOutput(result as never, false)));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport const createWebSearchTool = (options?: WebSearchToolOptions) =>\n\twrapToolDefinition(createWebSearchToolDefinition(options));\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"device-auth.d.ts","sourceRoot":"","sources":["../../../src/extensions/gitnexus/device-auth.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,eAAe;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,yCAAyC;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,iBAAiB;AAC5B,iDAAiD;AAC/C,eAAe;AACjB,+DAA+D;GAC7D,eAAe;AACjB,+BAA+B;GAC7B,SAAS,CAAC;AAEb,qBAAa,eAAgB,SAAQ,KAAK;IACzC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,GAAG,SAAS,GAAG,UAAU,CAAC;IAE5D,YAAY,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,CAAC,QAAQ,CAAC,EAI7D;CACD;AAED,gFAAgF;AAChF,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,IAAI,MAAM,CAQ1C;AAED,2EAA2E;AAC3E,wBAAgB,iBAAiB,IAAI,MAAM,CAS1C;AAkCD,2CAA2C;AAC3C,wBAAsB,eAAe,CACpC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAO,GACrF,OAAO,CAAC,eAAe,CAAC,CA4B1B;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CACnC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,eAAe,EACtB,OAAO,GAAE;IACR,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACd,GACJ,OAAO,CAAC,gBAAgB,CAAC,CAgD3B","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { arch, hostname, platform, userInfo } from \"node:os\";\nimport { APP_NAME, VERSION } from \"../../config.ts\";\n\n/**\n * Device authorization client for hub login.\n *\n * The CLI cannot receive a browser redirect, so it starts a request, shows the\n * user a short code, and polls until they approve it in the web app. The hub\n * mints the token on the first successful poll, so it exists in plaintext only\n * in that one response and in the local auth file.\n *\n * Protocol shape follows RFC 8628, and the hub returns its error codes\n * verbatim so callers branch on a stable string rather than prose.\n */\n\nexport interface DeviceAuthStart {\n\tdeviceCode: string;\n\tuserCode: string;\n\tverificationUrl: string;\n\tverificationUrlComplete?: string;\n\t/** Seconds until the request expires. */\n\texpiresIn: number;\n\t/** Seconds the CLI must wait between polls. */\n\tinterval: number;\n}\n\nexport interface DeviceAuthResult {\n\ttoken: string;\n\thubUrl: string;\n\ttokenId?: string;\n}\n\nexport type DeviceAuthFailure =\n\t/** The user rejected the code in the browser. */\n\t| \"access_denied\"\n\t/** The request expired, was already used, or never existed. */\n\t| \"expired_token\"\n\t/** The CLI gave up waiting. */\n\t| \"timeout\";\n\nexport class DeviceAuthError extends Error {\n\treadonly reason: DeviceAuthFailure | \"network\" | \"protocol\";\n\n\tconstructor(message: string, reason: DeviceAuthError[\"reason\"]) {\n\t\tsuper(message);\n\t\tthis.name = \"DeviceAuthError\";\n\t\tthis.reason = reason;\n\t}\n}\n\n/** Base URL of the hub's HTTP API, derived from the configured MCP endpoint. */\nexport function hubApiBase(hubUrl: string): string {\n\treturn hubUrl.replace(/\\/mcp\\/?$/, \"\").replace(/\\/+$/, \"\");\n}\n\n/** Human-readable device label, so tokens are identifiable in the hub UI. */\nexport function defaultDeviceName(): string {\n\ttry {\n\t\tconst host = hostname();\n\t\tconst user = userInfo().username;\n\t\treturn user ? `${user}@${host}` : host;\n\t} catch {\n\t\treturn \"unknown-device\";\n\t}\n}\n\n/** Stable machine identity required by Hub device-token authentication. */\nexport function deviceFingerprint(): string {\n\tlet username = \"unknown-user\";\n\ttry {\n\t\tusername = userInfo().username || username;\n\t} catch {\n\t\t// Keep deterministic fallback.\n\t}\n\tconst identity = `${hostname()}:${username}:${platform()}:${arch()}`;\n\treturn createHash(\"sha256\").update(identity).digest(\"hex\").slice(0, 16);\n}\n\nasync function postJson(\n\turl: string,\n\tbody: unknown,\n\tsignal: AbortSignal | undefined,\n): Promise<{ status: number; data: any }> {\n\tlet response: Response;\n\ttry {\n\t\tresponse = await fetch(url, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: { \"content-type\": \"application/json\" },\n\t\t\tbody: JSON.stringify(body),\n\t\t\tsignal,\n\t\t});\n\t} catch (error) {\n\t\tif (signal?.aborted) throw new DeviceAuthError(\"Login cancelled\", \"timeout\");\n\t\tthrow new DeviceAuthError(`Cannot reach the hub at ${url}: ${errorText(error)}`, \"network\");\n\t}\n\n\tconst text = await response.text();\n\tlet data: any;\n\ttry {\n\t\tdata = text ? JSON.parse(text) : {};\n\t} catch {\n\t\tthrow new DeviceAuthError(`Hub returned a non-JSON response (HTTP ${response.status})`, \"protocol\");\n\t}\n\treturn { status: response.status, data };\n}\n\nfunction errorText(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\n/** Ask the hub to begin a device login. */\nexport async function startDeviceAuth(\n\thubUrl: string,\n\toptions: { deviceName?: string; deviceFingerprint?: string; signal?: AbortSignal } = {},\n): Promise<DeviceAuthStart> {\n\tconst { status, data } = await postJson(\n\t\t`${hubApiBase(hubUrl)}/auth/device/start`,\n\t\t{\n\t\t\tagentName: APP_NAME,\n\t\t\tagentVersion: VERSION,\n\t\t\tdeviceName: options.deviceName ?? defaultDeviceName(),\n\t\t\tdeviceFingerprint: options.deviceFingerprint ?? deviceFingerprint(),\n\t\t},\n\t\toptions.signal,\n\t);\n\n\tif (status !== 200 || typeof data?.deviceCode !== \"string\" || typeof data?.userCode !== \"string\") {\n\t\tthrow new DeviceAuthError(\n\t\t\tdata?.error ? String(data.error) : `Hub rejected the login request (HTTP ${status})`,\n\t\t\t\"protocol\",\n\t\t);\n\t}\n\n\treturn {\n\t\tdeviceCode: data.deviceCode,\n\t\tuserCode: data.userCode,\n\t\tverificationUrl: String(data.verificationUrl ?? `${hubApiBase(hubUrl)}/device`),\n\t\tverificationUrlComplete:\n\t\t\ttypeof data.verificationUrlComplete === \"string\" ? data.verificationUrlComplete : undefined,\n\t\texpiresIn: typeof data.expiresIn === \"number\" ? data.expiresIn : 600,\n\t\tinterval: typeof data.interval === \"number\" && data.interval > 0 ? data.interval : 5,\n\t};\n}\n\n/**\n * Poll until the user approves, the request dies, or the deadline passes.\n *\n * `sleep` is injectable so tests do not wait in real time.\n */\nexport async function pollDeviceAuth(\n\thubUrl: string,\n\tstart: DeviceAuthStart,\n\toptions: {\n\t\tsignal?: AbortSignal;\n\t\tsleep?: (ms: number) => Promise<void>;\n\t\tnow?: () => number;\n\t} = {},\n): Promise<DeviceAuthResult> {\n\tconst sleep = options.sleep ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));\n\tconst now = options.now ?? (() => Date.now());\n\tconst deadline = now() + start.expiresIn * 1000;\n\tlet intervalMs = start.interval * 1000;\n\n\twhile (true) {\n\t\tif (options.signal?.aborted) throw new DeviceAuthError(\"Login cancelled\", \"timeout\");\n\t\tawait sleep(intervalMs);\n\t\t// Re-check after sleeping: a request that expired while we waited should\n\t\t// not cost the hub another round trip.\n\t\tif (now() >= deadline) break;\n\n\t\tconst { status, data } = await postJson(\n\t\t\t`${hubApiBase(hubUrl)}/auth/device/poll`,\n\t\t\t{ deviceCode: start.deviceCode },\n\t\t\toptions.signal,\n\t\t);\n\n\t\tif (status === 200 && typeof data?.token === \"string\") {\n\t\t\treturn {\n\t\t\t\ttoken: data.token,\n\t\t\t\thubUrl: typeof data.hubUrl === \"string\" ? data.hubUrl : hubUrl,\n\t\t\t\ttokenId: typeof data.tokenId === \"string\" ? data.tokenId : undefined,\n\t\t\t};\n\t\t}\n\n\t\tswitch (data?.error) {\n\t\t\tcase \"authorization_pending\":\n\t\t\t\tbreak;\n\t\t\tcase \"slow_down\":\n\t\t\t\t// Back off to whatever the hub now advertises, or a second more.\n\t\t\t\tintervalMs =\n\t\t\t\t\ttypeof data.interval === \"number\" && data.interval > 0 ? data.interval * 1000 : intervalMs + 1000;\n\t\t\t\tbreak;\n\t\t\tcase \"access_denied\":\n\t\t\t\tthrow new DeviceAuthError(\"Login was denied in the browser\", \"access_denied\");\n\t\t\tcase \"expired_token\":\n\t\t\t\tthrow new DeviceAuthError(\"The login code expired or was already used\", \"expired_token\");\n\t\t\tdefault:\n\t\t\t\tthrow new DeviceAuthError(\n\t\t\t\t\tdata?.error ? String(data.error) : `Unexpected hub response (HTTP ${status})`,\n\t\t\t\t\t\"protocol\",\n\t\t\t\t);\n\t\t}\n\t}\n\n\tthrow new DeviceAuthError(\"Timed out waiting for approval\", \"timeout\");\n}\n"]}
1
+ {"version":3,"file":"device-auth.d.ts","sourceRoot":"","sources":["../../../src/extensions/gitnexus/device-auth.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,eAAe;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,yCAAyC;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,iBAAiB;AAC5B,iDAAiD;AAC/C,eAAe;AACjB,+DAA+D;GAC7D,eAAe;AACjB,+BAA+B;GAC7B,SAAS,CAAC;AAEb,qBAAa,eAAgB,SAAQ,KAAK;IACzC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,GAAG,SAAS,GAAG,UAAU,CAAC;IAE5D,YAAY,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,CAAC,QAAQ,CAAC,EAI7D;CACD;AAED,gFAAgF;AAChF,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,6EAA6E;AAC7E,wBAAgB,iBAAiB,IAAI,MAAM,CAQ1C;AAED,2EAA2E;AAC3E,wBAAgB,iBAAiB,IAAI,MAAM,CAS1C;AAkCD,2CAA2C;AAC3C,wBAAsB,eAAe,CACpC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAO,GACrF,OAAO,CAAC,eAAe,CAAC,CA4B1B;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CACnC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,eAAe,EACtB,OAAO,GAAE;IACR,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACd,GACJ,OAAO,CAAC,gBAAgB,CAAC,CA+C3B","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { arch, hostname, platform, userInfo } from \"node:os\";\nimport { APP_NAME, VERSION } from \"../../config.ts\";\n\n/**\n * Device authorization client for hub login.\n *\n * The CLI cannot receive a browser redirect, so it starts a request, shows the\n * user a short code, and polls until they approve it in the web app. The hub\n * mints the token on the first successful poll, so it exists in plaintext only\n * in that one response and in the local auth file.\n *\n * Protocol shape follows RFC 8628, and the hub returns its error codes\n * verbatim so callers branch on a stable string rather than prose.\n */\n\nexport interface DeviceAuthStart {\n\tdeviceCode: string;\n\tuserCode: string;\n\tverificationUrl: string;\n\tverificationUrlComplete?: string;\n\t/** Seconds until the request expires. */\n\texpiresIn: number;\n\t/** Seconds the CLI must wait between polls. */\n\tinterval: number;\n}\n\nexport interface DeviceAuthResult {\n\ttoken: string;\n\thubUrl: string;\n\ttokenId?: string;\n}\n\nexport type DeviceAuthFailure =\n\t/** The user rejected the code in the browser. */\n\t| \"access_denied\"\n\t/** The request expired, was already used, or never existed. */\n\t| \"expired_token\"\n\t/** The CLI gave up waiting. */\n\t| \"timeout\";\n\nexport class DeviceAuthError extends Error {\n\treadonly reason: DeviceAuthFailure | \"network\" | \"protocol\";\n\n\tconstructor(message: string, reason: DeviceAuthError[\"reason\"]) {\n\t\tsuper(message);\n\t\tthis.name = \"DeviceAuthError\";\n\t\tthis.reason = reason;\n\t}\n}\n\n/** Base URL of the hub's HTTP API, derived from the configured MCP endpoint. */\nexport function hubApiBase(hubUrl: string): string {\n\treturn hubUrl.replace(/\\/mcp\\/?$/, \"\").replace(/\\/+$/, \"\");\n}\n\n/** Human-readable device label, so tokens are identifiable in the hub UI. */\nexport function defaultDeviceName(): string {\n\ttry {\n\t\tconst host = hostname();\n\t\tconst user = userInfo().username;\n\t\treturn user ? `${user}@${host}` : host;\n\t} catch {\n\t\treturn \"unknown-device\";\n\t}\n}\n\n/** Stable machine identity required by Hub device-token authentication. */\nexport function deviceFingerprint(): string {\n\tlet username = \"unknown-user\";\n\ttry {\n\t\tusername = userInfo().username || username;\n\t} catch {\n\t\t// Keep deterministic fallback.\n\t}\n\tconst identity = `${hostname()}:${username}:${platform()}:${arch()}`;\n\treturn createHash(\"sha256\").update(identity).digest(\"hex\").slice(0, 16);\n}\n\nasync function postJson(\n\turl: string,\n\tbody: unknown,\n\tsignal: AbortSignal | undefined,\n): Promise<{ status: number; data: any }> {\n\tlet response: Response;\n\ttry {\n\t\tresponse = await fetch(url, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: { \"content-type\": \"application/json\" },\n\t\t\tbody: JSON.stringify(body),\n\t\t\tsignal,\n\t\t});\n\t} catch (error) {\n\t\tif (signal?.aborted) throw new DeviceAuthError(\"Login cancelled\", \"timeout\");\n\t\tthrow new DeviceAuthError(`Cannot reach the hub at ${url}: ${errorText(error)}`, \"network\");\n\t}\n\n\tconst text = await response.text();\n\tlet data: any;\n\ttry {\n\t\tdata = text ? JSON.parse(text) : {};\n\t} catch {\n\t\tthrow new DeviceAuthError(`Hub returned a non-JSON response (HTTP ${response.status})`, \"protocol\");\n\t}\n\treturn { status: response.status, data };\n}\n\nfunction errorText(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\n/** Ask the hub to begin a device login. */\nexport async function startDeviceAuth(\n\thubUrl: string,\n\toptions: { deviceName?: string; deviceFingerprint?: string; signal?: AbortSignal } = {},\n): Promise<DeviceAuthStart> {\n\tconst { status, data } = await postJson(\n\t\t`${hubApiBase(hubUrl)}/auth/device/start`,\n\t\t{\n\t\t\tagentName: APP_NAME,\n\t\t\tagentVersion: VERSION,\n\t\t\tdeviceName: options.deviceName ?? defaultDeviceName(),\n\t\t\tdeviceFingerprint: options.deviceFingerprint ?? deviceFingerprint(),\n\t\t},\n\t\toptions.signal,\n\t);\n\n\tif (status !== 200 || typeof data?.deviceCode !== \"string\" || typeof data?.userCode !== \"string\") {\n\t\tthrow new DeviceAuthError(\n\t\t\tdata?.error ? String(data.error) : `Hub rejected the login request (HTTP ${status})`,\n\t\t\t\"protocol\",\n\t\t);\n\t}\n\n\treturn {\n\t\tdeviceCode: data.deviceCode,\n\t\tuserCode: data.userCode,\n\t\tverificationUrl: String(data.verificationUrl ?? `${hubApiBase(hubUrl)}/device`),\n\t\tverificationUrlComplete:\n\t\t\ttypeof data.verificationUrlComplete === \"string\" ? data.verificationUrlComplete : undefined,\n\t\texpiresIn: typeof data.expiresIn === \"number\" ? data.expiresIn : 600,\n\t\tinterval: typeof data.interval === \"number\" && data.interval > 0 ? data.interval : 5,\n\t};\n}\n\n/**\n * Poll until the user approves, the request dies, or the deadline passes.\n *\n * `sleep` is injectable so tests do not wait in real time.\n */\nexport async function pollDeviceAuth(\n\thubUrl: string,\n\tstart: DeviceAuthStart,\n\toptions: {\n\t\tsignal?: AbortSignal;\n\t\tsleep?: (ms: number) => Promise<void>;\n\t\tnow?: () => number;\n\t} = {},\n): Promise<DeviceAuthResult> {\n\tconst sleep = options.sleep ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));\n\tconst now = options.now ?? (() => Date.now());\n\tconst deadline = now() + start.expiresIn * 1000;\n\tlet intervalMs = start.interval * 1000;\n\n\twhile (true) {\n\t\tif (options.signal?.aborted) throw new DeviceAuthError(\"Login cancelled\", \"timeout\");\n\t\tawait sleep(intervalMs);\n\t\t// Re-check after sleeping: a request that expired while we waited should\n\t\t// not cost the hub another round trip.\n\t\tif (now() >= deadline) break;\n\n\t\tconst { status, data } = await postJson(\n\t\t\t`${hubApiBase(hubUrl)}/auth/device/poll`,\n\t\t\t{ deviceCode: start.deviceCode },\n\t\t\toptions.signal,\n\t\t);\n\n\t\tif (status === 200 && typeof data?.token === \"string\") {\n\t\t\treturn {\n\t\t\t\ttoken: data.token,\n\t\t\t\thubUrl: typeof data.hubUrl === \"string\" ? data.hubUrl : hubUrl,\n\t\t\t\ttokenId: typeof data.tokenId === \"string\" ? data.tokenId : undefined,\n\t\t\t};\n\t\t}\n\n\t\tswitch (data?.error) {\n\t\t\tcase \"authorization_pending\":\n\t\t\t\tbreak;\n\t\t\tcase \"slow_down\":\n\t\t\t\t// Back off to whatever the hub now advertises, or a second more.\n\t\t\t\tintervalMs = typeof data.interval === \"number\" ? data.interval * 1000 : intervalMs + 1000;\n\t\t\t\tbreak;\n\t\t\tcase \"access_denied\":\n\t\t\t\tthrow new DeviceAuthError(\"Login was denied in the browser\", \"access_denied\");\n\t\t\tcase \"expired_token\":\n\t\t\t\tthrow new DeviceAuthError(\"The login code expired or was already used\", \"expired_token\");\n\t\t\tdefault:\n\t\t\t\tthrow new DeviceAuthError(\n\t\t\t\t\tdata?.error ? String(data.error) : `Unexpected hub response (HTTP ${status})`,\n\t\t\t\t\t\"protocol\",\n\t\t\t\t);\n\t\t}\n\t}\n\n\tthrow new DeviceAuthError(\"Timed out waiting for approval\", \"timeout\");\n}\n"]}
@@ -115,8 +115,7 @@ export async function pollDeviceAuth(hubUrl, start, options = {}) {
115
115
  break;
116
116
  case "slow_down":
117
117
  // Back off to whatever the hub now advertises, or a second more.
118
- intervalMs =
119
- typeof data.interval === "number" && data.interval > 0 ? data.interval * 1000 : intervalMs + 1000;
118
+ intervalMs = typeof data.interval === "number" ? data.interval * 1000 : intervalMs + 1000;
120
119
  break;
121
120
  case "access_denied":
122
121
  throw new DeviceAuthError("Login was denied in the browser", "access_denied");
@@ -1 +1 @@
1
- {"version":3,"file":"device-auth.js","sourceRoot":"","sources":["../../../src/extensions/gitnexus/device-auth.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { arch, hostname, platform, userInfo } from \"node:os\";\nimport { APP_NAME, VERSION } from \"../../config.ts\";\n\n/**\n * Device authorization client for hub login.\n *\n * The CLI cannot receive a browser redirect, so it starts a request, shows the\n * user a short code, and polls until they approve it in the web app. The hub\n * mints the token on the first successful poll, so it exists in plaintext only\n * in that one response and in the local auth file.\n *\n * Protocol shape follows RFC 8628, and the hub returns its error codes\n * verbatim so callers branch on a stable string rather than prose.\n */\n\nexport interface DeviceAuthStart {\n\tdeviceCode: string;\n\tuserCode: string;\n\tverificationUrl: string;\n\tverificationUrlComplete?: string;\n\t/** Seconds until the request expires. */\n\texpiresIn: number;\n\t/** Seconds the CLI must wait between polls. */\n\tinterval: number;\n}\n\nexport interface DeviceAuthResult {\n\ttoken: string;\n\thubUrl: string;\n\ttokenId?: string;\n}\n\nexport type DeviceAuthFailure =\n\t/** The user rejected the code in the browser. */\n\t| \"access_denied\"\n\t/** The request expired, was already used, or never existed. */\n\t| \"expired_token\"\n\t/** The CLI gave up waiting. */\n\t| \"timeout\";\n\nexport class DeviceAuthError extends Error {\n\treadonly reason: DeviceAuthFailure | \"network\" | \"protocol\";\n\n\tconstructor(message: string, reason: DeviceAuthError[\"reason\"]) {\n\t\tsuper(message);\n\t\tthis.name = \"DeviceAuthError\";\n\t\tthis.reason = reason;\n\t}\n}\n\n/** Base URL of the hub's HTTP API, derived from the configured MCP endpoint. */\nexport function hubApiBase(hubUrl: string): string {\n\treturn hubUrl.replace(/\\/mcp\\/?$/, \"\").replace(/\\/+$/, \"\");\n}\n\n/** Human-readable device label, so tokens are identifiable in the hub UI. */\nexport function defaultDeviceName(): string {\n\ttry {\n\t\tconst host = hostname();\n\t\tconst user = userInfo().username;\n\t\treturn user ? `${user}@${host}` : host;\n\t} catch {\n\t\treturn \"unknown-device\";\n\t}\n}\n\n/** Stable machine identity required by Hub device-token authentication. */\nexport function deviceFingerprint(): string {\n\tlet username = \"unknown-user\";\n\ttry {\n\t\tusername = userInfo().username || username;\n\t} catch {\n\t\t// Keep deterministic fallback.\n\t}\n\tconst identity = `${hostname()}:${username}:${platform()}:${arch()}`;\n\treturn createHash(\"sha256\").update(identity).digest(\"hex\").slice(0, 16);\n}\n\nasync function postJson(\n\turl: string,\n\tbody: unknown,\n\tsignal: AbortSignal | undefined,\n): Promise<{ status: number; data: any }> {\n\tlet response: Response;\n\ttry {\n\t\tresponse = await fetch(url, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: { \"content-type\": \"application/json\" },\n\t\t\tbody: JSON.stringify(body),\n\t\t\tsignal,\n\t\t});\n\t} catch (error) {\n\t\tif (signal?.aborted) throw new DeviceAuthError(\"Login cancelled\", \"timeout\");\n\t\tthrow new DeviceAuthError(`Cannot reach the hub at ${url}: ${errorText(error)}`, \"network\");\n\t}\n\n\tconst text = await response.text();\n\tlet data: any;\n\ttry {\n\t\tdata = text ? JSON.parse(text) : {};\n\t} catch {\n\t\tthrow new DeviceAuthError(`Hub returned a non-JSON response (HTTP ${response.status})`, \"protocol\");\n\t}\n\treturn { status: response.status, data };\n}\n\nfunction errorText(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\n/** Ask the hub to begin a device login. */\nexport async function startDeviceAuth(\n\thubUrl: string,\n\toptions: { deviceName?: string; deviceFingerprint?: string; signal?: AbortSignal } = {},\n): Promise<DeviceAuthStart> {\n\tconst { status, data } = await postJson(\n\t\t`${hubApiBase(hubUrl)}/auth/device/start`,\n\t\t{\n\t\t\tagentName: APP_NAME,\n\t\t\tagentVersion: VERSION,\n\t\t\tdeviceName: options.deviceName ?? defaultDeviceName(),\n\t\t\tdeviceFingerprint: options.deviceFingerprint ?? deviceFingerprint(),\n\t\t},\n\t\toptions.signal,\n\t);\n\n\tif (status !== 200 || typeof data?.deviceCode !== \"string\" || typeof data?.userCode !== \"string\") {\n\t\tthrow new DeviceAuthError(\n\t\t\tdata?.error ? String(data.error) : `Hub rejected the login request (HTTP ${status})`,\n\t\t\t\"protocol\",\n\t\t);\n\t}\n\n\treturn {\n\t\tdeviceCode: data.deviceCode,\n\t\tuserCode: data.userCode,\n\t\tverificationUrl: String(data.verificationUrl ?? `${hubApiBase(hubUrl)}/device`),\n\t\tverificationUrlComplete:\n\t\t\ttypeof data.verificationUrlComplete === \"string\" ? data.verificationUrlComplete : undefined,\n\t\texpiresIn: typeof data.expiresIn === \"number\" ? data.expiresIn : 600,\n\t\tinterval: typeof data.interval === \"number\" && data.interval > 0 ? data.interval : 5,\n\t};\n}\n\n/**\n * Poll until the user approves, the request dies, or the deadline passes.\n *\n * `sleep` is injectable so tests do not wait in real time.\n */\nexport async function pollDeviceAuth(\n\thubUrl: string,\n\tstart: DeviceAuthStart,\n\toptions: {\n\t\tsignal?: AbortSignal;\n\t\tsleep?: (ms: number) => Promise<void>;\n\t\tnow?: () => number;\n\t} = {},\n): Promise<DeviceAuthResult> {\n\tconst sleep = options.sleep ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));\n\tconst now = options.now ?? (() => Date.now());\n\tconst deadline = now() + start.expiresIn * 1000;\n\tlet intervalMs = start.interval * 1000;\n\n\twhile (true) {\n\t\tif (options.signal?.aborted) throw new DeviceAuthError(\"Login cancelled\", \"timeout\");\n\t\tawait sleep(intervalMs);\n\t\t// Re-check after sleeping: a request that expired while we waited should\n\t\t// not cost the hub another round trip.\n\t\tif (now() >= deadline) break;\n\n\t\tconst { status, data } = await postJson(\n\t\t\t`${hubApiBase(hubUrl)}/auth/device/poll`,\n\t\t\t{ deviceCode: start.deviceCode },\n\t\t\toptions.signal,\n\t\t);\n\n\t\tif (status === 200 && typeof data?.token === \"string\") {\n\t\t\treturn {\n\t\t\t\ttoken: data.token,\n\t\t\t\thubUrl: typeof data.hubUrl === \"string\" ? data.hubUrl : hubUrl,\n\t\t\t\ttokenId: typeof data.tokenId === \"string\" ? data.tokenId : undefined,\n\t\t\t};\n\t\t}\n\n\t\tswitch (data?.error) {\n\t\t\tcase \"authorization_pending\":\n\t\t\t\tbreak;\n\t\t\tcase \"slow_down\":\n\t\t\t\t// Back off to whatever the hub now advertises, or a second more.\n\t\t\t\tintervalMs =\n\t\t\t\t\ttypeof data.interval === \"number\" && data.interval > 0 ? data.interval * 1000 : intervalMs + 1000;\n\t\t\t\tbreak;\n\t\t\tcase \"access_denied\":\n\t\t\t\tthrow new DeviceAuthError(\"Login was denied in the browser\", \"access_denied\");\n\t\t\tcase \"expired_token\":\n\t\t\t\tthrow new DeviceAuthError(\"The login code expired or was already used\", \"expired_token\");\n\t\t\tdefault:\n\t\t\t\tthrow new DeviceAuthError(\n\t\t\t\t\tdata?.error ? String(data.error) : `Unexpected hub response (HTTP ${status})`,\n\t\t\t\t\t\"protocol\",\n\t\t\t\t);\n\t\t}\n\t}\n\n\tthrow new DeviceAuthError(\"Timed out waiting for approval\", \"timeout\");\n}\n"]}
1
+ {"version":3,"file":"device-auth.js","sourceRoot":"","sources":["../../../src/extensions/gitnexus/device-auth.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { arch, hostname, platform, userInfo } from \"node:os\";\nimport { APP_NAME, VERSION } from \"../../config.ts\";\n\n/**\n * Device authorization client for hub login.\n *\n * The CLI cannot receive a browser redirect, so it starts a request, shows the\n * user a short code, and polls until they approve it in the web app. The hub\n * mints the token on the first successful poll, so it exists in plaintext only\n * in that one response and in the local auth file.\n *\n * Protocol shape follows RFC 8628, and the hub returns its error codes\n * verbatim so callers branch on a stable string rather than prose.\n */\n\nexport interface DeviceAuthStart {\n\tdeviceCode: string;\n\tuserCode: string;\n\tverificationUrl: string;\n\tverificationUrlComplete?: string;\n\t/** Seconds until the request expires. */\n\texpiresIn: number;\n\t/** Seconds the CLI must wait between polls. */\n\tinterval: number;\n}\n\nexport interface DeviceAuthResult {\n\ttoken: string;\n\thubUrl: string;\n\ttokenId?: string;\n}\n\nexport type DeviceAuthFailure =\n\t/** The user rejected the code in the browser. */\n\t| \"access_denied\"\n\t/** The request expired, was already used, or never existed. */\n\t| \"expired_token\"\n\t/** The CLI gave up waiting. */\n\t| \"timeout\";\n\nexport class DeviceAuthError extends Error {\n\treadonly reason: DeviceAuthFailure | \"network\" | \"protocol\";\n\n\tconstructor(message: string, reason: DeviceAuthError[\"reason\"]) {\n\t\tsuper(message);\n\t\tthis.name = \"DeviceAuthError\";\n\t\tthis.reason = reason;\n\t}\n}\n\n/** Base URL of the hub's HTTP API, derived from the configured MCP endpoint. */\nexport function hubApiBase(hubUrl: string): string {\n\treturn hubUrl.replace(/\\/mcp\\/?$/, \"\").replace(/\\/+$/, \"\");\n}\n\n/** Human-readable device label, so tokens are identifiable in the hub UI. */\nexport function defaultDeviceName(): string {\n\ttry {\n\t\tconst host = hostname();\n\t\tconst user = userInfo().username;\n\t\treturn user ? `${user}@${host}` : host;\n\t} catch {\n\t\treturn \"unknown-device\";\n\t}\n}\n\n/** Stable machine identity required by Hub device-token authentication. */\nexport function deviceFingerprint(): string {\n\tlet username = \"unknown-user\";\n\ttry {\n\t\tusername = userInfo().username || username;\n\t} catch {\n\t\t// Keep deterministic fallback.\n\t}\n\tconst identity = `${hostname()}:${username}:${platform()}:${arch()}`;\n\treturn createHash(\"sha256\").update(identity).digest(\"hex\").slice(0, 16);\n}\n\nasync function postJson(\n\turl: string,\n\tbody: unknown,\n\tsignal: AbortSignal | undefined,\n): Promise<{ status: number; data: any }> {\n\tlet response: Response;\n\ttry {\n\t\tresponse = await fetch(url, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: { \"content-type\": \"application/json\" },\n\t\t\tbody: JSON.stringify(body),\n\t\t\tsignal,\n\t\t});\n\t} catch (error) {\n\t\tif (signal?.aborted) throw new DeviceAuthError(\"Login cancelled\", \"timeout\");\n\t\tthrow new DeviceAuthError(`Cannot reach the hub at ${url}: ${errorText(error)}`, \"network\");\n\t}\n\n\tconst text = await response.text();\n\tlet data: any;\n\ttry {\n\t\tdata = text ? JSON.parse(text) : {};\n\t} catch {\n\t\tthrow new DeviceAuthError(`Hub returned a non-JSON response (HTTP ${response.status})`, \"protocol\");\n\t}\n\treturn { status: response.status, data };\n}\n\nfunction errorText(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\n/** Ask the hub to begin a device login. */\nexport async function startDeviceAuth(\n\thubUrl: string,\n\toptions: { deviceName?: string; deviceFingerprint?: string; signal?: AbortSignal } = {},\n): Promise<DeviceAuthStart> {\n\tconst { status, data } = await postJson(\n\t\t`${hubApiBase(hubUrl)}/auth/device/start`,\n\t\t{\n\t\t\tagentName: APP_NAME,\n\t\t\tagentVersion: VERSION,\n\t\t\tdeviceName: options.deviceName ?? defaultDeviceName(),\n\t\t\tdeviceFingerprint: options.deviceFingerprint ?? deviceFingerprint(),\n\t\t},\n\t\toptions.signal,\n\t);\n\n\tif (status !== 200 || typeof data?.deviceCode !== \"string\" || typeof data?.userCode !== \"string\") {\n\t\tthrow new DeviceAuthError(\n\t\t\tdata?.error ? String(data.error) : `Hub rejected the login request (HTTP ${status})`,\n\t\t\t\"protocol\",\n\t\t);\n\t}\n\n\treturn {\n\t\tdeviceCode: data.deviceCode,\n\t\tuserCode: data.userCode,\n\t\tverificationUrl: String(data.verificationUrl ?? `${hubApiBase(hubUrl)}/device`),\n\t\tverificationUrlComplete:\n\t\t\ttypeof data.verificationUrlComplete === \"string\" ? data.verificationUrlComplete : undefined,\n\t\texpiresIn: typeof data.expiresIn === \"number\" ? data.expiresIn : 600,\n\t\tinterval: typeof data.interval === \"number\" && data.interval > 0 ? data.interval : 5,\n\t};\n}\n\n/**\n * Poll until the user approves, the request dies, or the deadline passes.\n *\n * `sleep` is injectable so tests do not wait in real time.\n */\nexport async function pollDeviceAuth(\n\thubUrl: string,\n\tstart: DeviceAuthStart,\n\toptions: {\n\t\tsignal?: AbortSignal;\n\t\tsleep?: (ms: number) => Promise<void>;\n\t\tnow?: () => number;\n\t} = {},\n): Promise<DeviceAuthResult> {\n\tconst sleep = options.sleep ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));\n\tconst now = options.now ?? (() => Date.now());\n\tconst deadline = now() + start.expiresIn * 1000;\n\tlet intervalMs = start.interval * 1000;\n\n\twhile (true) {\n\t\tif (options.signal?.aborted) throw new DeviceAuthError(\"Login cancelled\", \"timeout\");\n\t\tawait sleep(intervalMs);\n\t\t// Re-check after sleeping: a request that expired while we waited should\n\t\t// not cost the hub another round trip.\n\t\tif (now() >= deadline) break;\n\n\t\tconst { status, data } = await postJson(\n\t\t\t`${hubApiBase(hubUrl)}/auth/device/poll`,\n\t\t\t{ deviceCode: start.deviceCode },\n\t\t\toptions.signal,\n\t\t);\n\n\t\tif (status === 200 && typeof data?.token === \"string\") {\n\t\t\treturn {\n\t\t\t\ttoken: data.token,\n\t\t\t\thubUrl: typeof data.hubUrl === \"string\" ? data.hubUrl : hubUrl,\n\t\t\t\ttokenId: typeof data.tokenId === \"string\" ? data.tokenId : undefined,\n\t\t\t};\n\t\t}\n\n\t\tswitch (data?.error) {\n\t\t\tcase \"authorization_pending\":\n\t\t\t\tbreak;\n\t\t\tcase \"slow_down\":\n\t\t\t\t// Back off to whatever the hub now advertises, or a second more.\n\t\t\t\tintervalMs = typeof data.interval === \"number\" ? data.interval * 1000 : intervalMs + 1000;\n\t\t\t\tbreak;\n\t\t\tcase \"access_denied\":\n\t\t\t\tthrow new DeviceAuthError(\"Login was denied in the browser\", \"access_denied\");\n\t\t\tcase \"expired_token\":\n\t\t\t\tthrow new DeviceAuthError(\"The login code expired or was already used\", \"expired_token\");\n\t\t\tdefault:\n\t\t\t\tthrow new DeviceAuthError(\n\t\t\t\t\tdata?.error ? String(data.error) : `Unexpected hub response (HTTP ${status})`,\n\t\t\t\t\t\"protocol\",\n\t\t\t\t);\n\t\t}\n\t}\n\n\tthrow new DeviceAuthError(\"Timed out waiting for approval\", \"timeout\");\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../../../src/extensions/gitnexus/gateway.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,KAAK,cAAc,EAAsB,MAAM,aAAa,CAAC;AAGtE;;;;;;;;;;;;;;;;;;GAkBG;AAEH,eAAO,MAAM,gBAAgB,aAAa,CAAC;AAC3C,eAAO,MAAM,aAAa,SAAS,CAAC;AAEpC,MAAM,WAAW,aAAa;IAC7B,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5D;AAaD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAIzF;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,oEAAoE;AACpE,wBAAgB,gBAAgB,CAAC,IAAI,GAAE,MAA2B,GAAG,aAAa,GAAG,SAAS,CAa7F;AAED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CAGzC,MAAM,EAAE,cAAc,GAAG,SAAS,EAClC,OAAO,GAAE;IACR,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACd,GACJ,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAuCpC;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CACnC,EAAE,EAAE,YAAY,EAChB,MAAM,EAAE,cAAc,GAAG,SAAS,EAClC,MAAM,EAAE,aAAa,GAAG,SAAS,GAC/B,OAAO,CAwCT","sourcesContent":["import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport type { ExtensionAPI } from \"../../core/extensions/types.ts\";\nimport { type GitNexusConfig, gitNexusConfigPath } from \"./config.ts\";\nimport { hubApiBase } from \"./device-auth.ts\";\n\n/**\n * Auto mode: models served through the hub's LLM gateway.\n *\n * The hub decides which model \"auto\" is and never tells the CLI — the policy\n * endpoint serves budget numbers (context window, output cap), not a name, so\n * there is nothing here that could display one. The entry the user sees is\n * \"Auto\", and the hub can move the underlying model without a CLI release.\n *\n * Providers must exist before the initial model is resolved, which happens\n * before any network round-trip could finish, so registration reads a policy\n * cache written the last time the hub was reachable:\n *\n * startup gate (already talks to the hub) -> refreshGatewayPolicy()\n * extension factory (synchronous) -> registerAutoProvider()\n *\n * The gate runs at every startup, so the cache is at most one session stale.\n * Entitlement is the hub's call: a 404 from the policy endpoint (feature off\n * for this account) deletes the cache and the next session has no Auto entry.\n */\n\nexport const AUTO_PROVIDER_ID = \"gitnexus\";\nexport const AUTO_MODEL_ID = \"auto\";\n\nexport interface GatewayPolicy {\n\tconfigured: boolean;\n\tcontextWindow: number;\n\tmaxTokens: number;\n\tusage?: { used: number; limit: number; remaining?: number };\n}\n\nfunction parseUsage(value: unknown): GatewayPolicy[\"usage\"] {\n\tif (!value || typeof value !== \"object\") return undefined;\n\tconst usage = value as Record<string, unknown>;\n\tif (typeof usage.used !== \"number\" || typeof usage.limit !== \"number\") return undefined;\n\treturn {\n\t\tused: usage.used,\n\t\tlimit: usage.limit,\n\t\t...(typeof usage.remaining === \"number\" ? { remaining: usage.remaining } : {}),\n\t};\n}\n\nexport function formatCreditBalance(policy: GatewayPolicy | undefined): string | undefined {\n\tif (!policy?.usage) return undefined;\n\tconst remaining = policy.usage.remaining ?? Math.max(0, policy.usage.limit - policy.usage.used);\n\treturn `Nexus Credits: ${remaining.toLocaleString()} / ${policy.usage.limit.toLocaleString()}`;\n}\n\nexport function gatewayCachePath(): string {\n\treturn join(dirname(gitNexusConfigPath()), \"gateway.json\");\n}\n\n/** Cached policy from the last successful refresh, or undefined. */\nexport function readGatewayCache(path: string = gatewayCachePath()): GatewayPolicy | undefined {\n\ttry {\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf8\")) as Partial<GatewayPolicy>;\n\t\tif (typeof parsed.contextWindow !== \"number\" || typeof parsed.maxTokens !== \"number\") return undefined;\n\t\treturn {\n\t\t\tconfigured: parsed.configured === true,\n\t\t\tcontextWindow: parsed.contextWindow,\n\t\t\tmaxTokens: parsed.maxTokens,\n\t\t\tusage: parseUsage(parsed.usage),\n\t\t};\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Ask the hub what auto mode looks like for this account and cache the answer.\n *\n * Never throws: auto mode is additive, and a hub outage at startup must not\n * cost the user their session. A definitive \"no\" (404: not entitled; 401/403:\n * bad credential) clears the cache; a transient failure keeps it, so a flaky\n * network does not flap the Auto entry in and out of the model list.\n */\nexport async function refreshGatewayPolicy(\n\t// No default: an explicit `undefined` must mean signed out, never \"read the\n\t// real config\" — a test passing undefined once sent a real token upstream.\n\tconfig: GitNexusConfig | undefined,\n\toptions: {\n\t\tfetchImpl?: typeof fetch;\n\t\tcachePath?: string;\n\t\ttimeoutMs?: number;\n\t} = {},\n): Promise<GatewayPolicy | undefined> {\n\tconst cachePath = options.cachePath ?? gatewayCachePath();\n\tif (!config) return undefined;\n\tconst fetchImpl = options.fetchImpl ?? fetch;\n\n\ttry {\n\t\tconst response = await fetchImpl(`${hubApiBase(config.hubUrl)}/llm/policy`, {\n\t\t\t// Device-bound tokens are rejected without the fingerprint headers the\n\t\t\t// login flow registered them with, so these mirror GitNexusClient.\n\t\t\theaders: {\n\t\t\t\tauthorization: `Bearer ${config.token}`,\n\t\t\t\taccept: \"application/json\",\n\t\t\t\t...(config.deviceFingerprint ? { \"x-device-fingerprint\": config.deviceFingerprint } : {}),\n\t\t\t\t...(config.deviceName ? { \"x-device-name\": config.deviceName } : {}),\n\t\t\t},\n\t\t\tsignal: AbortSignal.timeout(options.timeoutMs ?? 10_000),\n\t\t});\n\t\tif (response.status === 404 || response.status === 401 || response.status === 403) {\n\t\t\trmSync(cachePath, { force: true });\n\t\t\treturn undefined;\n\t\t}\n\t\tif (!response.ok) return readGatewayCache(cachePath);\n\n\t\tconst body = (await response.json()) as Partial<GatewayPolicy>;\n\t\tif (typeof body.contextWindow !== \"number\" || typeof body.maxTokens !== \"number\") {\n\t\t\treturn readGatewayCache(cachePath);\n\t\t}\n\t\tconst policy: GatewayPolicy = {\n\t\t\tconfigured: body.configured === true,\n\t\t\tcontextWindow: body.contextWindow,\n\t\t\tmaxTokens: body.maxTokens,\n\t\t\tusage: parseUsage(body.usage),\n\t\t};\n\t\tif (!existsSync(dirname(cachePath))) mkdirSync(dirname(cachePath), { recursive: true, mode: 0o700 });\n\t\twriteFileSync(cachePath, JSON.stringify(policy), { mode: 0o600 });\n\t\treturn policy;\n\t} catch {\n\t\treturn readGatewayCache(cachePath);\n\t}\n}\n\n/**\n * Register the Auto entry. Synchronous — called from the extension factory so\n * the provider exists before initial model resolution.\n *\n * No config, no cache, or a gateway with no upstream key all mean no\n * registration: an Auto entry that cannot serve a request is worse than none.\n */\nexport function registerAutoProvider(\n\tpi: ExtensionAPI,\n\tconfig: GitNexusConfig | undefined,\n\tpolicy: GatewayPolicy | undefined,\n): boolean {\n\tif (!config || !policy || !policy.configured) return false;\n\n\tpi.registerProvider(AUTO_PROVIDER_ID, {\n\t\tname: \"GitNexus\",\n\t\tbaseUrl: `${hubApiBase(config.hubUrl)}/llm`,\n\t\tapiKey: config.token,\n\t\tauthHeader: true,\n\t\t// Device-bound tokens 403 without the fingerprint they were minted with.\n\t\theaders: {\n\t\t\t...(config.deviceFingerprint ? { \"x-device-fingerprint\": config.deviceFingerprint } : {}),\n\t\t\t...(config.deviceName ? { \"x-device-name\": config.deviceName } : {}),\n\t\t},\n\t\tapi: \"openai-completions\",\n\t\tmodels: [\n\t\t\t{\n\t\t\t\tid: AUTO_MODEL_ID,\n\t\t\t\tname: \"Auto\",\n\t\t\t\t// The agent already owns a stable session ID. Forward it only to the\n\t\t\t\t// Hub, which hashes it before applying provider-specific cache affinity.\n\t\t\t\tcompat: {\n\t\t\t\t\tsendSessionAffinityHeaders: true,\n\t\t\t\t\tsessionAffinityFormat: \"openai-nosession\",\n\t\t\t\t},\n\t\t\t\t// The hub may route to a reasoning model, but reasoning controls are\n\t\t\t\t// provider-specific and \"auto\" promises nothing about the provider —\n\t\t\t\t// so none are offered.\n\t\t\t\treasoning: false,\n\t\t\t\t// The Nexus Agent beta is text-only at the Hub boundary, regardless\n\t\t\t\t// of the capabilities of the model selected behind Auto.\n\t\t\t\tinput: [\"text\"],\n\t\t\t\t// Spend is metered hub-side against Nexus Credits; there is no\n\t\t\t\t// per-token or dollar price to show.\n\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\t\t\tcontextWindow: policy.contextWindow,\n\t\t\t\tmaxTokens: policy.maxTokens,\n\t\t\t},\n\t\t],\n\t});\n\treturn true;\n}\n"]}
1
+ {"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../../../src/extensions/gitnexus/gateway.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,KAAK,cAAc,EAAsB,MAAM,aAAa,CAAC;AAGtE;;;;;;;;;;;;;;;;;;GAkBG;AAEH,eAAO,MAAM,gBAAgB,aAAa,CAAC;AAC3C,eAAO,MAAM,aAAa,SAAS,CAAC;AAEpC,MAAM,WAAW,aAAa;IAC7B,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5D;AAaD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAIzF;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,oEAAoE;AACpE,wBAAgB,gBAAgB,CAAC,IAAI,GAAE,MAA2B,GAAG,aAAa,GAAG,SAAS,CAa7F;AAED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CAGzC,MAAM,EAAE,cAAc,GAAG,SAAS,EAClC,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAChF,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAuCpC;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CACnC,EAAE,EAAE,YAAY,EAChB,MAAM,EAAE,cAAc,GAAG,SAAS,EAClC,MAAM,EAAE,aAAa,GAAG,SAAS,GAC/B,OAAO,CAgCT","sourcesContent":["import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport type { ExtensionAPI } from \"../../core/extensions/types.ts\";\nimport { type GitNexusConfig, gitNexusConfigPath } from \"./config.ts\";\nimport { hubApiBase } from \"./device-auth.ts\";\n\n/**\n * Auto mode: models served through the hub's LLM gateway.\n *\n * The hub decides which model \"auto\" is and never tells the CLI — the policy\n * endpoint serves budget numbers (context window, output cap), not a name, so\n * there is nothing here that could display one. The entry the user sees is\n * \"Auto\", and the hub can move the underlying model without a CLI release.\n *\n * Providers must exist before the initial model is resolved, which happens\n * before any network round-trip could finish, so registration reads a policy\n * cache written the last time the hub was reachable:\n *\n * startup gate (already talks to the hub) -> refreshGatewayPolicy()\n * extension factory (synchronous) -> registerAutoProvider()\n *\n * The gate runs at every startup, so the cache is at most one session stale.\n * Entitlement is the hub's call: a 404 from the policy endpoint (feature off\n * for this account) deletes the cache and the next session has no Auto entry.\n */\n\nexport const AUTO_PROVIDER_ID = \"gitnexus\";\nexport const AUTO_MODEL_ID = \"auto\";\n\nexport interface GatewayPolicy {\n\tconfigured: boolean;\n\tcontextWindow: number;\n\tmaxTokens: number;\n\tusage?: { used: number; limit: number; remaining?: number };\n}\n\nfunction parseUsage(value: unknown): GatewayPolicy[\"usage\"] {\n\tif (!value || typeof value !== \"object\") return undefined;\n\tconst usage = value as Record<string, unknown>;\n\tif (typeof usage.used !== \"number\" || typeof usage.limit !== \"number\") return undefined;\n\treturn {\n\t\tused: usage.used,\n\t\tlimit: usage.limit,\n\t\t...(typeof usage.remaining === \"number\" ? { remaining: usage.remaining } : {}),\n\t};\n}\n\nexport function formatCreditBalance(policy: GatewayPolicy | undefined): string | undefined {\n\tif (!policy?.usage) return undefined;\n\tconst remaining = policy.usage.remaining ?? Math.max(0, policy.usage.limit - policy.usage.used);\n\treturn `Nexus Credits: ${remaining.toLocaleString()} / ${policy.usage.limit.toLocaleString()}`;\n}\n\nexport function gatewayCachePath(): string {\n\treturn join(dirname(gitNexusConfigPath()), \"gateway.json\");\n}\n\n/** Cached policy from the last successful refresh, or undefined. */\nexport function readGatewayCache(path: string = gatewayCachePath()): GatewayPolicy | undefined {\n\ttry {\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf8\")) as Partial<GatewayPolicy>;\n\t\tif (typeof parsed.contextWindow !== \"number\" || typeof parsed.maxTokens !== \"number\") return undefined;\n\t\treturn {\n\t\t\tconfigured: parsed.configured === true,\n\t\t\tcontextWindow: parsed.contextWindow,\n\t\t\tmaxTokens: parsed.maxTokens,\n\t\t\tusage: parseUsage(parsed.usage),\n\t\t};\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Ask the hub what auto mode looks like for this account and cache the answer.\n *\n * Never throws: auto mode is additive, and a hub outage at startup must not\n * cost the user their session. A definitive \"no\" (404: not entitled; 401/403:\n * bad credential) clears the cache; a transient failure keeps it, so a flaky\n * network does not flap the Auto entry in and out of the model list.\n */\nexport async function refreshGatewayPolicy(\n\t// No default: an explicit `undefined` must mean signed out, never \"read the\n\t// real config\" — a test passing undefined once sent a real token upstream.\n\tconfig: GitNexusConfig | undefined,\n\toptions: { fetchImpl?: typeof fetch; cachePath?: string; timeoutMs?: number } = {},\n): Promise<GatewayPolicy | undefined> {\n\tconst cachePath = options.cachePath ?? gatewayCachePath();\n\tif (!config) return undefined;\n\tconst fetchImpl = options.fetchImpl ?? fetch;\n\n\ttry {\n\t\tconst response = await fetchImpl(`${hubApiBase(config.hubUrl)}/llm/policy`, {\n\t\t\t// Device-bound tokens are rejected without the fingerprint headers the\n\t\t\t// login flow registered them with, so these mirror GitNexusClient.\n\t\t\theaders: {\n\t\t\t\tauthorization: `Bearer ${config.token}`,\n\t\t\t\taccept: \"application/json\",\n\t\t\t\t...(config.deviceFingerprint ? { \"x-device-fingerprint\": config.deviceFingerprint } : {}),\n\t\t\t\t...(config.deviceName ? { \"x-device-name\": config.deviceName } : {}),\n\t\t\t},\n\t\t\tsignal: AbortSignal.timeout(options.timeoutMs ?? 10_000),\n\t\t});\n\t\tif (response.status === 404 || response.status === 401 || response.status === 403) {\n\t\t\trmSync(cachePath, { force: true });\n\t\t\treturn undefined;\n\t\t}\n\t\tif (!response.ok) return readGatewayCache(cachePath);\n\n\t\tconst body = (await response.json()) as Partial<GatewayPolicy>;\n\t\tif (typeof body.contextWindow !== \"number\" || typeof body.maxTokens !== \"number\") {\n\t\t\treturn readGatewayCache(cachePath);\n\t\t}\n\t\tconst policy: GatewayPolicy = {\n\t\t\tconfigured: body.configured === true,\n\t\t\tcontextWindow: body.contextWindow,\n\t\t\tmaxTokens: body.maxTokens,\n\t\t\tusage: parseUsage(body.usage),\n\t\t};\n\t\tif (!existsSync(dirname(cachePath))) mkdirSync(dirname(cachePath), { recursive: true, mode: 0o700 });\n\t\twriteFileSync(cachePath, JSON.stringify(policy), { mode: 0o600 });\n\t\treturn policy;\n\t} catch {\n\t\treturn readGatewayCache(cachePath);\n\t}\n}\n\n/**\n * Register the Auto entry. Synchronous — called from the extension factory so\n * the provider exists before initial model resolution.\n *\n * No config, no cache, or a gateway with no upstream key all mean no\n * registration: an Auto entry that cannot serve a request is worse than none.\n */\nexport function registerAutoProvider(\n\tpi: ExtensionAPI,\n\tconfig: GitNexusConfig | undefined,\n\tpolicy: GatewayPolicy | undefined,\n): boolean {\n\tif (!config || !policy || !policy.configured) return false;\n\n\tpi.registerProvider(AUTO_PROVIDER_ID, {\n\t\tname: \"GitNexus\",\n\t\tbaseUrl: `${hubApiBase(config.hubUrl)}/llm`,\n\t\tapiKey: config.token,\n\t\tauthHeader: true,\n\t\t// Device-bound tokens 403 without the fingerprint they were minted with.\n\t\theaders: {\n\t\t\t...(config.deviceFingerprint ? { \"x-device-fingerprint\": config.deviceFingerprint } : {}),\n\t\t\t...(config.deviceName ? { \"x-device-name\": config.deviceName } : {}),\n\t\t},\n\t\tapi: \"openai-completions\",\n\t\tmodels: [\n\t\t\t{\n\t\t\t\tid: AUTO_MODEL_ID,\n\t\t\t\tname: \"Auto\",\n\t\t\t\t// The hub may route to a reasoning model, but reasoning controls are\n\t\t\t\t// provider-specific and \"auto\" promises nothing about the provider —\n\t\t\t\t// so none are offered.\n\t\t\t\treasoning: false,\n\t\t\t\tinput: [\"text\", \"image\"],\n\t\t\t\t// Spend is metered hub-side against Nexus Credits; there is no\n\t\t\t\t// per-token or dollar price to show.\n\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\t\t\tcontextWindow: policy.contextWindow,\n\t\t\t\tmaxTokens: policy.maxTokens,\n\t\t\t},\n\t\t],\n\t});\n\treturn true;\n}\n"]}
@@ -139,19 +139,11 @@ export function registerAutoProvider(pi, config, policy) {
139
139
  {
140
140
  id: AUTO_MODEL_ID,
141
141
  name: "Auto",
142
- // The agent already owns a stable session ID. Forward it only to the
143
- // Hub, which hashes it before applying provider-specific cache affinity.
144
- compat: {
145
- sendSessionAffinityHeaders: true,
146
- sessionAffinityFormat: "openai-nosession",
147
- },
148
142
  // The hub may route to a reasoning model, but reasoning controls are
149
143
  // provider-specific and "auto" promises nothing about the provider —
150
144
  // so none are offered.
151
145
  reasoning: false,
152
- // The Nexus Agent beta is text-only at the Hub boundary, regardless
153
- // of the capabilities of the model selected behind Auto.
154
- input: ["text"],
146
+ input: ["text", "image"],
155
147
  // Spend is metered hub-side against Nexus Credits; there is no
156
148
  // per-token or dollar price to show.
157
149
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
@@ -1 +1 @@
1
- {"version":3,"file":"gateway.js","sourceRoot":"","sources":["../../../src/extensions/gitnexus/gateway.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport type { ExtensionAPI } from \"../../core/extensions/types.ts\";\nimport { type GitNexusConfig, gitNexusConfigPath } from \"./config.ts\";\nimport { hubApiBase } from \"./device-auth.ts\";\n\n/**\n * Auto mode: models served through the hub's LLM gateway.\n *\n * The hub decides which model \"auto\" is and never tells the CLI — the policy\n * endpoint serves budget numbers (context window, output cap), not a name, so\n * there is nothing here that could display one. The entry the user sees is\n * \"Auto\", and the hub can move the underlying model without a CLI release.\n *\n * Providers must exist before the initial model is resolved, which happens\n * before any network round-trip could finish, so registration reads a policy\n * cache written the last time the hub was reachable:\n *\n * startup gate (already talks to the hub) -> refreshGatewayPolicy()\n * extension factory (synchronous) -> registerAutoProvider()\n *\n * The gate runs at every startup, so the cache is at most one session stale.\n * Entitlement is the hub's call: a 404 from the policy endpoint (feature off\n * for this account) deletes the cache and the next session has no Auto entry.\n */\n\nexport const AUTO_PROVIDER_ID = \"gitnexus\";\nexport const AUTO_MODEL_ID = \"auto\";\n\nexport interface GatewayPolicy {\n\tconfigured: boolean;\n\tcontextWindow: number;\n\tmaxTokens: number;\n\tusage?: { used: number; limit: number; remaining?: number };\n}\n\nfunction parseUsage(value: unknown): GatewayPolicy[\"usage\"] {\n\tif (!value || typeof value !== \"object\") return undefined;\n\tconst usage = value as Record<string, unknown>;\n\tif (typeof usage.used !== \"number\" || typeof usage.limit !== \"number\") return undefined;\n\treturn {\n\t\tused: usage.used,\n\t\tlimit: usage.limit,\n\t\t...(typeof usage.remaining === \"number\" ? { remaining: usage.remaining } : {}),\n\t};\n}\n\nexport function formatCreditBalance(policy: GatewayPolicy | undefined): string | undefined {\n\tif (!policy?.usage) return undefined;\n\tconst remaining = policy.usage.remaining ?? Math.max(0, policy.usage.limit - policy.usage.used);\n\treturn `Nexus Credits: ${remaining.toLocaleString()} / ${policy.usage.limit.toLocaleString()}`;\n}\n\nexport function gatewayCachePath(): string {\n\treturn join(dirname(gitNexusConfigPath()), \"gateway.json\");\n}\n\n/** Cached policy from the last successful refresh, or undefined. */\nexport function readGatewayCache(path: string = gatewayCachePath()): GatewayPolicy | undefined {\n\ttry {\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf8\")) as Partial<GatewayPolicy>;\n\t\tif (typeof parsed.contextWindow !== \"number\" || typeof parsed.maxTokens !== \"number\") return undefined;\n\t\treturn {\n\t\t\tconfigured: parsed.configured === true,\n\t\t\tcontextWindow: parsed.contextWindow,\n\t\t\tmaxTokens: parsed.maxTokens,\n\t\t\tusage: parseUsage(parsed.usage),\n\t\t};\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Ask the hub what auto mode looks like for this account and cache the answer.\n *\n * Never throws: auto mode is additive, and a hub outage at startup must not\n * cost the user their session. A definitive \"no\" (404: not entitled; 401/403:\n * bad credential) clears the cache; a transient failure keeps it, so a flaky\n * network does not flap the Auto entry in and out of the model list.\n */\nexport async function refreshGatewayPolicy(\n\t// No default: an explicit `undefined` must mean signed out, never \"read the\n\t// real config\" — a test passing undefined once sent a real token upstream.\n\tconfig: GitNexusConfig | undefined,\n\toptions: {\n\t\tfetchImpl?: typeof fetch;\n\t\tcachePath?: string;\n\t\ttimeoutMs?: number;\n\t} = {},\n): Promise<GatewayPolicy | undefined> {\n\tconst cachePath = options.cachePath ?? gatewayCachePath();\n\tif (!config) return undefined;\n\tconst fetchImpl = options.fetchImpl ?? fetch;\n\n\ttry {\n\t\tconst response = await fetchImpl(`${hubApiBase(config.hubUrl)}/llm/policy`, {\n\t\t\t// Device-bound tokens are rejected without the fingerprint headers the\n\t\t\t// login flow registered them with, so these mirror GitNexusClient.\n\t\t\theaders: {\n\t\t\t\tauthorization: `Bearer ${config.token}`,\n\t\t\t\taccept: \"application/json\",\n\t\t\t\t...(config.deviceFingerprint ? { \"x-device-fingerprint\": config.deviceFingerprint } : {}),\n\t\t\t\t...(config.deviceName ? { \"x-device-name\": config.deviceName } : {}),\n\t\t\t},\n\t\t\tsignal: AbortSignal.timeout(options.timeoutMs ?? 10_000),\n\t\t});\n\t\tif (response.status === 404 || response.status === 401 || response.status === 403) {\n\t\t\trmSync(cachePath, { force: true });\n\t\t\treturn undefined;\n\t\t}\n\t\tif (!response.ok) return readGatewayCache(cachePath);\n\n\t\tconst body = (await response.json()) as Partial<GatewayPolicy>;\n\t\tif (typeof body.contextWindow !== \"number\" || typeof body.maxTokens !== \"number\") {\n\t\t\treturn readGatewayCache(cachePath);\n\t\t}\n\t\tconst policy: GatewayPolicy = {\n\t\t\tconfigured: body.configured === true,\n\t\t\tcontextWindow: body.contextWindow,\n\t\t\tmaxTokens: body.maxTokens,\n\t\t\tusage: parseUsage(body.usage),\n\t\t};\n\t\tif (!existsSync(dirname(cachePath))) mkdirSync(dirname(cachePath), { recursive: true, mode: 0o700 });\n\t\twriteFileSync(cachePath, JSON.stringify(policy), { mode: 0o600 });\n\t\treturn policy;\n\t} catch {\n\t\treturn readGatewayCache(cachePath);\n\t}\n}\n\n/**\n * Register the Auto entry. Synchronous — called from the extension factory so\n * the provider exists before initial model resolution.\n *\n * No config, no cache, or a gateway with no upstream key all mean no\n * registration: an Auto entry that cannot serve a request is worse than none.\n */\nexport function registerAutoProvider(\n\tpi: ExtensionAPI,\n\tconfig: GitNexusConfig | undefined,\n\tpolicy: GatewayPolicy | undefined,\n): boolean {\n\tif (!config || !policy || !policy.configured) return false;\n\n\tpi.registerProvider(AUTO_PROVIDER_ID, {\n\t\tname: \"GitNexus\",\n\t\tbaseUrl: `${hubApiBase(config.hubUrl)}/llm`,\n\t\tapiKey: config.token,\n\t\tauthHeader: true,\n\t\t// Device-bound tokens 403 without the fingerprint they were minted with.\n\t\theaders: {\n\t\t\t...(config.deviceFingerprint ? { \"x-device-fingerprint\": config.deviceFingerprint } : {}),\n\t\t\t...(config.deviceName ? { \"x-device-name\": config.deviceName } : {}),\n\t\t},\n\t\tapi: \"openai-completions\",\n\t\tmodels: [\n\t\t\t{\n\t\t\t\tid: AUTO_MODEL_ID,\n\t\t\t\tname: \"Auto\",\n\t\t\t\t// The agent already owns a stable session ID. Forward it only to the\n\t\t\t\t// Hub, which hashes it before applying provider-specific cache affinity.\n\t\t\t\tcompat: {\n\t\t\t\t\tsendSessionAffinityHeaders: true,\n\t\t\t\t\tsessionAffinityFormat: \"openai-nosession\",\n\t\t\t\t},\n\t\t\t\t// The hub may route to a reasoning model, but reasoning controls are\n\t\t\t\t// provider-specific and \"auto\" promises nothing about the provider —\n\t\t\t\t// so none are offered.\n\t\t\t\treasoning: false,\n\t\t\t\t// The Nexus Agent beta is text-only at the Hub boundary, regardless\n\t\t\t\t// of the capabilities of the model selected behind Auto.\n\t\t\t\tinput: [\"text\"],\n\t\t\t\t// Spend is metered hub-side against Nexus Credits; there is no\n\t\t\t\t// per-token or dollar price to show.\n\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\t\t\tcontextWindow: policy.contextWindow,\n\t\t\t\tmaxTokens: policy.maxTokens,\n\t\t\t},\n\t\t],\n\t});\n\treturn true;\n}\n"]}
1
+ {"version":3,"file":"gateway.js","sourceRoot":"","sources":["../../../src/extensions/gitnexus/gateway.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport type { ExtensionAPI } from \"../../core/extensions/types.ts\";\nimport { type GitNexusConfig, gitNexusConfigPath } from \"./config.ts\";\nimport { hubApiBase } from \"./device-auth.ts\";\n\n/**\n * Auto mode: models served through the hub's LLM gateway.\n *\n * The hub decides which model \"auto\" is and never tells the CLI — the policy\n * endpoint serves budget numbers (context window, output cap), not a name, so\n * there is nothing here that could display one. The entry the user sees is\n * \"Auto\", and the hub can move the underlying model without a CLI release.\n *\n * Providers must exist before the initial model is resolved, which happens\n * before any network round-trip could finish, so registration reads a policy\n * cache written the last time the hub was reachable:\n *\n * startup gate (already talks to the hub) -> refreshGatewayPolicy()\n * extension factory (synchronous) -> registerAutoProvider()\n *\n * The gate runs at every startup, so the cache is at most one session stale.\n * Entitlement is the hub's call: a 404 from the policy endpoint (feature off\n * for this account) deletes the cache and the next session has no Auto entry.\n */\n\nexport const AUTO_PROVIDER_ID = \"gitnexus\";\nexport const AUTO_MODEL_ID = \"auto\";\n\nexport interface GatewayPolicy {\n\tconfigured: boolean;\n\tcontextWindow: number;\n\tmaxTokens: number;\n\tusage?: { used: number; limit: number; remaining?: number };\n}\n\nfunction parseUsage(value: unknown): GatewayPolicy[\"usage\"] {\n\tif (!value || typeof value !== \"object\") return undefined;\n\tconst usage = value as Record<string, unknown>;\n\tif (typeof usage.used !== \"number\" || typeof usage.limit !== \"number\") return undefined;\n\treturn {\n\t\tused: usage.used,\n\t\tlimit: usage.limit,\n\t\t...(typeof usage.remaining === \"number\" ? { remaining: usage.remaining } : {}),\n\t};\n}\n\nexport function formatCreditBalance(policy: GatewayPolicy | undefined): string | undefined {\n\tif (!policy?.usage) return undefined;\n\tconst remaining = policy.usage.remaining ?? Math.max(0, policy.usage.limit - policy.usage.used);\n\treturn `Nexus Credits: ${remaining.toLocaleString()} / ${policy.usage.limit.toLocaleString()}`;\n}\n\nexport function gatewayCachePath(): string {\n\treturn join(dirname(gitNexusConfigPath()), \"gateway.json\");\n}\n\n/** Cached policy from the last successful refresh, or undefined. */\nexport function readGatewayCache(path: string = gatewayCachePath()): GatewayPolicy | undefined {\n\ttry {\n\t\tconst parsed = JSON.parse(readFileSync(path, \"utf8\")) as Partial<GatewayPolicy>;\n\t\tif (typeof parsed.contextWindow !== \"number\" || typeof parsed.maxTokens !== \"number\") return undefined;\n\t\treturn {\n\t\t\tconfigured: parsed.configured === true,\n\t\t\tcontextWindow: parsed.contextWindow,\n\t\t\tmaxTokens: parsed.maxTokens,\n\t\t\tusage: parseUsage(parsed.usage),\n\t\t};\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Ask the hub what auto mode looks like for this account and cache the answer.\n *\n * Never throws: auto mode is additive, and a hub outage at startup must not\n * cost the user their session. A definitive \"no\" (404: not entitled; 401/403:\n * bad credential) clears the cache; a transient failure keeps it, so a flaky\n * network does not flap the Auto entry in and out of the model list.\n */\nexport async function refreshGatewayPolicy(\n\t// No default: an explicit `undefined` must mean signed out, never \"read the\n\t// real config\" — a test passing undefined once sent a real token upstream.\n\tconfig: GitNexusConfig | undefined,\n\toptions: { fetchImpl?: typeof fetch; cachePath?: string; timeoutMs?: number } = {},\n): Promise<GatewayPolicy | undefined> {\n\tconst cachePath = options.cachePath ?? gatewayCachePath();\n\tif (!config) return undefined;\n\tconst fetchImpl = options.fetchImpl ?? fetch;\n\n\ttry {\n\t\tconst response = await fetchImpl(`${hubApiBase(config.hubUrl)}/llm/policy`, {\n\t\t\t// Device-bound tokens are rejected without the fingerprint headers the\n\t\t\t// login flow registered them with, so these mirror GitNexusClient.\n\t\t\theaders: {\n\t\t\t\tauthorization: `Bearer ${config.token}`,\n\t\t\t\taccept: \"application/json\",\n\t\t\t\t...(config.deviceFingerprint ? { \"x-device-fingerprint\": config.deviceFingerprint } : {}),\n\t\t\t\t...(config.deviceName ? { \"x-device-name\": config.deviceName } : {}),\n\t\t\t},\n\t\t\tsignal: AbortSignal.timeout(options.timeoutMs ?? 10_000),\n\t\t});\n\t\tif (response.status === 404 || response.status === 401 || response.status === 403) {\n\t\t\trmSync(cachePath, { force: true });\n\t\t\treturn undefined;\n\t\t}\n\t\tif (!response.ok) return readGatewayCache(cachePath);\n\n\t\tconst body = (await response.json()) as Partial<GatewayPolicy>;\n\t\tif (typeof body.contextWindow !== \"number\" || typeof body.maxTokens !== \"number\") {\n\t\t\treturn readGatewayCache(cachePath);\n\t\t}\n\t\tconst policy: GatewayPolicy = {\n\t\t\tconfigured: body.configured === true,\n\t\t\tcontextWindow: body.contextWindow,\n\t\t\tmaxTokens: body.maxTokens,\n\t\t\tusage: parseUsage(body.usage),\n\t\t};\n\t\tif (!existsSync(dirname(cachePath))) mkdirSync(dirname(cachePath), { recursive: true, mode: 0o700 });\n\t\twriteFileSync(cachePath, JSON.stringify(policy), { mode: 0o600 });\n\t\treturn policy;\n\t} catch {\n\t\treturn readGatewayCache(cachePath);\n\t}\n}\n\n/**\n * Register the Auto entry. Synchronous — called from the extension factory so\n * the provider exists before initial model resolution.\n *\n * No config, no cache, or a gateway with no upstream key all mean no\n * registration: an Auto entry that cannot serve a request is worse than none.\n */\nexport function registerAutoProvider(\n\tpi: ExtensionAPI,\n\tconfig: GitNexusConfig | undefined,\n\tpolicy: GatewayPolicy | undefined,\n): boolean {\n\tif (!config || !policy || !policy.configured) return false;\n\n\tpi.registerProvider(AUTO_PROVIDER_ID, {\n\t\tname: \"GitNexus\",\n\t\tbaseUrl: `${hubApiBase(config.hubUrl)}/llm`,\n\t\tapiKey: config.token,\n\t\tauthHeader: true,\n\t\t// Device-bound tokens 403 without the fingerprint they were minted with.\n\t\theaders: {\n\t\t\t...(config.deviceFingerprint ? { \"x-device-fingerprint\": config.deviceFingerprint } : {}),\n\t\t\t...(config.deviceName ? { \"x-device-name\": config.deviceName } : {}),\n\t\t},\n\t\tapi: \"openai-completions\",\n\t\tmodels: [\n\t\t\t{\n\t\t\t\tid: AUTO_MODEL_ID,\n\t\t\t\tname: \"Auto\",\n\t\t\t\t// The hub may route to a reasoning model, but reasoning controls are\n\t\t\t\t// provider-specific and \"auto\" promises nothing about the provider —\n\t\t\t\t// so none are offered.\n\t\t\t\treasoning: false,\n\t\t\t\tinput: [\"text\", \"image\"],\n\t\t\t\t// Spend is metered hub-side against Nexus Credits; there is no\n\t\t\t\t// per-token or dollar price to show.\n\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\t\t\t\tcontextWindow: policy.contextWindow,\n\t\t\t\tmaxTokens: policy.maxTokens,\n\t\t\t},\n\t\t],\n\t});\n\treturn true;\n}\n"]}