@akon-labs/nexus-cli 0.1.5 → 0.1.7

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 (94) hide show
  1. package/CHANGELOG.md +29 -7
  2. package/README.md +2 -3
  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 +155 -213
  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 +3 -2
  12. package/dist/core/auth-guidance.js.map +1 -1
  13. package/dist/core/auto-model.d.ts +9 -0
  14. package/dist/core/auto-model.d.ts.map +1 -0
  15. package/dist/core/auto-model.js +11 -0
  16. package/dist/core/auto-model.js.map +1 -0
  17. package/dist/core/extensions/loader.d.ts.map +1 -1
  18. package/dist/core/extensions/loader.js +4 -8
  19. package/dist/core/extensions/loader.js.map +1 -1
  20. package/dist/core/model-resolver.d.ts.map +1 -1
  21. package/dist/core/model-resolver.js +2 -1
  22. package/dist/core/model-resolver.js.map +1 -1
  23. package/dist/core/models-dev-catalog.d.ts.map +1 -1
  24. package/dist/core/models-dev-catalog.js +11 -7
  25. package/dist/core/models-dev-catalog.js.map +1 -1
  26. package/dist/core/sdk.d.ts +1 -1
  27. package/dist/core/sdk.d.ts.map +1 -1
  28. package/dist/core/sdk.js +1 -1
  29. package/dist/core/sdk.js.map +1 -1
  30. package/dist/core/slash-commands.d.ts.map +1 -1
  31. package/dist/core/slash-commands.js +1 -1
  32. package/dist/core/slash-commands.js.map +1 -1
  33. package/dist/core/tools/websearch.d.ts.map +1 -1
  34. package/dist/core/tools/websearch.js +52 -37
  35. package/dist/core/tools/websearch.js.map +1 -1
  36. package/dist/extensions/gitnexus/device-auth.d.ts.map +1 -1
  37. package/dist/extensions/gitnexus/device-auth.js +2 -1
  38. package/dist/extensions/gitnexus/device-auth.js.map +1 -1
  39. package/dist/extensions/gitnexus/gateway.d.ts +0 -2
  40. package/dist/extensions/gitnexus/gateway.d.ts.map +1 -1
  41. package/dist/extensions/gitnexus/gateway.js +10 -22
  42. package/dist/extensions/gitnexus/gateway.js.map +1 -1
  43. package/dist/extensions/gitnexus/index.d.ts.map +1 -1
  44. package/dist/extensions/gitnexus/index.js +10 -4
  45. package/dist/extensions/gitnexus/index.js.map +1 -1
  46. package/dist/extensions/web-access.d.ts +1 -6
  47. package/dist/extensions/web-access.d.ts.map +1 -1
  48. package/dist/extensions/web-access.js +8 -21
  49. package/dist/extensions/web-access.js.map +1 -1
  50. package/dist/index.d.ts +1 -1
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +159 -213
  53. package/dist/index.js.map +1 -1
  54. package/dist/main.d.ts.map +1 -1
  55. package/dist/main.js +1 -1
  56. package/dist/main.js.map +1 -1
  57. package/dist/migrations.d.ts.map +1 -1
  58. package/dist/migrations.js +2 -2
  59. package/dist/migrations.js.map +1 -1
  60. package/dist/modes/interactive/components/command-palette.d.ts.map +1 -1
  61. package/dist/modes/interactive/components/command-palette.js +2 -2
  62. package/dist/modes/interactive/components/command-palette.js.map +1 -1
  63. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  64. package/dist/modes/interactive/components/model-selector.js +18 -7
  65. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  66. package/dist/modes/interactive/interactive-mode.d.ts +0 -5
  67. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  68. package/dist/modes/interactive/interactive-mode.js +22 -128
  69. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  70. package/dist/rpc-entry.js +155 -213
  71. package/dist/vendor/ai.js +12 -12
  72. package/docs/compaction.md +1 -1
  73. package/docs/rpc.md +1 -1
  74. package/docs/settings.md +1 -1
  75. package/docs/usage.md +0 -2
  76. package/examples/extensions/border-status-editor.ts +1 -6
  77. package/examples/extensions/custom-compaction.ts +1 -1
  78. package/examples/extensions/custom-footer.ts +1 -1
  79. package/examples/extensions/custom-provider-anthropic/index.ts +3 -3
  80. package/examples/extensions/custom-provider-gitlab-duo/index.ts +1 -1
  81. package/examples/extensions/gondolin/index.ts +1 -1
  82. package/examples/extensions/handoff.ts +2 -2
  83. package/examples/extensions/hello.ts +1 -1
  84. package/examples/extensions/plan-mode/index.ts +1 -1
  85. package/examples/extensions/preset.ts +1 -1
  86. package/examples/extensions/qna.ts +1 -1
  87. package/examples/extensions/sandbox/index.ts +1 -1
  88. package/examples/extensions/subagent/index.ts +3 -3
  89. package/examples/extensions/summarize.ts +1 -1
  90. package/examples/extensions/tic-tac-toe.ts +1 -1
  91. package/examples/extensions/todo.ts +1 -1
  92. package/examples/extensions/tool-override.ts +1 -1
  93. package/npm-shrinkwrap.json +2 -2
  94. package/package.json +1 -1
@@ -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\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
+ {"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 +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,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"]}
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"]}
@@ -115,7 +115,8 @@ 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 = typeof data.interval === "number" ? data.interval * 1000 : intervalMs + 1000;
118
+ intervalMs =
119
+ typeof data.interval === "number" && data.interval > 0 ? data.interval * 1000 : intervalMs + 1000;
119
120
  break;
120
121
  case "access_denied":
121
122
  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 = 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
+ {"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"]}
@@ -19,8 +19,6 @@ import { type GitNexusConfig } from "./config.ts";
19
19
  * Entitlement is the hub's call: a 404 from the policy endpoint (feature off
20
20
  * for this account) deletes the cache and the next session has no Auto entry.
21
21
  */
