@link-assistant/agent 0.26.15 → 0.27.1

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 (65) hide show
  1. package/README.md +49 -7
  2. package/package.json +51 -42
  3. package/src/agent/agent.ts +12 -7
  4. package/src/auth/azure-login.ts +241 -0
  5. package/src/auth/plugins.ts +9 -0
  6. package/src/auth/provider-logins.ts +804 -0
  7. package/src/cli/argv.ts +1 -1
  8. package/src/cli/cmd/auth.ts +4 -0
  9. package/src/cli/cmd/models.ts +68 -13
  10. package/src/cli/cmd/run.ts +7 -1
  11. package/src/cli/continuous-mode.js +38 -12
  12. package/src/cli/event-handler.js +3 -1
  13. package/src/cli/model-config.js +27 -8
  14. package/src/cli/run-options.js +144 -78
  15. package/src/config/config.ts +306 -105
  16. package/src/config/defaults.ts +13 -5
  17. package/src/config/file-config.ts +16 -0
  18. package/src/file/ripgrep.ts +38 -5
  19. package/src/file/time.ts +86 -9
  20. package/src/index.js +42 -33
  21. package/src/mcp/index.ts +370 -106
  22. package/src/permission/index.ts +22 -0
  23. package/src/project/instance.ts +5 -1
  24. package/src/project/project.ts +35 -2
  25. package/src/provider/env-template.ts +115 -0
  26. package/src/provider/extra-providers.ts +347 -0
  27. package/src/provider/free-tier-error.ts +1 -1
  28. package/src/provider/kilo-deprecation.ts +30 -0
  29. package/src/provider/kilo.ts +44 -7
  30. package/src/provider/model-discovery.ts +690 -0
  31. package/src/provider/models.ts +11 -1
  32. package/src/provider/parity-loaders.ts +595 -0
  33. package/src/provider/provider.ts +309 -203
  34. package/src/provider/retry-fetch.ts +5 -0
  35. package/src/provider/transform.ts +168 -17
  36. package/src/session/compaction.ts +18 -2
  37. package/src/session/index.ts +30 -3
  38. package/src/session/message-v2.ts +318 -32
  39. package/src/session/processor.ts +274 -140
  40. package/src/session/prompt.ts +154 -128
  41. package/src/session/retry.ts +26 -13
  42. package/src/session/summary.ts +4 -1
  43. package/src/tool/bash.ts +232 -45
  44. package/src/tool/bash.txt +1 -1
  45. package/src/tool/codesearch.ts +15 -78
  46. package/src/tool/edit.ts +111 -30
  47. package/src/tool/glob.ts +2 -1
  48. package/src/tool/grep.ts +175 -57
  49. package/src/tool/ls.ts +2 -1
  50. package/src/tool/multiedit.ts +3 -1
  51. package/src/tool/read.ts +169 -41
  52. package/src/tool/task.ts +26 -18
  53. package/src/tool/tool.ts +4 -1
  54. package/src/tool/webfetch.ts +68 -56
  55. package/src/tool/webfetch.txt +4 -4
  56. package/src/tool/websearch.ts +20 -75
  57. package/src/tool/websearch.txt +2 -0
  58. package/src/tool/write.ts +23 -6
  59. package/src/util/bom.ts +32 -0
  60. package/src/util/exa-mcp.ts +82 -0
  61. package/src/util/legacy-error-names.ts +39 -0
  62. package/src/util/read-bounded.ts +32 -0
  63. package/src/util/sse-usage-extractor.ts +67 -13
  64. package/src/util/timeout.ts +3 -4
  65. package/src/util/verbose-fetch.ts +40 -1
package/README.md CHANGED
@@ -24,7 +24,7 @@ This is an MVP implementation of an OpenCode-compatible CLI agent, focused on ma
24
24
 
25
25
  - ✅ **JSON Input/Output**: Compatible with `opencode run --format json`
26
26
  - ✅ **Plain Text Input**: Also accepts plain text messages (auto-converted to JSON format)