22
- export declare const AUTO_PROVIDER_ID = "gitnexus";
23
- export declare const AUTO_MODEL_ID = "auto";
24
22
  export interface GatewayPolicy {
25
23
  configured: boolean;
26
24
  contextWindow: number;
@@ -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;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"]}
1
+ {"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../../../src/extensions/gitnexus/gateway.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,KAAK,cAAc,EAAsB,MAAM,aAAa,CAAC;AAGtE;;;;;;;;;;;;;;;;;;GAkBG;AAEH,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 { AUTO_MODEL_ID, AUTO_PROVIDER_ID } from \"../../core/auto-model.ts\";\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 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,28 +1,8 @@
1
1
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
+ import { AUTO_MODEL_ID, AUTO_PROVIDER_ID } from "../../core/auto-model.js";
3
4
  import { gitNexusConfigPath } from "./config.js";
4
5
  import { hubApiBase } from "./device-auth.js";
5
- /**
6
- * Auto mode: models served through the hub's LLM gateway.
7
- *
8
- * The hub decides which model "auto" is and never tells the CLI — the policy
9
- * endpoint serves budget numbers (context window, output cap), not a name, so
10
- * there is nothing here that could display one. The entry the user sees is
11
- * "Auto", and the hub can move the underlying model without a CLI release.
12
- *
13
- * Providers must exist before the initial model is resolved, which happens
14
- * before any network round-trip could finish, so registration reads a policy
15
- * cache written the last time the hub was reachable:
16
- *
17
- * startup gate (already talks to the hub) -> refreshGatewayPolicy()
18
- * extension factory (synchronous) -> registerAutoProvider()
19
- *
20
- * The gate runs at every startup, so the cache is at most one session stale.
21
- * Entitlement is the hub's call: a 404 from the policy endpoint (feature off
22
- * for this account) deletes the cache and the next session has no Auto entry.
23
- */
24
- export const AUTO_PROVIDER_ID = "gitnexus";
25
- export const AUTO_MODEL_ID = "auto";
26
6
  function parseUsage(value) {
27
7
  if (!value || typeof value !== "object")
28
8
  return undefined;
@@ -139,11 +119,19 @@ export function registerAutoProvider(pi, config, policy) {
139
119
  {
140
120
  id: AUTO_MODEL_ID,
141
121
  name: "Auto",
122
+ // The agent already owns a stable session ID. Forward it only to the
123
+ // Hub, which hashes it before applying provider-specific cache affinity.
124
+ compat: {
125
+ sendSessionAffinityHeaders: true,
126
+ sessionAffinityFormat: "openai-nosession",
127
+ },
142
128
  // The hub may route to a reasoning model, but reasoning controls are
143
129
  // provider-specific and "auto" promises nothing about the provider —
144
130
  // so none are offered.
145
131
  reasoning: false,
146
- input: ["text", "image"],
132
+ // The Nexus Agent beta is text-only at the Hub boundary, regardless
133
+ // of the capabilities of the model selected behind Auto.
134
+ input: ["text"],
147
135
  // Spend is metered hub-side against Nexus Credits; there is no
148
136
  // per-token or dollar price to show.
149
137
  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":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACrF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE1C,OAAO,EAAuB,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACtE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,CAAC,MAAM,gBAAgB,GAAG,UAAU,CAAC;AAC3C,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC;AASpC,SAAS,UAAU,CAAC,KAAc,EAA0B;IAC3D,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAC1D,MAAM,KAAK,GAAG,KAAgC,CAAC;IAC/C,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IACxF,OAAO;QACN,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,GAAG,CAAC,OAAO,KAAK,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC9E,CAAC;AAAA,CACF;AAED,MAAM,UAAU,mBAAmB,CAAC,MAAiC,EAAsB;IAC1F,IAAI,CAAC,MAAM,EAAE,KAAK;QAAE,OAAO,SAAS,CAAC;IACrC,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAChG,OAAO,kBAAkB,SAAS,CAAC,cAAc,EAAE,MAAM,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,cAAc,EAAE,EAAE,CAAC;AAAA,CAC/F;AAED,MAAM,UAAU,gBAAgB,GAAW;IAC1C,OAAO,IAAI,CAAC,OAAO,CAAC,kBAAkB,EAAE,CAAC,EAAE,cAAc,CAAC,CAAC;AAAA,CAC3D;AAED,oEAAoE;AACpE,MAAM,UAAU,gBAAgB,CAAC,IAAI,GAAW,gBAAgB,EAAE,EAA6B;IAC9F,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAA2B,CAAC;QAChF,IAAI,OAAO,MAAM,CAAC,aAAa,KAAK,QAAQ,IAAI,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;QACvG,OAAO;YACN,UAAU,EAAE,MAAM,CAAC,UAAU,KAAK,IAAI;YACtC,aAAa,EAAE,MAAM,CAAC,aAAa;YACnC,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,KAAK,EAAE,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC;SAC/B,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,SAAS,CAAC;IAClB,CAAC;AAAA,CACD;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB;AACzC,4EAA4E;AAC5E,6EAA2E;AAC3E,MAAkC,EAClC,OAAO,GAAyE,EAAE,EAC7C;IACrC,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,gBAAgB,EAAE,CAAC;IAC1D,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IAC9B,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,KAAK,CAAC;IAE7C,IAAI,CAAC;QACJ,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,aAAa,EAAE;YAC3E,uEAAuE;YACvE,mEAAmE;YACnE,OAAO,EAAE;gBACR,aAAa,EAAE,UAAU,MAAM,CAAC,KAAK,EAAE;gBACvC,MAAM,EAAE,kBAAkB;gBAC1B,GAAG,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,sBAAsB,EAAE,MAAM,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACzF,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACpE;YACD,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,IAAI,MAAM,CAAC;SACxD,CAAC,CAAC;QACH,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YACnF,MAAM,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YACnC,OAAO,SAAS,CAAC;QAClB,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,EAAE;YAAE,OAAO,gBAAgB,CAAC,SAAS,CAAC,CAAC;QAErD,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA2B,CAAC;QAC/D,IAAI,OAAO,IAAI,CAAC,aAAa,KAAK,QAAQ,IAAI,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ,EAAE,CAAC;YAClF,OAAO,gBAAgB,CAAC,SAAS,CAAC,CAAC;QACpC,CAAC;QACD,MAAM,MAAM,GAAkB;YAC7B,UAAU,EAAE,IAAI,CAAC,UAAU,KAAK,IAAI;YACpC,aAAa,EAAE,IAAI,CAAC,aAAa;YACjC,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,KAAK,EAAE,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC;SAC7B,CAAC;QACF,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YAAE,SAAS,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACrG,aAAa,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAClE,OAAO,MAAM,CAAC;IACf,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,gBAAgB,CAAC,SAAS,CAAC,CAAC;IACpC,CAAC;AAAA,CACD;AAED;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CACnC,EAAgB,EAChB,MAAkC,EAClC,MAAiC,EACvB;IACV,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU;QAAE,OAAO,KAAK,CAAC;IAE3D,EAAE,CAAC,gBAAgB,CAAC,gBAAgB,EAAE;QACrC,IAAI,EAAE,UAAU;QAChB,OAAO,EAAE,GAAG,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM;QAC3C,MAAM,EAAE,MAAM,CAAC,KAAK;QACpB,UAAU,EAAE,IAAI;QAChB,yEAAyE;QACzE,OAAO,EAAE;YACR,GAAG,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,sBAAsB,EAAE,MAAM,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACzF,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACpE;QACD,GAAG,EAAE,oBAAoB;QACzB,MAAM,EAAE;YACP;gBACC,EAAE,EAAE,aAAa;gBACjB,IAAI,EAAE,MAAM;gBACZ,qEAAqE;gBACrE,uEAAqE;gBACrE,uBAAuB;gBACvB,SAAS,EAAE,KAAK;gBAChB,KAAK,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC;gBACxB,+DAA+D;gBAC/D,qCAAqC;gBACrC,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE;gBAC1D,aAAa,EAAE,MAAM,CAAC,aAAa;gBACnC,SAAS,EAAE,MAAM,CAAC,SAAS;aAC3B;SACD;KACD,CAAC,CAAC;IACH,OAAO,IAAI,CAAC;AAAA,CACZ","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"]}
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 { AUTO_MODEL_ID, AUTO_PROVIDER_ID } from \"../../core/auto-model.ts\";\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 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 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/extensions/gitnexus/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAqBnE,2EAA2E;AAC3E,wBAAgB,wBAAwB,CACvC,SAAS,GAAE,MAAwB,EACnC,UAAU,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAoB,GAChD,MAAM,GAAG,SAAS,CAMpB;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CA2LhE","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { ExtensionAPI } from \"../../core/extensions/types.ts\";\nimport { graphRailStore } from \"../../core/graph-rail-store.ts\";\nimport { openBrowser } from \"../../utils/open-browser.ts\";\nimport { registerAugmentedGrep } from \"./augment.ts\";\nimport { registerChangeTracking } from \"./changes.ts\";\nimport { GitNexusClient, GitNexusClientError } from \"./client.ts\";\nimport { DEFAULT_HUB_URL, resolveGitNexusConfig, saveGitNexusConfig } from \"./config.ts\";\nimport { defaultDeviceName, deviceFingerprint, pollDeviceAuth, startDeviceAuth } from \"./device-auth.ts\";\nimport { formatCreditBalance, readGatewayCache, refreshGatewayPolicy, registerAutoProvider } from \"./gateway.ts\";\nimport { spawnObservatoryFetch } from \"./observatory.ts\";\nimport { buildGitNexusPrompt } from \"./prompt.ts\";\nimport { withGitNexusRenderer } from \"./render.ts\";\nimport {\n\tgitRemoteSlug,\n\ttype IndexedRepo,\n\tmatchCurrentRepo,\n\tparseRepoInventory,\n\ttype RepoInventory,\n} from \"./resolve.ts\";\nimport { createGitNexusTools } from \"./tools.ts\";\n\n/** Resolve workflow skills in source mode and bundled dist/cli.js mode. */\nexport function resolveGitNexusSkillsDir(\n\tmoduleUrl: string = import.meta.url,\n\tpathExists: (path: string) => boolean = existsSync,\n): string | undefined {\n\tconst candidates = [\n\t\tfileURLToPath(new URL(\"./skills/\", moduleUrl)),\n\t\tfileURLToPath(new URL(\"./extensions/gitnexus/skills/\", moduleUrl)),\n\t];\n\treturn candidates.find(pathExists);\n}\n\n/**\n * GitNexus built-in extension: connects nexus-cli to the GitNexus hub.\n *\n * - session_start: resolve config + repo inventory, register the six graph\n * tools and the augmented grep. No token / hub down / repo unindexed all\n * degrade to a plain coding agent (graph is additive, never load-bearing).\n * - before_agent_start: append the GitNexus guidance block (ported from the\n * CLAUDE.md generator) to the system prompt.\n */\nexport default function gitnexusExtension(pi: ExtensionAPI): void {\n\tlet inventory: RepoInventory | undefined;\n\tlet current: IndexedRepo | undefined;\n\tlet activeClient: GitNexusClient | undefined;\n\tlet toolsRegistered = false;\n\tlet startupNotice: string | undefined;\n\tlet loginRequired = false;\n\n\tconst cwd = process.cwd();\n\n\t// Auto mode must also register at factory time: the initial model is\n\t// resolved before session_start fires, and a provider that appears after\n\t// that is invisible to it. Reads the policy cache the startup gate wrote.\n\tlet gatewayPolicy = readGatewayCache();\n\tregisterAutoProvider(pi, resolveGitNexusConfig(), gatewayPolicy);\n\n\tfunction updateCreditStatus(ctx: { ui: { setStatus(key: string, text: string | undefined): void } }): void {\n\t\tctx.ui.setStatus(\"nexus-credits\", formatCreditBalance(gatewayPolicy));\n\t}\n\n\t// Built-in tool overrides must happen at factory time (like the bash-spawn-hook\n\t// example) — session_start registrations only add NEW names reliably. The wrapper\n\t// behaves as plain grep until connect() provides a client + current repo.\n\tregisterAugmentedGrep(\n\t\tpi,\n\t\tcwd,\n\t\t() => activeClient,\n\t\t() => current?.name,\n\t);\n\tregisterChangeTracking(pi, cwd);\n\n\tasync function connect(): Promise<void> {\n\t\tinventory = undefined;\n\t\tcurrent = undefined;\n\t\tactiveClient = undefined;\n\t\tstartupNotice = undefined;\n\t\tloginRequired = false;\n\t\tconst config = resolveGitNexusConfig();\n\t\tif (!config) return;\n\t\tconst client = new GitNexusClient(config);\n\t\ttry {\n\t\t\tconst listResult = await client.callTool(\"list_repos\", {});\n\t\t\tinventory = parseRepoInventory(GitNexusClient.text(listResult));\n\t\t} catch (error) {\n\t\t\tconst reason = error instanceof GitNexusClientError ? error.message : String(error);\n\t\t\tif (error instanceof GitNexusClientError && error.kind === \"auth\") {\n\t\t\t\tloginRequired = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstartupNotice = `GitNexus unavailable: ${reason}`;\n\t\t\treturn;\n\t\t}\n\n\t\tcurrent = matchCurrentRepo(inventory, gitRemoteSlug(cwd));\n\t\tactiveClient = client;\n\t\tgraphRailStore.setRepoList(inventory.repos.map((repo) => ({ name: repo.name, symbols: repo.symbols })));\n\t\t// Observatory (community map) rides a detached subprocess + temp-file\n\t\t// handoff — in-process hub calls made after startup can starve in some\n\t\t// environments, a fresh process never does. The UI picks the file up\n\t\t// lazily via tryLoadObservatoryCache.\n\t\tif (current) spawnObservatoryFetch(config, current.name);\n\t\tgraphRailStore.setRepo(\n\t\t\tcurrent\n\t\t\t\t? {\n\t\t\t\t\t\tname: current.name,\n\t\t\t\t\t\tsymbols: current.symbols,\n\t\t\t\t\t\trelationships: current.relationships,\n\t\t\t\t\t\tindexedAt: current.indexedAt,\n\t\t\t\t\t\trepoCount: inventory.repos.length,\n\t\t\t\t\t}\n\t\t\t\t: {\n\t\t\t\t\t\tname: \"\",\n\t\t\t\t\t\tsymbols: \"0\",\n\t\t\t\t\t\trelationships: \"0\",\n\t\t\t\t\t\tindexedAt: \"\",\n\t\t\t\t\t\trepoCount: inventory.repos.length,\n\t\t\t\t\t},\n\t\t\tcurrent !== undefined,\n\t\t);\n\t\tif (!toolsRegistered) {\n\t\t\tconst ctx = { client, currentRepo: () => current?.name, cwd };\n\t\t\tfor (const tool of createGitNexusTools(ctx)) pi.registerTool(withGitNexusRenderer(tool));\n\t\t\ttoolsRegistered = true;\n\t\t}\n\n\t\tstartupNotice = current\n\t\t\t? `GitNexus: connected — ${current.name} (${current.symbols} symbols, indexed ${current.indexedAt}), ${inventory.repos.length} repos available.`\n\t\t\t: `GitNexus: connected — current directory is not indexed; ${inventory.repos.length} indexed repos remain queryable. Index this repo from the Hub dashboard to unlock local graph context.`;\n\t}\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tawait connect();\n\t\tupdateCreditStatus(ctx);\n\t\tif (startupNotice) ctx.ui.notify(startupNotice, inventory ? \"info\" : \"warning\");\n\t});\n\n\tpi.on(\"agent_settled\", async (_event, ctx) => {\n\t\tif (!gatewayPolicy || ctx.model?.provider !== \"gitnexus\") return;\n\t\tconst config = resolveGitNexusConfig();\n\t\tif (config) gatewayPolicy = await refreshGatewayPolicy(config);\n\t\tupdateCreditStatus(ctx);\n\t});\n\n\t// Bundled GitNexus workflow skills (debugging, exploring, impact-analysis,\n\t// refactoring, pr-review, guide) — same SKILL.md content the Claude plugin\n\t// ships, adapted for hub-based indexing. Loaded on-demand by pi's skill system.\n\t// Skipped entirely when the user already has gitnexus skills (project or\n\t// global) so the startup screen doesn't report shadowed duplicates.\n\tpi.on(\"resources_discover\", (event) => {\n\t\tconst hasOwnGitnexusSkills =\n\t\t\texistsSync(join(event.cwd, \".agents\", \"skills\", \"gitnexus\")) ||\n\t\t\texistsSync(join(homedir(), \".agents\", \"skills\", \"gitnexus-debugging\"));\n\t\tif (hasOwnGitnexusSkills) return;\n\t\tconst skillsDir = resolveGitNexusSkillsDir();\n\t\tif (!skillsDir) return;\n\t\treturn { skillPaths: [skillsDir] };\n\t});\n\n\tpi.on(\"before_agent_start\", (event) => {\n\t\tif (!inventory) return;\n\t\treturn { systemPrompt: `${event.systemPrompt}\\n\\n${buildGitNexusPrompt(inventory, current)}` };\n\t});\n\n\tpi.registerCommand(\"gitnexus-login\", {\n\t\tdescription: \"Sign in to GitNexus (implementation of /login → GitNexus account; accepts a pasted token)\",\n\t\thandler: async (args, ctx) => {\n\t\t\tconst hubUrl = process.env.GITNEXUS_HUB_URL ?? DEFAULT_HUB_URL;\n\t\t\tconst pasted = args.trim();\n\n\t\t\t// Pasting a token stays supported for CI and headless installs.\n\t\t\tif (pasted) {\n\t\t\t\tawait new GitNexusClient({ hubUrl, token: pasted }).callTool(\"list_repos\", {});\n\t\t\t\tconst path = saveGitNexusConfig({ hubUrl, token: pasted });\n\t\t\t\tawait connect();\n\t\t\t\tctx.ui.notify(`GitNexus token saved to ${path}. ${startupNotice ?? \"\"}`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst deviceName = defaultDeviceName();\n\t\t\t\tconst fingerprint = deviceFingerprint();\n\t\t\t\tconst start = await startDeviceAuth(hubUrl, { deviceName, deviceFingerprint: fingerprint });\n\t\t\t\tconst link = start.verificationUrlComplete ?? start.verificationUrl;\n\t\t\t\t// The link carries the code, so an opened browser needs no typing.\n\t\t\t\t// Print it too: the launcher is best effort and can be headless.\n\t\t\t\tctx.ui.notify(`Code ${start.userCode} — opening ${link}`, \"info\");\n\t\t\t\topenBrowser(link);\n\n\t\t\t\tconst result = await pollDeviceAuth(hubUrl, start);\n\t\t\t\tconst config = {\n\t\t\t\t\thubUrl: result.hubUrl,\n\t\t\t\t\ttoken: result.token,\n\t\t\t\t\tdeviceFingerprint: fingerprint,\n\t\t\t\t\tdeviceName,\n\t\t\t\t};\n\t\t\t\tawait new GitNexusClient(config).callTool(\"list_repos\", {});\n\t\t\t\tconst path = saveGitNexusConfig(config);\n\t\t\t\tawait connect();\n\t\t\t\tctx.ui.notify(`Signed in. Token saved to ${path}. ${startupNotice ?? \"\"}`, \"info\");\n\t\t\t} catch (error) {\n\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\tctx.ui.notify(`GitNexus login failed: ${message}`, \"error\");\n\t\t\t}\n\t\t},\n\t});\n\n\tpi.registerCommand(\"gitnexus-status\", {\n\t\tdescription: \"Show GitNexus connection status\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tconst config = resolveGitNexusConfig();\n\t\t\tif (config) gatewayPolicy = await refreshGatewayPolicy(config);\n\t\t\tupdateCreditStatus(ctx);\n\t\t\tif (!inventory) {\n\t\t\t\tconst message = loginRequired\n\t\t\t\t\t? \"GitNexus session expired. Run /login to sign in again.\"\n\t\t\t\t\t: (startupNotice ?? \"GitNexus: not signed in. Run /login.\");\n\t\t\t\tctx.ui.notify(message, \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst repoLine = current ? `current repo: ${current.name}` : \"current repo: not indexed\";\n\t\t\tconst creditLine = formatCreditBalance(gatewayPolicy);\n\t\t\tctx.ui.notify(\n\t\t\t\t`GitNexus: connected — ${inventory.repos.length} repos, ${repoLine}${creditLine ? `, ${creditLine}` : \"\"}`,\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t},\n\t});\n}\n"]}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/extensions/gitnexus/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAqBnE,2EAA2E;AAC3E,wBAAgB,wBAAwB,CACvC,SAAS,GAAE,MAAwB,EACnC,UAAU,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAoB,GAChD,MAAM,GAAG,SAAS,CAMpB;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAgMhE","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { ExtensionAPI } from \"../../core/extensions/types.ts\";\nimport { graphRailStore } from \"../../core/graph-rail-store.ts\";\nimport { openBrowser } from \"../../utils/open-browser.ts\";\nimport { registerAugmentedGrep } from \"./augment.ts\";\nimport { registerChangeTracking } from \"./changes.ts\";\nimport { GitNexusClient, GitNexusClientError } from \"./client.ts\";\nimport { DEFAULT_HUB_URL, resolveGitNexusConfig, saveGitNexusConfig } from \"./config.ts\";\nimport { defaultDeviceName, deviceFingerprint, pollDeviceAuth, startDeviceAuth } from \"./device-auth.ts\";\nimport { formatCreditBalance, readGatewayCache, refreshGatewayPolicy, registerAutoProvider } from \"./gateway.ts\";\nimport { spawnObservatoryFetch } from \"./observatory.ts\";\nimport { buildGitNexusPrompt } from \"./prompt.ts\";\nimport { withGitNexusRenderer } from \"./render.ts\";\nimport {\n\tgitRemoteSlug,\n\ttype IndexedRepo,\n\tmatchCurrentRepo,\n\tparseRepoInventory,\n\ttype RepoInventory,\n} from \"./resolve.ts\";\nimport { createGitNexusTools } from \"./tools.ts\";\n\n/** Resolve workflow skills in source mode and bundled dist/cli.js mode. */\nexport function resolveGitNexusSkillsDir(\n\tmoduleUrl: string = import.meta.url,\n\tpathExists: (path: string) => boolean = existsSync,\n): string | undefined {\n\tconst candidates = [\n\t\tfileURLToPath(new URL(\"./skills/\", moduleUrl)),\n\t\tfileURLToPath(new URL(\"./extensions/gitnexus/skills/\", moduleUrl)),\n\t];\n\treturn candidates.find(pathExists);\n}\n\n/**\n * GitNexus built-in extension: connects nexus-cli to the GitNexus hub.\n *\n * - session_start: resolve config + repo inventory, register the six graph\n * tools and the augmented grep. No token / hub down / repo unindexed all\n * degrade to a plain coding agent (graph is additive, never load-bearing).\n * - before_agent_start: append the GitNexus guidance block (ported from the\n * CLAUDE.md generator) to the system prompt.\n */\nexport default function gitnexusExtension(pi: ExtensionAPI): void {\n\tlet inventory: RepoInventory | undefined;\n\tlet current: IndexedRepo | undefined;\n\tlet activeClient: GitNexusClient | undefined;\n\tlet toolsRegistered = false;\n\tlet startupNotice: string | undefined;\n\tlet loginRequired = false;\n\n\tconst cwd = process.cwd();\n\n\t// Auto mode must also register at factory time: the initial model is\n\t// resolved before session_start fires, and a provider that appears after\n\t// that is invisible to it. Reads the policy cache the startup gate wrote.\n\tlet gatewayPolicy = readGatewayCache();\n\tregisterAutoProvider(pi, resolveGitNexusConfig(), gatewayPolicy);\n\n\tfunction updateCreditStatus(ctx: { ui: { setStatus(key: string, text: string | undefined): void } }): void {\n\t\tctx.ui.setStatus(\"nexus-credits\", formatCreditBalance(gatewayPolicy));\n\t}\n\n\t// Built-in tool overrides must happen at factory time (like the bash-spawn-hook\n\t// example) — session_start registrations only add NEW names reliably. The wrapper\n\t// behaves as plain grep until connect() provides a client + current repo.\n\tregisterAugmentedGrep(\n\t\tpi,\n\t\tcwd,\n\t\t() => activeClient,\n\t\t() => current?.name,\n\t);\n\tregisterChangeTracking(pi, cwd);\n\n\tasync function connect(): Promise<void> {\n\t\tinventory = undefined;\n\t\tcurrent = undefined;\n\t\tactiveClient = undefined;\n\t\tstartupNotice = undefined;\n\t\tloginRequired = false;\n\t\tconst config = resolveGitNexusConfig();\n\t\tif (!config) return;\n\t\tconst client = new GitNexusClient(config);\n\t\ttry {\n\t\t\tconst listResult = await client.callTool(\"list_repos\", {});\n\t\t\tinventory = parseRepoInventory(GitNexusClient.text(listResult));\n\t\t} catch (error) {\n\t\t\tconst reason = error instanceof GitNexusClientError ? error.message : String(error);\n\t\t\tif (error instanceof GitNexusClientError && error.kind === \"auth\") {\n\t\t\t\tloginRequired = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstartupNotice = `GitNexus unavailable: ${reason}`;\n\t\t\treturn;\n\t\t}\n\n\t\tcurrent = matchCurrentRepo(inventory, gitRemoteSlug(cwd));\n\t\tactiveClient = client;\n\t\tgraphRailStore.setRepoList(inventory.repos.map((repo) => ({ name: repo.name, symbols: repo.symbols })));\n\t\t// Observatory (community map) rides a detached subprocess + temp-file\n\t\t// handoff — in-process hub calls made after startup can starve in some\n\t\t// environments, a fresh process never does. The UI picks the file up\n\t\t// lazily via tryLoadObservatoryCache.\n\t\tif (current) spawnObservatoryFetch(config, current.name);\n\t\tgraphRailStore.setRepo(\n\t\t\tcurrent\n\t\t\t\t? {\n\t\t\t\t\t\tname: current.name,\n\t\t\t\t\t\tsymbols: current.symbols,\n\t\t\t\t\t\trelationships: current.relationships,\n\t\t\t\t\t\tindexedAt: current.indexedAt,\n\t\t\t\t\t\trepoCount: inventory.repos.length,\n\t\t\t\t\t}\n\t\t\t\t: {\n\t\t\t\t\t\tname: \"\",\n\t\t\t\t\t\tsymbols: \"0\",\n\t\t\t\t\t\trelationships: \"0\",\n\t\t\t\t\t\tindexedAt: \"\",\n\t\t\t\t\t\trepoCount: inventory.repos.length,\n\t\t\t\t\t},\n\t\t\tcurrent !== undefined,\n\t\t);\n\t\tif (!toolsRegistered) {\n\t\t\tconst ctx = { client, currentRepo: () => current?.name, cwd };\n\t\t\tfor (const tool of createGitNexusTools(ctx)) pi.registerTool(withGitNexusRenderer(tool));\n\t\t\ttoolsRegistered = true;\n\t\t}\n\n\t\tstartupNotice = current\n\t\t\t? `GitNexus: connected — ${current.name} (${current.symbols} symbols, indexed ${current.indexedAt}), ${inventory.repos.length} repos available.`\n\t\t\t: `GitNexus: connected — current directory is not indexed; ${inventory.repos.length} indexed repos remain queryable. Index this repo from the Hub dashboard to unlock local graph context.`;\n\t}\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tawait connect();\n\t\tupdateCreditStatus(ctx);\n\t\tif (startupNotice) ctx.ui.notify(startupNotice, inventory ? \"info\" : \"warning\");\n\t});\n\n\tpi.on(\"agent_settled\", async (_event, ctx) => {\n\t\tif (!gatewayPolicy || ctx.model?.provider !== \"gitnexus\") return;\n\t\tconst config = resolveGitNexusConfig();\n\t\tif (config) gatewayPolicy = await refreshGatewayPolicy(config);\n\t\tupdateCreditStatus(ctx);\n\t});\n\n\t// Bundled GitNexus workflow skills (debugging, exploring, impact-analysis,\n\t// refactoring, pr-review, guide) — same SKILL.md content the Claude plugin\n\t// ships, adapted for hub-based indexing. Loaded on-demand by pi's skill system.\n\t// Skipped entirely when the user already has gitnexus skills (project or\n\t// global) so the startup screen doesn't report shadowed duplicates.\n\tpi.on(\"resources_discover\", (event) => {\n\t\tconst hasOwnGitnexusSkills =\n\t\t\texistsSync(join(event.cwd, \".agents\", \"skills\", \"gitnexus\")) ||\n\t\t\texistsSync(join(homedir(), \".agents\", \"skills\", \"gitnexus-debugging\"));\n\t\tif (hasOwnGitnexusSkills) return;\n\t\tconst skillsDir = resolveGitNexusSkillsDir();\n\t\tif (!skillsDir) return;\n\t\treturn { skillPaths: [skillsDir] };\n\t});\n\n\tpi.on(\"before_agent_start\", (event) => {\n\t\tif (!inventory) return;\n\t\treturn { systemPrompt: `${event.systemPrompt}\\n\\n${buildGitNexusPrompt(inventory, current)}` };\n\t});\n\n\tpi.registerCommand(\"gitnexus-login\", {\n\t\tdescription: \"Sign in to GitNexus (implementation of /login → GitNexus account; accepts a pasted token)\",\n\t\thandler: async (args, ctx) => {\n\t\t\tconst hubUrl = process.env.GITNEXUS_HUB_URL ?? DEFAULT_HUB_URL;\n\t\t\tconst pasted = args.trim();\n\n\t\t\t// Pasting a token stays supported for CI and headless installs.\n\t\t\tif (pasted) {\n\t\t\t\ttry {\n\t\t\t\t\tawait new GitNexusClient({ hubUrl, token: pasted }).callTool(\"list_repos\", {});\n\t\t\t\t\tconst path = saveGitNexusConfig({ hubUrl, token: pasted });\n\t\t\t\t\tawait connect();\n\t\t\t\t\tctx.ui.notify(`GitNexus token saved to ${path}. ${startupNotice ?? \"\"}`, \"info\");\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\tctx.ui.notify(`GitNexus login failed: ${message}`, \"error\");\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst deviceName = defaultDeviceName();\n\t\t\t\tconst fingerprint = deviceFingerprint();\n\t\t\t\tconst start = await startDeviceAuth(hubUrl, { deviceName, deviceFingerprint: fingerprint });\n\t\t\t\tconst link = start.verificationUrlComplete ?? start.verificationUrl;\n\t\t\t\t// The link carries the code, so an opened browser needs no typing.\n\t\t\t\t// Print it too: the launcher is best effort and can be headless.\n\t\t\t\tctx.ui.notify(`Code ${start.userCode} — opening ${link}`, \"info\");\n\t\t\t\topenBrowser(link);\n\n\t\t\t\tconst result = await pollDeviceAuth(hubUrl, start);\n\t\t\t\tconst config = {\n\t\t\t\t\thubUrl: result.hubUrl,\n\t\t\t\t\ttoken: result.token,\n\t\t\t\t\tdeviceFingerprint: fingerprint,\n\t\t\t\t\tdeviceName,\n\t\t\t\t};\n\t\t\t\tawait new GitNexusClient(config).callTool(\"list_repos\", {});\n\t\t\t\tconst path = saveGitNexusConfig(config);\n\t\t\t\tawait connect();\n\t\t\t\tctx.ui.notify(`Signed in. Token saved to ${path}. ${startupNotice ?? \"\"}`, \"info\");\n\t\t\t} catch (error) {\n\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\tctx.ui.notify(`GitNexus login failed: ${message}`, \"error\");\n\t\t\t}\n\t\t},\n\t});\n\n\tpi.registerCommand(\"gitnexus-status\", {\n\t\tdescription: \"Show GitNexus connection status\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tconst config = resolveGitNexusConfig();\n\t\t\tif (config) gatewayPolicy = await refreshGatewayPolicy(config);\n\t\t\tupdateCreditStatus(ctx);\n\t\t\tif (!inventory) {\n\t\t\t\tconst message = loginRequired\n\t\t\t\t\t? \"GitNexus session expired. Run /login to sign in again.\"\n\t\t\t\t\t: (startupNotice ?? \"GitNexus: not signed in. Run /login.\");\n\t\t\t\tctx.ui.notify(message, \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst repoLine = current ? `current repo: ${current.name}` : \"current repo: not indexed\";\n\t\t\tconst creditLine = formatCreditBalance(gatewayPolicy);\n\t\t\tctx.ui.notify(\n\t\t\t\t`GitNexus: connected — ${inventory.repos.length} repos, ${repoLine}${creditLine ? `, ${creditLine}` : \"\"}`,\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t},\n\t});\n}\n"]}
@@ -151,10 +151,16 @@ export default function gitnexusExtension(pi) {
151
151
  const pasted = args.trim();
152
152
  // Pasting a token stays supported for CI and headless installs.
153
153
  if (pasted) {
154
- await new GitNexusClient({ hubUrl, token: pasted }).callTool("list_repos", {});
155
- const path = saveGitNexusConfig({ hubUrl, token: pasted });
156
- await connect();
157
- ctx.ui.notify(`GitNexus token saved to ${path}. ${startupNotice ?? ""}`, "info");
154
+ try {
155
+ await new GitNexusClient({ hubUrl, token: pasted }).callTool("list_repos", {});
156
+ const path = saveGitNexusConfig({ hubUrl, token: pasted });
157
+ await connect();
158
+ ctx.ui.notify(`GitNexus token saved to ${path}. ${startupNotice ?? ""}`, "info");
159
+ }
160
+ catch (error) {
161
+ const message = error instanceof Error ? error.message : String(error);
162
+ ctx.ui.notify(`GitNexus login failed: ${message}`, "error");
163
+ }
158
164
  return;
159
165
  }
160
166
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/extensions/gitnexus/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { ExtensionAPI } from \"../../core/extensions/types.ts\";\nimport { graphRailStore } from \"../../core/graph-rail-store.ts\";\nimport { openBrowser } from \"../../utils/open-browser.ts\";\nimport { registerAugmentedGrep } from \"./augment.ts\";\nimport { registerChangeTracking } from \"./changes.ts\";\nimport { GitNexusClient, GitNexusClientError } from \"./client.ts\";\nimport { DEFAULT_HUB_URL, resolveGitNexusConfig, saveGitNexusConfig } from \"./config.ts\";\nimport { defaultDeviceName, deviceFingerprint, pollDeviceAuth, startDeviceAuth } from \"./device-auth.ts\";\nimport { formatCreditBalance, readGatewayCache, refreshGatewayPolicy, registerAutoProvider } from \"./gateway.ts\";\nimport { spawnObservatoryFetch } from \"./observatory.ts\";\nimport { buildGitNexusPrompt } from \"./prompt.ts\";\nimport { withGitNexusRenderer } from \"./render.ts\";\nimport {\n\tgitRemoteSlug,\n\ttype IndexedRepo,\n\tmatchCurrentRepo,\n\tparseRepoInventory,\n\ttype RepoInventory,\n} from \"./resolve.ts\";\nimport { createGitNexusTools } from \"./tools.ts\";\n\n/** Resolve workflow skills in source mode and bundled dist/cli.js mode. */\nexport function resolveGitNexusSkillsDir(\n\tmoduleUrl: string = import.meta.url,\n\tpathExists: (path: string) => boolean = existsSync,\n): string | undefined {\n\tconst candidates = [\n\t\tfileURLToPath(new URL(\"./skills/\", moduleUrl)),\n\t\tfileURLToPath(new URL(\"./extensions/gitnexus/skills/\", moduleUrl)),\n\t];\n\treturn candidates.find(pathExists);\n}\n\n/**\n * GitNexus built-in extension: connects nexus-cli to the GitNexus hub.\n *\n * - session_start: resolve config + repo inventory, register the six graph\n * tools and the augmented grep. No token / hub down / repo unindexed all\n * degrade to a plain coding agent (graph is additive, never load-bearing).\n * - before_agent_start: append the GitNexus guidance block (ported from the\n * CLAUDE.md generator) to the system prompt.\n */\nexport default function gitnexusExtension(pi: ExtensionAPI): void {\n\tlet inventory: RepoInventory | undefined;\n\tlet current: IndexedRepo | undefined;\n\tlet activeClient: GitNexusClient | undefined;\n\tlet toolsRegistered = false;\n\tlet startupNotice: string | undefined;\n\tlet loginRequired = false;\n\n\tconst cwd = process.cwd();\n\n\t// Auto mode must also register at factory time: the initial model is\n\t// resolved before session_start fires, and a provider that appears after\n\t// that is invisible to it. Reads the policy cache the startup gate wrote.\n\tlet gatewayPolicy = readGatewayCache();\n\tregisterAutoProvider(pi, resolveGitNexusConfig(), gatewayPolicy);\n\n\tfunction updateCreditStatus(ctx: { ui: { setStatus(key: string, text: string | undefined): void } }): void {\n\t\tctx.ui.setStatus(\"nexus-credits\", formatCreditBalance(gatewayPolicy));\n\t}\n\n\t// Built-in tool overrides must happen at factory time (like the bash-spawn-hook\n\t// example) — session_start registrations only add NEW names reliably. The wrapper\n\t// behaves as plain grep until connect() provides a client + current repo.\n\tregisterAugmentedGrep(\n\t\tpi,\n\t\tcwd,\n\t\t() => activeClient,\n\t\t() => current?.name,\n\t);\n\tregisterChangeTracking(pi, cwd);\n\n\tasync function connect(): Promise<void> {\n\t\tinventory = undefined;\n\t\tcurrent = undefined;\n\t\tactiveClient = undefined;\n\t\tstartupNotice = undefined;\n\t\tloginRequired = false;\n\t\tconst config = resolveGitNexusConfig();\n\t\tif (!config) return;\n\t\tconst client = new GitNexusClient(config);\n\t\ttry {\n\t\t\tconst listResult = await client.callTool(\"list_repos\", {});\n\t\t\tinventory = parseRepoInventory(GitNexusClient.text(listResult));\n\t\t} catch (error) {\n\t\t\tconst reason = error instanceof GitNexusClientError ? error.message : String(error);\n\t\t\tif (error instanceof GitNexusClientError && error.kind === \"auth\") {\n\t\t\t\tloginRequired = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstartupNotice = `GitNexus unavailable: ${reason}`;\n\t\t\treturn;\n\t\t}\n\n\t\tcurrent = matchCurrentRepo(inventory, gitRemoteSlug(cwd));\n\t\tactiveClient = client;\n\t\tgraphRailStore.setRepoList(inventory.repos.map((repo) => ({ name: repo.name, symbols: repo.symbols })));\n\t\t// Observatory (community map) rides a detached subprocess + temp-file\n\t\t// handoff — in-process hub calls made after startup can starve in some\n\t\t// environments, a fresh process never does. The UI picks the file up\n\t\t// lazily via tryLoadObservatoryCache.\n\t\tif (current) spawnObservatoryFetch(config, current.name);\n\t\tgraphRailStore.setRepo(\n\t\t\tcurrent\n\t\t\t\t? {\n\t\t\t\t\t\tname: current.name,\n\t\t\t\t\t\tsymbols: current.symbols,\n\t\t\t\t\t\trelationships: current.relationships,\n\t\t\t\t\t\tindexedAt: current.indexedAt,\n\t\t\t\t\t\trepoCount: inventory.repos.length,\n\t\t\t\t\t}\n\t\t\t\t: {\n\t\t\t\t\t\tname: \"\",\n\t\t\t\t\t\tsymbols: \"0\",\n\t\t\t\t\t\trelationships: \"0\",\n\t\t\t\t\t\tindexedAt: \"\",\n\t\t\t\t\t\trepoCount: inventory.repos.length,\n\t\t\t\t\t},\n\t\t\tcurrent !== undefined,\n\t\t);\n\t\tif (!toolsRegistered) {\n\t\t\tconst ctx = { client, currentRepo: () => current?.name, cwd };\n\t\t\tfor (const tool of createGitNexusTools(ctx)) pi.registerTool(withGitNexusRenderer(tool));\n\t\t\ttoolsRegistered = true;\n\t\t}\n\n\t\tstartupNotice = current\n\t\t\t? `GitNexus: connected — ${current.name} (${current.symbols} symbols, indexed ${current.indexedAt}), ${inventory.repos.length} repos available.`\n\t\t\t: `GitNexus: connected — current directory is not indexed; ${inventory.repos.length} indexed repos remain queryable. Index this repo from the Hub dashboard to unlock local graph context.`;\n\t}\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tawait connect();\n\t\tupdateCreditStatus(ctx);\n\t\tif (startupNotice) ctx.ui.notify(startupNotice, inventory ? \"info\" : \"warning\");\n\t});\n\n\tpi.on(\"agent_settled\", async (_event, ctx) => {\n\t\tif (!gatewayPolicy || ctx.model?.provider !== \"gitnexus\") return;\n\t\tconst config = resolveGitNexusConfig();\n\t\tif (config) gatewayPolicy = await refreshGatewayPolicy(config);\n\t\tupdateCreditStatus(ctx);\n\t});\n\n\t// Bundled GitNexus workflow skills (debugging, exploring, impact-analysis,\n\t// refactoring, pr-review, guide) — same SKILL.md content the Claude plugin\n\t// ships, adapted for hub-based indexing. Loaded on-demand by pi's skill system.\n\t// Skipped entirely when the user already has gitnexus skills (project or\n\t// global) so the startup screen doesn't report shadowed duplicates.\n\tpi.on(\"resources_discover\", (event) => {\n\t\tconst hasOwnGitnexusSkills =\n\t\t\texistsSync(join(event.cwd, \".agents\", \"skills\", \"gitnexus\")) ||\n\t\t\texistsSync(join(homedir(), \".agents\", \"skills\", \"gitnexus-debugging\"));\n\t\tif (hasOwnGitnexusSkills) return;\n\t\tconst skillsDir = resolveGitNexusSkillsDir();\n\t\tif (!skillsDir) return;\n\t\treturn { skillPaths: [skillsDir] };\n\t});\n\n\tpi.on(\"before_agent_start\", (event) => {\n\t\tif (!inventory) return;\n\t\treturn { systemPrompt: `${event.systemPrompt}\\n\\n${buildGitNexusPrompt(inventory, current)}` };\n\t});\n\n\tpi.registerCommand(\"gitnexus-login\", {\n\t\tdescription: \"Sign in to GitNexus (implementation of /login → GitNexus account; accepts a pasted token)\",\n\t\thandler: async (args, ctx) => {\n\t\t\tconst hubUrl = process.env.GITNEXUS_HUB_URL ?? DEFAULT_HUB_URL;\n\t\t\tconst pasted = args.trim();\n\n\t\t\t// Pasting a token stays supported for CI and headless installs.\n\t\t\tif (pasted) {\n\t\t\t\tawait new GitNexusClient({ hubUrl, token: pasted }).callTool(\"list_repos\", {});\n\t\t\t\tconst path = saveGitNexusConfig({ hubUrl, token: pasted });\n\t\t\t\tawait connect();\n\t\t\t\tctx.ui.notify(`GitNexus token saved to ${path}. ${startupNotice ?? \"\"}`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst deviceName = defaultDeviceName();\n\t\t\t\tconst fingerprint = deviceFingerprint();\n\t\t\t\tconst start = await startDeviceAuth(hubUrl, { deviceName, deviceFingerprint: fingerprint });\n\t\t\t\tconst link = start.verificationUrlComplete ?? start.verificationUrl;\n\t\t\t\t// The link carries the code, so an opened browser needs no typing.\n\t\t\t\t// Print it too: the launcher is best effort and can be headless.\n\t\t\t\tctx.ui.notify(`Code ${start.userCode} — opening ${link}`, \"info\");\n\t\t\t\topenBrowser(link);\n\n\t\t\t\tconst result = await pollDeviceAuth(hubUrl, start);\n\t\t\t\tconst config = {\n\t\t\t\t\thubUrl: result.hubUrl,\n\t\t\t\t\ttoken: result.token,\n\t\t\t\t\tdeviceFingerprint: fingerprint,\n\t\t\t\t\tdeviceName,\n\t\t\t\t};\n\t\t\t\tawait new GitNexusClient(config).callTool(\"list_repos\", {});\n\t\t\t\tconst path = saveGitNexusConfig(config);\n\t\t\t\tawait connect();\n\t\t\t\tctx.ui.notify(`Signed in. Token saved to ${path}. ${startupNotice ?? \"\"}`, \"info\");\n\t\t\t} catch (error) {\n\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\tctx.ui.notify(`GitNexus login failed: ${message}`, \"error\");\n\t\t\t}\n\t\t},\n\t});\n\n\tpi.registerCommand(\"gitnexus-status\", {\n\t\tdescription: \"Show GitNexus connection status\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tconst config = resolveGitNexusConfig();\n\t\t\tif (config) gatewayPolicy = await refreshGatewayPolicy(config);\n\t\t\tupdateCreditStatus(ctx);\n\t\t\tif (!inventory) {\n\t\t\t\tconst message = loginRequired\n\t\t\t\t\t? \"GitNexus session expired. Run /login to sign in again.\"\n\t\t\t\t\t: (startupNotice ?? \"GitNexus: not signed in. Run /login.\");\n\t\t\t\tctx.ui.notify(message, \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst repoLine = current ? `current repo: ${current.name}` : \"current repo: not indexed\";\n\t\t\tconst creditLine = formatCreditBalance(gatewayPolicy);\n\t\t\tctx.ui.notify(\n\t\t\t\t`GitNexus: connected — ${inventory.repos.length} repos, ${repoLine}${creditLine ? `, ${creditLine}` : \"\"}`,\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t},\n\t});\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/extensions/gitnexus/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { ExtensionAPI } from \"../../core/extensions/types.ts\";\nimport { graphRailStore } from \"../../core/graph-rail-store.ts\";\nimport { openBrowser } from \"../../utils/open-browser.ts\";\nimport { registerAugmentedGrep } from \"./augment.ts\";\nimport { registerChangeTracking } from \"./changes.ts\";\nimport { GitNexusClient, GitNexusClientError } from \"./client.ts\";\nimport { DEFAULT_HUB_URL, resolveGitNexusConfig, saveGitNexusConfig } from \"./config.ts\";\nimport { defaultDeviceName, deviceFingerprint, pollDeviceAuth, startDeviceAuth } from \"./device-auth.ts\";\nimport { formatCreditBalance, readGatewayCache, refreshGatewayPolicy, registerAutoProvider } from \"./gateway.ts\";\nimport { spawnObservatoryFetch } from \"./observatory.ts\";\nimport { buildGitNexusPrompt } from \"./prompt.ts\";\nimport { withGitNexusRenderer } from \"./render.ts\";\nimport {\n\tgitRemoteSlug,\n\ttype IndexedRepo,\n\tmatchCurrentRepo,\n\tparseRepoInventory,\n\ttype RepoInventory,\n} from \"./resolve.ts\";\nimport { createGitNexusTools } from \"./tools.ts\";\n\n/** Resolve workflow skills in source mode and bundled dist/cli.js mode. */\nexport function resolveGitNexusSkillsDir(\n\tmoduleUrl: string = import.meta.url,\n\tpathExists: (path: string) => boolean = existsSync,\n): string | undefined {\n\tconst candidates = [\n\t\tfileURLToPath(new URL(\"./skills/\", moduleUrl)),\n\t\tfileURLToPath(new URL(\"./extensions/gitnexus/skills/\", moduleUrl)),\n\t];\n\treturn candidates.find(pathExists);\n}\n\n/**\n * GitNexus built-in extension: connects nexus-cli to the GitNexus hub.\n *\n * - session_start: resolve config + repo inventory, register the six graph\n * tools and the augmented grep. No token / hub down / repo unindexed all\n * degrade to a plain coding agent (graph is additive, never load-bearing).\n * - before_agent_start: append the GitNexus guidance block (ported from the\n * CLAUDE.md generator) to the system prompt.\n */\nexport default function gitnexusExtension(pi: ExtensionAPI): void {\n\tlet inventory: RepoInventory | undefined;\n\tlet current: IndexedRepo | undefined;\n\tlet activeClient: GitNexusClient | undefined;\n\tlet toolsRegistered = false;\n\tlet startupNotice: string | undefined;\n\tlet loginRequired = false;\n\n\tconst cwd = process.cwd();\n\n\t// Auto mode must also register at factory time: the initial model is\n\t// resolved before session_start fires, and a provider that appears after\n\t// that is invisible to it. Reads the policy cache the startup gate wrote.\n\tlet gatewayPolicy = readGatewayCache();\n\tregisterAutoProvider(pi, resolveGitNexusConfig(), gatewayPolicy);\n\n\tfunction updateCreditStatus(ctx: { ui: { setStatus(key: string, text: string | undefined): void } }): void {\n\t\tctx.ui.setStatus(\"nexus-credits\", formatCreditBalance(gatewayPolicy));\n\t}\n\n\t// Built-in tool overrides must happen at factory time (like the bash-spawn-hook\n\t// example) — session_start registrations only add NEW names reliably. The wrapper\n\t// behaves as plain grep until connect() provides a client + current repo.\n\tregisterAugmentedGrep(\n\t\tpi,\n\t\tcwd,\n\t\t() => activeClient,\n\t\t() => current?.name,\n\t);\n\tregisterChangeTracking(pi, cwd);\n\n\tasync function connect(): Promise<void> {\n\t\tinventory = undefined;\n\t\tcurrent = undefined;\n\t\tactiveClient = undefined;\n\t\tstartupNotice = undefined;\n\t\tloginRequired = false;\n\t\tconst config = resolveGitNexusConfig();\n\t\tif (!config) return;\n\t\tconst client = new GitNexusClient(config);\n\t\ttry {\n\t\t\tconst listResult = await client.callTool(\"list_repos\", {});\n\t\t\tinventory = parseRepoInventory(GitNexusClient.text(listResult));\n\t\t} catch (error) {\n\t\t\tconst reason = error instanceof GitNexusClientError ? error.message : String(error);\n\t\t\tif (error instanceof GitNexusClientError && error.kind === \"auth\") {\n\t\t\t\tloginRequired = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tstartupNotice = `GitNexus unavailable: ${reason}`;\n\t\t\treturn;\n\t\t}\n\n\t\tcurrent = matchCurrentRepo(inventory, gitRemoteSlug(cwd));\n\t\tactiveClient = client;\n\t\tgraphRailStore.setRepoList(inventory.repos.map((repo) => ({ name: repo.name, symbols: repo.symbols })));\n\t\t// Observatory (community map) rides a detached subprocess + temp-file\n\t\t// handoff — in-process hub calls made after startup can starve in some\n\t\t// environments, a fresh process never does. The UI picks the file up\n\t\t// lazily via tryLoadObservatoryCache.\n\t\tif (current) spawnObservatoryFetch(config, current.name);\n\t\tgraphRailStore.setRepo(\n\t\t\tcurrent\n\t\t\t\t? {\n\t\t\t\t\t\tname: current.name,\n\t\t\t\t\t\tsymbols: current.symbols,\n\t\t\t\t\t\trelationships: current.relationships,\n\t\t\t\t\t\tindexedAt: current.indexedAt,\n\t\t\t\t\t\trepoCount: inventory.repos.length,\n\t\t\t\t\t}\n\t\t\t\t: {\n\t\t\t\t\t\tname: \"\",\n\t\t\t\t\t\tsymbols: \"0\",\n\t\t\t\t\t\trelationships: \"0\",\n\t\t\t\t\t\tindexedAt: \"\",\n\t\t\t\t\t\trepoCount: inventory.repos.length,\n\t\t\t\t\t},\n\t\t\tcurrent !== undefined,\n\t\t);\n\t\tif (!toolsRegistered) {\n\t\t\tconst ctx = { client, currentRepo: () => current?.name, cwd };\n\t\t\tfor (const tool of createGitNexusTools(ctx)) pi.registerTool(withGitNexusRenderer(tool));\n\t\t\ttoolsRegistered = true;\n\t\t}\n\n\t\tstartupNotice = current\n\t\t\t? `GitNexus: connected — ${current.name} (${current.symbols} symbols, indexed ${current.indexedAt}), ${inventory.repos.length} repos available.`\n\t\t\t: `GitNexus: connected — current directory is not indexed; ${inventory.repos.length} indexed repos remain queryable. Index this repo from the Hub dashboard to unlock local graph context.`;\n\t}\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\tawait connect();\n\t\tupdateCreditStatus(ctx);\n\t\tif (startupNotice) ctx.ui.notify(startupNotice, inventory ? \"info\" : \"warning\");\n\t});\n\n\tpi.on(\"agent_settled\", async (_event, ctx) => {\n\t\tif (!gatewayPolicy || ctx.model?.provider !== \"gitnexus\") return;\n\t\tconst config = resolveGitNexusConfig();\n\t\tif (config) gatewayPolicy = await refreshGatewayPolicy(config);\n\t\tupdateCreditStatus(ctx);\n\t});\n\n\t// Bundled GitNexus workflow skills (debugging, exploring, impact-analysis,\n\t// refactoring, pr-review, guide) — same SKILL.md content the Claude plugin\n\t// ships, adapted for hub-based indexing. Loaded on-demand by pi's skill system.\n\t// Skipped entirely when the user already has gitnexus skills (project or\n\t// global) so the startup screen doesn't report shadowed duplicates.\n\tpi.on(\"resources_discover\", (event) => {\n\t\tconst hasOwnGitnexusSkills =\n\t\t\texistsSync(join(event.cwd, \".agents\", \"skills\", \"gitnexus\")) ||\n\t\t\texistsSync(join(homedir(), \".agents\", \"skills\", \"gitnexus-debugging\"));\n\t\tif (hasOwnGitnexusSkills) return;\n\t\tconst skillsDir = resolveGitNexusSkillsDir();\n\t\tif (!skillsDir) return;\n\t\treturn { skillPaths: [skillsDir] };\n\t});\n\n\tpi.on(\"before_agent_start\", (event) => {\n\t\tif (!inventory) return;\n\t\treturn { systemPrompt: `${event.systemPrompt}\\n\\n${buildGitNexusPrompt(inventory, current)}` };\n\t});\n\n\tpi.registerCommand(\"gitnexus-login\", {\n\t\tdescription: \"Sign in to GitNexus (implementation of /login → GitNexus account; accepts a pasted token)\",\n\t\thandler: async (args, ctx) => {\n\t\t\tconst hubUrl = process.env.GITNEXUS_HUB_URL ?? DEFAULT_HUB_URL;\n\t\t\tconst pasted = args.trim();\n\n\t\t\t// Pasting a token stays supported for CI and headless installs.\n\t\t\tif (pasted) {\n\t\t\t\ttry {\n\t\t\t\t\tawait new GitNexusClient({ hubUrl, token: pasted }).callTool(\"list_repos\", {});\n\t\t\t\t\tconst path = saveGitNexusConfig({ hubUrl, token: pasted });\n\t\t\t\t\tawait connect();\n\t\t\t\t\tctx.ui.notify(`GitNexus token saved to ${path}. ${startupNotice ?? \"\"}`, \"info\");\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\tctx.ui.notify(`GitNexus login failed: ${message}`, \"error\");\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst deviceName = defaultDeviceName();\n\t\t\t\tconst fingerprint = deviceFingerprint();\n\t\t\t\tconst start = await startDeviceAuth(hubUrl, { deviceName, deviceFingerprint: fingerprint });\n\t\t\t\tconst link = start.verificationUrlComplete ?? start.verificationUrl;\n\t\t\t\t// The link carries the code, so an opened browser needs no typing.\n\t\t\t\t// Print it too: the launcher is best effort and can be headless.\n\t\t\t\tctx.ui.notify(`Code ${start.userCode} — opening ${link}`, \"info\");\n\t\t\t\topenBrowser(link);\n\n\t\t\t\tconst result = await pollDeviceAuth(hubUrl, start);\n\t\t\t\tconst config = {\n\t\t\t\t\thubUrl: result.hubUrl,\n\t\t\t\t\ttoken: result.token,\n\t\t\t\t\tdeviceFingerprint: fingerprint,\n\t\t\t\t\tdeviceName,\n\t\t\t\t};\n\t\t\t\tawait new GitNexusClient(config).callTool(\"list_repos\", {});\n\t\t\t\tconst path = saveGitNexusConfig(config);\n\t\t\t\tawait connect();\n\t\t\t\tctx.ui.notify(`Signed in. Token saved to ${path}. ${startupNotice ?? \"\"}`, \"info\");\n\t\t\t} catch (error) {\n\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\tctx.ui.notify(`GitNexus login failed: ${message}`, \"error\");\n\t\t\t}\n\t\t},\n\t});\n\n\tpi.registerCommand(\"gitnexus-status\", {\n\t\tdescription: \"Show GitNexus connection status\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tconst config = resolveGitNexusConfig();\n\t\t\tif (config) gatewayPolicy = await refreshGatewayPolicy(config);\n\t\t\tupdateCreditStatus(ctx);\n\t\t\tif (!inventory) {\n\t\t\t\tconst message = loginRequired\n\t\t\t\t\t? \"GitNexus session expired. Run /login to sign in again.\"\n\t\t\t\t\t: (startupNotice ?? \"GitNexus: not signed in. Run /login.\");\n\t\t\t\tctx.ui.notify(message, \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst repoLine = current ? `current repo: ${current.name}` : \"current repo: not indexed\";\n\t\t\tconst creditLine = formatCreditBalance(gatewayPolicy);\n\t\t\tctx.ui.notify(\n\t\t\t\t`GitNexus: connected — ${inventory.repos.length} repos, ${repoLine}${creditLine ? `, ${creditLine}` : \"\"}`,\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t},\n\t});\n}\n"]}
@@ -1,9 +1,4 @@
1
- /**
2
- * Registry fallback for the default web access extension. Only used when no
3
- * vendored copy is available (e.g. a source checkout before `npm install`):
4
- * the published package must never install this at runtime, both because the
5
- * install is slow/flaky and because the package name is not ours to surface.
6
- */
1
+ /** Registry fallback used only when no source or bundled copy is available. */
7
2
  export declare const WEB_ACCESS_NPM_FALLBACK_SOURCE = "npm:pi-web-access@0.22.0";
8
3
  export declare const WEB_ACCESS_EXTENSION_SOURCE: string;
9
4
  export declare const WEB_ACCESS_CONFIG_FILENAME = "web-search.json";
@@ -1 +1 @@
1
- {"version":3,"file":"web-access.d.ts","sourceRoot":"","sources":["../../src/extensions/web-access.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,6BAA6B,CAAC;AA4BzE,eAAO,MAAM,2BAA2B,QAAoC,CAAC;AAC7E,eAAO,MAAM,0BAA0B,oBAAoB,CAAC;AAgC5D;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAmE/F;AAED,wBAAgB,+BAA+B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAQpF;AAED,0EAA0E;AAC1E,wBAAgB,6BAA6B,CAC5C,sBAAsB,EAAE,MAAM,EAAE,GAAG,SAAS,EAC5C,oBAAoB,EAAE,OAAO,GAC3B,MAAM,EAAE,GAAG,SAAS,CAStB","sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { getPackageDir, setAppEnv } from \"../config.ts\";\n\n/**\n * Registry fallback for the default web access extension. Only used when no\n * vendored copy is available (e.g. a source checkout before `npm install`):\n * the published package must never install this at runtime, both because the\n * install is slow/flaky and because the package name is not ours to surface.\n */\nexport const WEB_ACCESS_NPM_FALLBACK_SOURCE = \"npm:pi-web-access@0.22.0\";\n\nfunction resolveWebAccessExtensionSource(): string {\n\tconst packageDir = getPackageDir();\n\t// Source checkout: the extension is a root devDependency, so its TypeScript\n\t// loads directly (tsconfig paths then map its pi imports to the workspace\n\t// sources) instead of npm-installing at runtime or using a stale bundle.\n\tconst sourceCandidates = [\n\t\tjoin(packageDir, \"node_modules\", \"pi-web-access\", \"index.ts\"),\n\t\tjoin(packageDir, \"..\", \"..\", \"node_modules\", \"pi-web-access\", \"index.ts\"),\n\t];\n\t// Published npm build: bundled by scripts/bundle-coding-agent.mjs. Compiled\n\t// binary: assets sit next to the executable, which is what getPackageDir()\n\t// points at there.\n\tconst bundledCandidates = [\n\t\tjoin(packageDir, \"dist\", \"vendor\", \"web-access.js\"),\n\t\tjoin(packageDir, \"vendor\", \"web-access.js\"),\n\t];\n\tconst isTypeScriptSourceRuntime = import.meta.url.endsWith(\".ts\");\n\tconst candidates = isTypeScriptSourceRuntime\n\t\t? [...sourceCandidates, ...bundledCandidates]\n\t\t: [...bundledCandidates, ...sourceCandidates];\n\tfor (const candidate of candidates) {\n\t\tif (existsSync(candidate)) return candidate;\n\t}\n\treturn WEB_ACCESS_NPM_FALLBACK_SOURCE;\n}\n\nexport const WEB_ACCESS_EXTENSION_SOURCE = resolveWebAccessExtensionSource();\nexport const WEB_ACCESS_CONFIG_FILENAME = \"web-search.json\";\n\nconst DEFAULT_WORKFLOW = \"none\";\nconst DEFAULT_WEB_SEARCH_TOOL_NAME = \"websearch\";\nconst DEFAULT_SEARCH_ROUTING = {\n\tproviders: [\n\t\t\"searxng\",\n\t\t\"openai\",\n\t\t\"exa\",\n\t\t\"brave\",\n\t\t\"parallel\",\n\t\t\"tinyfish\",\n\t\t\"search1api\",\n\t\t\"searchinfinity\",\n\t\t\"querit\",\n\t\t\"tavily\",\n\t\t\"jina\",\n\t\t\"serpdive\",\n\t\t\"kagi\",\n\t\t\"bocha\",\n\t\t\"ollama\",\n\t\t\"perplexity\",\n\t\t\"gemini\",\n\t\t\"duckduckgo\",\n\t],\n\tfallbackOn: [\"quota\", \"transient\", \"network\", \"invalid-response\"],\n};\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Point Pi ecosystem extensions at Nexus's config directory and add Nexus-safe\n * defaults without replacing user-selected web access settings.\n */\nexport function prepareWebAccessConfig(agentDir: string): { changed: boolean; warning?: string } {\n\tsetAppEnv(\"CODING_AGENT_DIR\", agentDir);\n\tconst configPath = join(agentDir, WEB_ACCESS_CONFIG_FILENAME);\n\n\tlet config: Record<string, unknown> = {};\n\tif (existsSync(configPath)) {\n\t\ttry {\n\t\t\tconst parsed: unknown = JSON.parse(readFileSync(configPath, \"utf-8\"));\n\t\t\tif (!isRecord(parsed)) {\n\t\t\t\treturn { changed: false };\n\t\t\t}\n\t\t\tconfig = parsed;\n\t\t} catch {\n\t\t\t// pi-web-access reports malformed config with its own path-aware diagnostic.\n\t\t\treturn { changed: false };\n\t\t}\n\t}\n\n\tlet changed = false;\n\tif (config.workflow === undefined) {\n\t\tconfig.workflow = DEFAULT_WORKFLOW;\n\t\tchanged = true;\n\t}\n\n\tif (config.toolNames === undefined) {\n\t\tconfig.toolNames = { webSearch: DEFAULT_WEB_SEARCH_TOOL_NAME };\n\t\tchanged = true;\n\t} else if (isRecord(config.toolNames) && config.toolNames.webSearch === undefined) {\n\t\tconfig.toolNames.webSearch = DEFAULT_WEB_SEARCH_TOOL_NAME;\n\t\tchanged = true;\n\t}\n\n\tconst hasExplicitProvider = config.provider !== undefined || config.searchProvider !== undefined;\n\tif (!hasExplicitProvider && config.searchRouting === undefined) {\n\t\tconfig.searchRouting = DEFAULT_SEARCH_ROUTING;\n\t\tchanged = true;\n\t}\n\n\t// The extension's slash commands (/websearch, /curator, /search,\n\t// /google-account) are operator utilities around the agent-facing tools;\n\t// they don't belong in Nexus's command list. Default them off — a dev can\n\t// re-enable per command with `commands.<name>.enabled: true`.\n\tif (config.commands === undefined) {\n\t\tconfig.commands = {\n\t\t\twebsearch: { enabled: false },\n\t\t\tcurator: { enabled: false },\n\t\t\tsearch: { enabled: false },\n\t\t\t\"google-account\": { enabled: false },\n\t\t};\n\t\tchanged = true;\n\t}\n\n\tif (!changed) {\n\t\treturn { changed: false };\n\t}\n\n\ttry {\n\t\tmkdirSync(agentDir, { recursive: true });\n\t\twriteFileSync(configPath, `${JSON.stringify(config, null, 2)}\\n`, {\n\t\t\tencoding: \"utf-8\",\n\t\t\tmode: 0o600,\n\t\t});\n\t\treturn { changed: true };\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\treturn { changed: false, warning: `Could not write web access defaults to ${configPath}: ${message}` };\n\t}\n}\n\nexport function readConfiguredWebSearchToolName(agentDir: string): string | undefined {\n\ttry {\n\t\tconst parsed: unknown = JSON.parse(readFileSync(join(agentDir, WEB_ACCESS_CONFIG_FILENAME), \"utf-8\"));\n\t\tif (!isRecord(parsed) || !isRecord(parsed.toolNames)) return undefined;\n\t\treturn typeof parsed.toolNames.webSearch === \"string\" ? parsed.toolNames.webSearch : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** Omit Nexus's default package without suppressing explicit -e paths. */\nexport function withDefaultWebAccessExtension(\n\texplicitExtensionPaths: string[] | undefined,\n\tomitDefaultExtension: boolean,\n): string[] | undefined {\n\tif (omitDefaultExtension) {\n\t\treturn explicitExtensionPaths;\n\t}\n\n\treturn [\n\t\tWEB_ACCESS_EXTENSION_SOURCE,\n\t\t...(explicitExtensionPaths ?? []).filter((source) => source !== WEB_ACCESS_EXTENSION_SOURCE),\n\t];\n}\n"]}
1
+ {"version":3,"file":"web-access.d.ts","sourceRoot":"","sources":["../../src/extensions/web-access.ts"],"names":[],"mappings":"AAIA,+EAA+E;AAC/E,eAAO,MAAM,8BAA8B,6BAA6B,CAAC;AAsBzE,eAAO,MAAM,2BAA2B,QAAoC,CAAC;AAC7E,eAAO,MAAM,0BAA0B,oBAAoB,CAAC;AAgC5D;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAiE/F;AAED,wBAAgB,+BAA+B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAQpF;AAED,0EAA0E;AAC1E,wBAAgB,6BAA6B,CAC5C,sBAAsB,EAAE,MAAM,EAAE,GAAG,SAAS,EAC5C,oBAAoB,EAAE,OAAO,GAC3B,MAAM,EAAE,GAAG,SAAS,CAStB","sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { getPackageDir, setAppEnv } from \"../config.ts\";\n\n/** Registry fallback used only when no source or bundled copy is available. */\nexport const WEB_ACCESS_NPM_FALLBACK_SOURCE = \"npm:pi-web-access@0.22.0\";\n\nfunction resolveWebAccessExtensionSource(): string {\n\tconst packageDir = getPackageDir();\n\tconst sourceCandidates = [\n\t\tjoin(packageDir, \"node_modules\", \"pi-web-access\", \"index.ts\"),\n\t\tjoin(packageDir, \"..\", \"..\", \"node_modules\", \"pi-web-access\", \"index.ts\"),\n\t];\n\tconst bundledCandidates = [\n\t\tjoin(packageDir, \"dist\", \"vendor\", \"web-access.js\"),\n\t\tjoin(packageDir, \"vendor\", \"web-access.js\"),\n\t];\n\tconst isTypeScriptSourceRuntime = import.meta.url.endsWith(\".ts\");\n\tconst candidates = isTypeScriptSourceRuntime\n\t\t? [...sourceCandidates, ...bundledCandidates]\n\t\t: [...bundledCandidates, ...sourceCandidates];\n\tfor (const candidate of candidates) {\n\t\tif (existsSync(candidate)) return candidate;\n\t}\n\treturn WEB_ACCESS_NPM_FALLBACK_SOURCE;\n}\n\nexport const WEB_ACCESS_EXTENSION_SOURCE = resolveWebAccessExtensionSource();\nexport const WEB_ACCESS_CONFIG_FILENAME = \"web-search.json\";\n\nconst DEFAULT_WORKFLOW = \"none\";\nconst DEFAULT_WEB_SEARCH_TOOL_NAME = \"websearch\";\nconst DEFAULT_SEARCH_ROUTING = {\n\tproviders: [\n\t\t\"searxng\",\n\t\t\"openai\",\n\t\t\"exa\",\n\t\t\"brave\",\n\t\t\"parallel\",\n\t\t\"tinyfish\",\n\t\t\"search1api\",\n\t\t\"searchinfinity\",\n\t\t\"querit\",\n\t\t\"tavily\",\n\t\t\"jina\",\n\t\t\"serpdive\",\n\t\t\"kagi\",\n\t\t\"bocha\",\n\t\t\"ollama\",\n\t\t\"perplexity\",\n\t\t\"gemini\",\n\t\t\"duckduckgo\",\n\t],\n\tfallbackOn: [\"quota\", \"transient\", \"network\", \"invalid-response\"],\n};\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Point Pi ecosystem extensions at Nexus's config directory and add Nexus-safe\n * defaults without replacing user-selected web access settings.\n */\nexport function prepareWebAccessConfig(agentDir: string): { changed: boolean; warning?: string } {\n\tsetAppEnv(\"CODING_AGENT_DIR\", agentDir);\n\tconst configPath = join(agentDir, WEB_ACCESS_CONFIG_FILENAME);\n\n\tlet config: Record<string, unknown> = {};\n\tif (existsSync(configPath)) {\n\t\ttry {\n\t\t\tconst parsed: unknown = JSON.parse(readFileSync(configPath, \"utf-8\"));\n\t\t\tif (!isRecord(parsed)) {\n\t\t\t\treturn { changed: false };\n\t\t\t}\n\t\t\tconfig = parsed;\n\t\t} catch {\n\t\t\t// pi-web-access reports malformed config with its own path-aware diagnostic.\n\t\t\treturn { changed: false };\n\t\t}\n\t}\n\n\tlet changed = false;\n\tif (config.workflow === undefined) {\n\t\tconfig.workflow = DEFAULT_WORKFLOW;\n\t\tchanged = true;\n\t}\n\n\tif (config.toolNames === undefined) {\n\t\tconfig.toolNames = { webSearch: DEFAULT_WEB_SEARCH_TOOL_NAME };\n\t\tchanged = true;\n\t} else if (isRecord(config.toolNames) && config.toolNames.webSearch === undefined) {\n\t\tconfig.toolNames.webSearch = DEFAULT_WEB_SEARCH_TOOL_NAME;\n\t\tchanged = true;\n\t}\n\n\t// These slash commands are operator utilities around the agent-facing tools;\n\t// keep Nexus's command list minimal unless a user explicitly enables them.\n\tif (config.commands === undefined) {\n\t\tconfig.commands = {\n\t\t\twebsearch: { enabled: false },\n\t\t\tcurator: { enabled: false },\n\t\t\tsearch: { enabled: false },\n\t\t\t\"google-account\": { enabled: false },\n\t\t};\n\t\tchanged = true;\n\t}\n\n\tconst hasExplicitProvider = config.provider !== undefined || config.searchProvider !== undefined;\n\tif (!hasExplicitProvider && config.searchRouting === undefined) {\n\t\tconfig.searchRouting = DEFAULT_SEARCH_ROUTING;\n\t\tchanged = true;\n\t}\n\n\tif (!changed) {\n\t\treturn { changed: false };\n\t}\n\n\ttry {\n\t\tmkdirSync(agentDir, { recursive: true });\n\t\twriteFileSync(configPath, `${JSON.stringify(config, null, 2)}\\n`, {\n\t\t\tencoding: \"utf-8\",\n\t\t\tmode: 0o600,\n\t\t});\n\t\treturn { changed: true };\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\treturn { changed: false, warning: `Could not write web access defaults to ${configPath}: ${message}` };\n\t}\n}\n\nexport function readConfiguredWebSearchToolName(agentDir: string): string | undefined {\n\ttry {\n\t\tconst parsed: unknown = JSON.parse(readFileSync(join(agentDir, WEB_ACCESS_CONFIG_FILENAME), \"utf-8\"));\n\t\tif (!isRecord(parsed) || !isRecord(parsed.toolNames)) return undefined;\n\t\treturn typeof parsed.toolNames.webSearch === \"string\" ? parsed.toolNames.webSearch : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** Omit Nexus's default package without suppressing explicit -e paths. */\nexport function withDefaultWebAccessExtension(\n\texplicitExtensionPaths: string[] | undefined,\n\tomitDefaultExtension: boolean,\n): string[] | undefined {\n\tif (omitDefaultExtension) {\n\t\treturn explicitExtensionPaths;\n\t}\n\n\treturn [\n\t\tWEB_ACCESS_EXTENSION_SOURCE,\n\t\t...(explicitExtensionPaths ?? []).filter((source) => source !== WEB_ACCESS_EXTENSION_SOURCE),\n\t];\n}\n"]}