27
- - ✅ **Flexible Model Selection**: Defaults to free Kilo Nemotron 3 Super (Kilo authentication recommended), supports [OpenCode Zen](https://opencode.ai/docs/zen/), [Formal AI](../docs/formal-ai.md), [Claude OAuth](../docs/claude-oauth.md), [Groq](../docs/groq.md), and [OpenRouter](../docs/openrouter.md) providers
27
+ - ✅ **Flexible Model Selection**: Defaults to free Kilo Nemotron 3 Ultra (Kilo authentication recommended), supports every [models.dev](https://models.dev) provider OpenCode supports ([full list](../docs/providers.md)), OpenCode's subscription logins, [gpt4free](../docs/gpt4free.md), Pollinations, local servers, [Formal AI](../docs/formal-ai.md) and [Claude OAuth](../docs/claude-oauth.md), with [live model discovery](../docs/model-discovery.md)
28
28
  - ✅ **No Restrictions**: Fully unrestricted file system and command execution access (no sandbox)
29
29
  - ✅ **Minimal Footprint**: Built with Bun.sh for maximum efficiency
30
30
  - ✅ **Full Tool Support**: 13 tools including websearch, codesearch, batch - all enabled by default
@@ -169,7 +169,7 @@ echo '{"message":"hi"}' | agent
169
169
  **With custom model:**
170
170
 
171
171
  ```bash
172
- echo "hi" | agent --model kilo/nemotron-3-super-free
172
+ echo "hi" | agent --model kilo/nemotron-3-ultra-free
173
173
  ```
174
174
 
175
175
  ### More Examples
@@ -190,11 +190,10 @@ echo '{"message":"run command","tools":[{"name":"bash","params":{"command":"ls -
190
190
  **Using different models:**
191
191
 
192
192
  ```bash
193
- # Default model (free Nemotron 3 Super)
193
+ # Default model (free Nemotron 3 Ultra)
194
194
  echo "hi" | agent
195
195
 
196
196
  # Built-in free models (authenticate with agent auth login or KILO_API_KEY)
197
- echo "hi" | agent --model kilo/nemotron-3-super-free
198
197
  echo "hi" | agent --model kilo/nemotron-3-ultra-free
199
198
  echo "hi" | agent --model kilo/nemotron-3.5-lightning-free
200
199
 
@@ -285,7 +284,7 @@ agent [options]
285
284
 
286
285
  Options:
287
286
  --model Model to use in format providerID/modelID
288
- Default: kilo/nemotron-3-super-free
287
+ Default: kilo/nemotron-3-ultra-free
289
288
  --json-standard JSON output format standard
290
289
  Choices: "opencode" (default), "claude" (experimental)
291
290
  --output-format Alias for JSON output format
@@ -311,6 +310,42 @@ Permission Options:
311
310
  invoked via batch.
312
311
  Env: LINK_ASSISTANT_AGENT_DISABLE_TOOLS
313
312
 
313
+ Compatibility Options (issue #335):
314
+ Each option restores behaviour from before the OpenCode stability sync, or
315
+ tunes a new limit. Every option also has a LINK_ASSISTANT_AGENT_* env var.
316
+ --bash-shell <auto|legacy|path> Shell for the bash tool. "auto" (default):
317
+ $SHELL if POSIX, else bash, else /bin/sh;
318
+ "legacy": /bin/sh as before.
319
+ Env: LINK_ASSISTANT_AGENT_BASH_SHELL
320
+ --legacy-tool-output Empty edit/write output, head-truncated bash
321
+ output, empty read past EOF (as before).
322
+ Env: LINK_ASSISTANT_AGENT_LEGACY_TOOL_OUTPUT
323
+ --legacy-edit-matching Loose fuzzy edit matching; an empty oldString
324
+ overwrites an existing file (as before).
325
+ Env: LINK_ASSISTANT_AGENT_LEGACY_EDIT_MATCHING
326
+ --glob-follow-symlinks Follow symlinked directories in glob/ls.
327
+ Env: LINK_ASSISTANT_AGENT_GLOB_FOLLOW_SYMLINKS
328
+ --no-grep-hidden Do not search hidden files in grep.
329
+ Env: LINK_ASSISTANT_AGENT_GREP_HIDDEN=false
330
+ --grep-timeout-ms <ms> Timeout of one grep search (default 30000,
331
+ 0 = none). Env: LINK_ASSISTANT_AGENT_GREP_TIMEOUT_MS
332
+ --read-max-lines <n> Max lines per read call (default 2000,
333
+ 0 = no cap). Env: LINK_ASSISTANT_AGENT_READ_MAX_LINES
334
+ --read-max-bytes <n> Max bytes per read call (default 51200,
335
+ 0 = no cap). Env: LINK_ASSISTANT_AGENT_READ_MAX_BYTES
336
+ --legacy-error-names Report ContextOverflowError as a
337
+ non-retryable APIError in JSON output.
338
+ Env: LINK_ASSISTANT_AGENT_LEGACY_ERROR_NAMES
339
+ --legacy-token-usage tokens.output includes reasoning tokens.
340
+ Env: LINK_ASSISTANT_AGENT_LEGACY_TOKEN_USAGE
341
+ --legacy-loop-exit End the turn on a "stop" finish even when
342
+ tools ran in that step.
343
+ Env: LINK_ASSISTANT_AGENT_LEGACY_LOOP_EXIT
344
+ --mcp-connect-timeout <ms> Default MCP connect/initialize timeout
345
+ (default 30000, 0 = none). Per server:
346
+ "connect_timeout" in the mcp config.
347
+ Env: LINK_ASSISTANT_AGENT_MCP_CONNECT_TIMEOUT
348
+
314
349
  Stdin Mode Options:
315
350
  -p, --prompt Direct prompt (bypasses stdin reading)
316
351
  --disable-stdin Disable stdin streaming (requires --prompt)
@@ -509,6 +544,13 @@ This will create a configuration file at `~/.config/link-assistant-agent/opencod
509
544
  }
510
545
  ```
511
546
 
547
+ **MCP timeouts:** each server entry accepts `timeout` (ms for listing tools,
548
+ default 5000; unchanged meaning) and `connect_timeout` (ms for starting the
549
+ server and finishing the `initialize` handshake, default
550
+ `--mcp-connect-timeout`, 30000, or `timeout` if larger; `0` disables the
551
+ bound). A server that does not connect in time is reported as `failed`
552
+ instead of blocking the session.
553
+
512
554
  **Available Playwright Tools:**
513
555
 
514
556
  Once configured, the agent gains access to 22+ browser automation tools:
@@ -735,7 +777,7 @@ This agent is **exclusively built for Bun** for the following reasons:
735
777
  4. **Minimalism**: Single runtime target keeps the codebase simple
736
778
  5. **Bun Ecosystem**: Leverages Bun-specific features and optimizations
737
779
 
738
- **Not supporting Node.js or Deno is intentional** to keep the project focused and minimal. If you need Node.js/Deno compatibility, consider using [OpenCode](https://github.com/sst/opencode) instead.
780
+ **Not supporting Node.js or Deno is intentional** to keep the project focused and minimal. If you need Node.js/Deno compatibility, consider using [OpenCode](https://github.com/anomalyco/opencode) instead.
739
781
 
740
782
  ## Project Structure
741
783
 
@@ -753,7 +795,7 @@ js/
753
795
 
754
796
  ## Architecture
755
797
 
756
- This agent-cli reproduces the core architecture of [OpenCode](https://github.com/sst/opencode)'s `run --format json` command:
798
+ This agent-cli reproduces the core architecture of [OpenCode](https://github.com/anomalyco/opencode)'s `run --format json` command:
757
799
 
758
800
  - **Streaming JSON Events**: Instead of single responses, outputs real-time event stream
759
801
  - **Event Types**: `tool_use`, `text`, `step_start`, `step_finish`, `error`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/agent",
3
- "version": "0.26.15",
3
+ "version": "0.27.1",
4
4
  "description": "A minimal, public domain AI CLI agent compatible with OpenCode's JSON interface. Bun-only runtime.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -51,73 +51,83 @@
51
51
  "LICENSE"
52
52
  ],
53
53
  "dependencies": {
54
- "@agentclientprotocol/sdk": "^1.8.0",
55
- "@ai-sdk/amazon-bedrock": "^5.0.113",
56
- "@ai-sdk/anthropic": "^4.0.78",
57
- "@ai-sdk/azure": "^4.0.99",
58
- "@ai-sdk/google": "^4.0.93",
59
- "@ai-sdk/google-vertex": "^5.0.109",
60
- "@ai-sdk/groq": "^4.0.59",
61
- "@ai-sdk/mcp": "^2.0.73",
62
- "@ai-sdk/mistral": "^4.0.62",
63
- "@ai-sdk/openai": "^4.0.91",
64
- "@ai-sdk/openai-compatible": "^3.0.67",
65
- "@ai-sdk/provider": "^4.0.26",
66
- "@ai-sdk/xai": "^5.0.20",
54
+ "@agentclientprotocol/sdk": "^2.4.0",
55
+ "@ai-sdk/alibaba": "^2.0.81",
56
+ "@ai-sdk/amazon-bedrock": "^5.0.131",
57
+ "@ai-sdk/anthropic": "^4.0.95",
58
+ "@ai-sdk/azure": "^4.0.115",
59
+ "@ai-sdk/cerebras": "^3.0.83",
60
+ "@ai-sdk/cohere": "^4.0.74",
61
+ "@ai-sdk/deepinfra": "^3.0.83",
62
+ "@ai-sdk/deepseek": "^3.0.78",
63
+ "@ai-sdk/fireworks": "^3.0.86",
64
+ "@ai-sdk/gateway": "^4.0.134",
65
+ "@ai-sdk/google": "^4.0.110",
66
+ "@ai-sdk/google-vertex": "^5.0.130",
67
+ "@ai-sdk/groq": "^4.0.75",
68
+ "@ai-sdk/mcp": "^2.0.89",
69
+ "@ai-sdk/mistral": "^4.0.78",
70
+ "@ai-sdk/openai": "^4.0.107",
71
+ "@ai-sdk/openai-compatible": "^3.0.83",
72
+ "@ai-sdk/perplexity": "^5.0.25",
73
+ "@ai-sdk/provider": "^4.0.35",
74
+ "@ai-sdk/togetherai": "^3.0.84",
75
+ "@ai-sdk/xai": "^5.0.37",
67
76
  "@clack/prompts": "^1.8.1",
68
- "@hono/standard-validator": "^0.4.0",
77
+ "@hono/standard-validator": "^0.4.1",
69
78
  "@hono/zod-validator": "^0.9.1",
70
- "@modelcontextprotocol/sdk": "^1.32.1",
79
+ "@modelcontextprotocol/sdk": "^1.32.2",
71
80
  "@octokit/graphql": "^9.0.5",
72
81
  "@octokit/rest": "^22.0.1",
73
82
  "@openauthjs/openauth": "^0.4.3",
74
- "@opencode-ai/sdk": "^1.19.0",
83
+ "@opencode-ai/sdk": "^1.19.5",
75
84
  "@openrouter/ai-sdk-provider": "^3.1.0",
76
- "@opentui/core": "0.6.1",
77
- "@opentui/solid": "0.6.1",
85
+ "@opentui/core": "^0.8.1",
86
+ "@opentui/solid": "^0.8.1",
78
87
  "@parcel/watcher": "^2.6.0",
79
88
  "@solid-primitives/event-bus": "^1.1.4",
80
- "@standard-community/standard-json": "^0.3.6",
81
- "@standard-community/standard-openapi": "^0.2.10",
82
- "@zip.js/zip.js": "^2.23.0",
83
- "ai": "^7.0.137",
84
- "chokidar": "^5.0.0",
89
+ "@standard-community/standard-json": "^0.4.0",
90
+ "@standard-community/standard-openapi": "^0.2.11",
91
+ "@zip.js/zip.js": "^2.24.0",
92
+ "ai": "^7.0.161",
93
+ "chokidar": "^6.0.0",
85
94
  "clipboardy": "^5.3.2",
86
- "command-stream": "^2.3.0",
95
+ "command-stream": "^2.6.0",
87
96
  "decimal.js": "^10.6.0",
88
97
  "diff": "^9.0.0",
89
98
  "fuzzysort": "^4.0.2",
90
- "glob": "^13.0.6",
91
- "gpt-tokenizer": "^4.0.0",
99
+ "glob": "^14.0.3",
100
+ "google-auth-library": "^11.3.2",
101
+ "gpt-tokenizer": "^4.1.0",
92
102
  "gray-matter": "^4.0.3",
93
- "hono": "^4.13.13",
94
- "hono-openapi": "^1.3.5",
95
- "ignore": "^7.0.12",
103
+ "hono": "^4.13.16",
104
+ "hono-openapi": "^1.5.0",
105
+ "ignore": "^7.0.14",
96
106
  "jsonc-parser": "^3.3.1",
97
107
  "lino-arguments": "^0.3.0",
98
- "lino-objects-codec": "^0.9.2",
108
+ "lino-objects-codec": "^0.9.5",
99
109
  "log-lazy": "^1.0.4",
100
- "minimatch": "^10.2.6",
101
- "remeda": "^2.51.0",
102
- "solid-js": "1.9.12",
110
+ "minimatch": "^11.0.4",
111
+ "remeda": "^2.58.0",
112
+ "solid-js": "^1.9.18",
103
113
  "strip-ansi": "^7.2.0",
104
114
  "test-anywhere": "^0.9.1",
105
115
  "tree-sitter-bash": "^0.25.1",
106
116
  "turndown": "^7.2.4",
107
117
  "ulid": "^3.0.2",
108
- "vscode-jsonrpc": "^9.0.3",
109
- "vscode-languageserver-types": "^3.18.4",
118
+ "vscode-jsonrpc": "^10.0.0",
119
+ "vscode-languageserver-types": "^3.18.5",
110
120
  "web-tree-sitter": "0.25.10",
111
121
  "xdg-basedir": "^5.1.0",
112
122
  "yargs": "^18.2.0",
113
- "zod": "^4.6.6"
123
+ "zod": "^4.6.7"
114
124
  },
115
125
  "devDependencies": {
116
126
  "@changesets/cli": "^3.0.3",
117
127
  "@eslint/js": "^10.0.1",
118
- "eslint": "^10.12.0",
128
+ "eslint": "^10.14.0",
119
129
  "eslint-config-prettier": "^10.1.8",
120
- "eslint-plugin-prettier": "^5.5.6",
130
+ "eslint-plugin-prettier": "^5.5.7",
121
131
  "eslint-plugin-promise": "^7.3.0",
122
132
  "husky": "^9.1.7",
123
133
  "lint-staged": "^17.6.0",
@@ -138,14 +148,13 @@
138
148
  "allowScripts": {
139
149
  "@parcel/watcher@2.6.0": true,
140
150
  "tree-sitter-bash@0.25.1": true,
141
- "node-pty@1.2.0-beta.15": true
151
+ "node-pty@1.1.0": true
142
152
  },
143
153
  "overrides": {
144
- "seroval": "^1.6.9",
145
154
  "undici": "^7.16.0"
146
155
  },
147
156
  "dependencyPins": {
148
- "solid-js": "https://github.com/anomalyco/opentui/issues/689",
157
+ "lino-arguments": "https://github.com/link-foundation/lino-arguments/issues/50",
149
158
  "web-tree-sitter": "https://github.com/anomalyco/opentui/issues/1550"
150
159
  }
151
160
  }
@@ -1,7 +1,7 @@
1
1
  import { Config } from '../config/file-config';
2
2
  import z from 'zod';
3
3
  import { Provider } from '../provider/provider';
4
- import { generateObject } from 'ai';
4
+ import { generateText, Output } from 'ai';
5
5
  import PROMPT_GENERATE from './generate.txt';
6
6
  import { SystemPrompt } from '../session/system';
7
7
  import { Instance } from '../project/instance';
@@ -136,7 +136,10 @@ export namespace Agent {
136
136
  const system = SystemPrompt.header(defaultModel.providerID);
137
137
  system.push(PROMPT_GENERATE);
138
138
  const existing = await list();
139
- const result = await generateObject({
139
+ // generateObject is deprecated in AI SDK 7 and printed a
140
+ // DeprecationWarning on every call (#337); structured output now goes
141
+ // through generateText's `output` setting.
142
+ const result = await generateText({
140
143
  temperature: 0.3,
141
144
  // The system prompt belongs in `instructions`, not in `prompt`: the AI SDK
142
145
  // warns that a system message inside the prompt enables prompt
@@ -152,13 +155,15 @@ export namespace Agent {
152
155
  },
153
156
  ],
154
157
  model: model.language,
155
- schema: z.object({
156
- identifier: z.string(),
157
- whenToUse: z.string(),
158
- systemPrompt: z.string(),
158
+ output: Output.object({
159
+ schema: z.object({
160
+ identifier: z.string(),
161
+ whenToUse: z.string(),
162
+ systemPrompt: z.string(),
163
+ }),
159
164
  }),
160
165
  });
161
- return result.object;
166
+ return result.output;
162
167
  }
163
168
  }
164
169
 
@@ -0,0 +1,241 @@
1
+ import { Auth } from './index';
2
+ import type { AuthPlugin } from './plugins';
3
+ import { Log } from '../util/log';
4
+
5
+ /**
6
+ * Microsoft Entra ID login for Azure OpenAI / AI Services through the Azure
7
+ * CLI session, ported from OpenCode's Azure plugin
8
+ * (https://github.com/anomalyco/opencode,
9
+ * packages/core/src/plugin/provider/azure.ts) (#334).
10
+ *
11
+ * Like OpenCode, no token is stored: `agent auth login` remembers the
12
+ * resource name, and every request mints (and caches) a bearer token with
13
+ * `az account get-access-token` for the scope of the target URL.
14
+ */
15
+ export namespace AzureLogin {
16
+ const log = Log.create({ service: 'azure-login' });
17
+
18
+ export const COGNITIVE_SCOPE = 'https://cognitiveservices.azure.com/.default';
19
+ export const FOUNDRY_SCOPE = 'https://ai.azure.com/.default';
20
+ /** Stored in `refresh` to mark an Azure CLI login. */
21
+ export const CLI_MARKER = 'azure-cli';
22
+ /**
23
+ * `@ai-sdk/azure` insists on an API key; requests replace the resulting
24
+ * `api-key` header with the Entra bearer token.
25
+ */
26
+ export const PLACEHOLDER_KEY = 'azure-entra-id';
27
+ // A resource name becomes a hostname label, so nothing else is accepted.
28
+ export const RESOURCE_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9-]*$/;
29
+ const REFRESH_MARGIN_MS = 5 * 60_000;
30
+ const TIMEOUT_MS = 10_000;
31
+
32
+ export type Run = (args: string[]) => Promise<string>;
33
+
34
+ /** Run the Azure CLI and return stdout; fails with its stderr. */
35
+ export const runAz: Run = async (args) => {
36
+ const proc = Bun.spawn(['az', ...args], {
37
+ stdin: 'ignore',
38
+ stdout: 'pipe',
39
+ stderr: 'pipe',
40
+ env: process.env,
41
+ });
42
+ const timer = setTimeout(() => proc.kill(), TIMEOUT_MS);
43
+ try {
44
+ const [stdout, stderr, code] = await Promise.all([
45
+ new Response(proc.stdout).text(),
46
+ new Response(proc.stderr).text(),
47
+ proc.exited,
48
+ ]);
49
+ if (code !== 0) {
50
+ throw new Error(
51
+ `az ${args.slice(0, 2).join(' ')} failed: ${stderr.trim() || `exit ${code}`}`
52
+ );
53
+ }
54
+ return stdout;
55
+ } finally {
56
+ clearTimeout(timer);
57
+ }
58
+ };
59
+
60
+ export function parseToken(stdout: string): {
61
+ access: string;
62
+ expires: number;
63
+ } {
64
+ const data = JSON.parse(stdout);
65
+ if (typeof data?.accessToken !== 'string' || !data.accessToken) {
66
+ throw new Error('Azure CLI returned no access token');
67
+ }
68
+ const expires =
69
+ typeof data.expires_on === 'number'
70
+ ? data.expires_on * 1000
71
+ : Date.parse(data.expiresOn ?? '');
72
+ if (!Number.isFinite(expires)) {
73
+ throw new Error('Azure CLI returned an invalid token expiration');
74
+ }
75
+ return { access: data.accessToken, expires };
76
+ }
77
+
78
+ /** Token getter with a per-scope cache refreshed 5 minutes early. */
79
+ export function tokenSource(
80
+ run: Run = runAz,
81
+ now: () => number = Date.now
82
+ ): (scope: string) => Promise<string> {
83
+ const cache = new Map<string, { access: string; expires: number }>();
84
+ const pending = new Map<string, Promise<string>>();
85
+ return (scope) => {
86
+ const cached = cache.get(scope);
87
+ if (cached && cached.expires - now() > REFRESH_MARGIN_MS) {
88
+ return Promise.resolve(cached.access);
89
+ }
90
+ const inflight = pending.get(scope);
91
+ if (inflight) return inflight;
92
+ const request = run([
93
+ 'account',
94
+ 'get-access-token',
95
+ '--scope',
96
+ scope,
97
+ '--output',
98
+ 'json',
99
+ ])
100
+ .then((stdout) => {
101
+ const token = parseToken(stdout);
102
+ cache.set(scope, token);
103
+ return token.access;
104
+ })
105
+ .finally(() => pending.delete(scope));
106
+ pending.set(scope, request);
107
+ return request;
108
+ };
109
+ }
110
+
111
+ /** Foundry project endpoints use their own audience, like OpenCode. */
112
+ export function scopeFor(url: string): string {
113
+ const target = new URL(url);
114
+ return target.hostname.endsWith('.services.ai.azure.com') &&
115
+ !target.pathname.startsWith('/models')
116
+ ? FOUNDRY_SCOPE
117
+ : COGNITIVE_SCOPE;
118
+ }
119
+
120
+ /** Swap the `api-key` header for an Entra bearer token. */
121
+ export function entraFetch(
122
+ token: (scope: string) => Promise<string>,
123
+ base: typeof fetch = fetch
124
+ ): typeof fetch {
125
+ return (async (input: any, init?: RequestInit) => {
126
+ const url =
127
+ typeof input === 'string'
128
+ ? input
129
+ : input instanceof URL
130
+ ? input.href
131
+ : input.url;
132
+ const headers = new Headers(
133
+ init?.headers ?? (input instanceof Request ? input.headers : undefined)
134
+ );
135
+ headers.delete('api-key');
136
+ headers.delete('x-api-key');
137
+ headers.set('authorization', `Bearer ${await token(scopeFor(url))}`);
138
+ return base(input, { ...init, headers });
139
+ }) as typeof fetch;
140
+ }
141
+
142
+ export function isCliLogin(info: Auth.Info | undefined): boolean {
143
+ return info?.type === 'oauth' && info.refresh === CLI_MARKER;
144
+ }
145
+
146
+ /**
147
+ * SDK options for a stored Azure CLI login, or undefined if there is none.
148
+ * Used by the `azure` custom loader in provider.ts.
149
+ */
150
+ export async function options(
151
+ info?: Auth.Info,
152
+ token: (scope: string) => Promise<string> = defaultToken()
153
+ ): Promise<Record<string, any> | undefined> {
154
+ if (!isCliLogin(info) || info?.type !== 'oauth') return undefined;
155
+ const resourceName =
156
+ info.enterpriseUrl || process.env['AZURE_RESOURCE_NAME'];
157
+ if (!resourceName || !RESOURCE_PATTERN.test(resourceName)) {
158
+ log.warn(() => ({
159
+ message: 'Azure CLI login has no valid resource name, ignoring it',
160
+ }));
161
+ return undefined;
162
+ }
163
+ return {
164
+ resourceName,
165
+ apiKey: PLACEHOLDER_KEY,
166
+ fetch: entraFetch(token),
167
+ };
168
+ }
169
+
170
+ let shared: ((scope: string) => Promise<string>) | undefined;
171
+ function defaultToken() {
172
+ return (shared ??= tokenSource());
173
+ }
174
+
175
+ export function makePlugin(run: Run = runAz): AuthPlugin {
176
+ return {
177
+ provider: 'azure',
178
+ methods: [
179
+ {
180
+ label: 'Microsoft Entra ID (Azure CLI)',
181
+ type: 'oauth',
182
+ prompts: [
183
+ {
184
+ type: 'text',
185
+ key: 'resourceName',
186
+ message: 'Azure OpenAI or AI Services resource name',
187
+ placeholder: process.env['AZURE_RESOURCE_NAME'] || 'my-models',
188
+ validate: (value) =>
189
+ !value && process.env['AZURE_RESOURCE_NAME']
190
+ ? undefined
191
+ : RESOURCE_PATTERN.test(value)
192
+ ? undefined
193
+ : 'Use letters, digits and hyphens only',
194
+ },
195
+ ],
196
+ async authorize(inputs = {}) {
197
+ const resourceName =
198
+ inputs['resourceName']?.trim() ||
199
+ process.env['AZURE_RESOURCE_NAME']?.trim() ||
200
+ '';
201
+ if (!RESOURCE_PATTERN.test(resourceName)) {
202
+ log.error(() => ({ message: 'invalid Azure resource name' }));
203
+ return { type: 'failed' as const };
204
+ }
205
+ return {
206
+ url: '',
207
+ instructions:
208
+ 'Using your Azure CLI session (`az login`) for Microsoft Entra ID.',
209
+ method: 'auto' as const,
210
+ async callback() {
211
+ try {
212
+ // Prove the CLI session works before saving the login.
213
+ await tokenSource(run)(COGNITIVE_SCOPE);
214
+ } catch (error) {
215
+ log.error(() => ({
216
+ message:
217
+ 'Azure CLI token request failed; install the Azure CLI and run `az login`',
218
+ error:
219
+ error instanceof Error ? error.message : String(error),
220
+ }));
221
+ return { type: 'failed' as const };
222
+ }
223
+ return {
224
+ type: 'success' as const,
225
+ provider: 'azure',
226
+ refresh: CLI_MARKER,
227
+ access: '',
228
+ expires: 0,
229
+ enterpriseUrl: resourceName,
230
+ };
231
+ },
232
+ };
233
+ },
234
+ },
235
+ { label: 'Manually enter API Key', type: 'api' },
236
+ ],
237
+ };
238
+ }
239
+
240
+ export const Plugin = makePlugin();
241
+ }
@@ -4,6 +4,8 @@ import * as net from 'node:net';
4
4
  import { Auth } from './index';
5
5
  import { Log } from '../util/log';
6
6
  import { createVerboseFetch } from '../util/verbose-fetch';
7
+ import { ProviderLogins } from './provider-logins';
8
+ import { AzureLogin } from './azure-login';
7
9
 
8
10
  /**
9
11
  * Auth Plugins Module
@@ -78,6 +80,8 @@ export interface AuthPlugin {
78
80
  apiKey?: string;
79
81
  baseURL?: string;
80
82
  fetch?: typeof fetch;
83
+ /** Extra SDK options, e.g. GitLab's `instanceUrl`. */
84
+ options?: Record<string, any>;
81
85
  }>;
82
86
  }
83
87
 
@@ -2819,6 +2823,11 @@ const plugins: Record<string, AuthPlugin> = {
2819
2823
  'qwen-coder': QwenPlugin,
2820
2824
  alibaba: AlibabaPlugin,
2821
2825
  kilo: KiloPlugin,
2826
+ xai: ProviderLogins.XAIPlugin,
2827
+ poe: ProviderLogins.PoePlugin,
2828
+ digitalocean: ProviderLogins.DigitalOceanPlugin,
2829
+ gitlab: ProviderLogins.GitLabPlugin,
2830
+ azure: AzureLogin.Plugin,
2822
2831
  };
2823
2832
 
2824
2833
  /**