@link-assistant/agent 0.26.15 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 Super (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
@@ -735,7 +735,7 @@ This agent is **exclusively built for Bun** for the following reasons:
735
735
  4. **Minimalism**: Single runtime target keeps the codebase simple
736
736
  5. **Bun Ecosystem**: Leverages Bun-specific features and optimizations
737
737
 
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.
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/anomalyco/opencode) instead.
739
739
 
740
740
  ## Project Structure
741
741
 
@@ -753,7 +753,7 @@ js/
753
753
 
754
754
  ## Architecture
755
755
 
756
- This agent-cli reproduces the core architecture of [OpenCode](https://github.com/sst/opencode)'s `run --format json` command:
756
+ This agent-cli reproduces the core architecture of [OpenCode](https://github.com/anomalyco/opencode)'s `run --format json` command:
757
757
 
758
758
  - **Streaming JSON Events**: Instead of single responses, outputs real-time event stream
759
759
  - **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.0",
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,19 +51,28 @@
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.0.1",
55
+ "@ai-sdk/alibaba": "^2.0.74",
56
+ "@ai-sdk/amazon-bedrock": "^5.0.122",
57
+ "@ai-sdk/anthropic": "^4.0.86",
58
+ "@ai-sdk/azure": "^4.0.108",
59
+ "@ai-sdk/cerebras": "^3.0.76",
60
+ "@ai-sdk/cohere": "^4.0.67",
61
+ "@ai-sdk/deepinfra": "^3.0.76",
62
+ "@ai-sdk/deepseek": "^3.0.71",
63
+ "@ai-sdk/fireworks": "^3.0.79",
64
+ "@ai-sdk/gateway": "^4.0.122",
65
+ "@ai-sdk/google": "^4.0.103",
66
+ "@ai-sdk/google-vertex": "^5.0.120",
67
+ "@ai-sdk/groq": "^4.0.68",
68
+ "@ai-sdk/mcp": "^2.0.82",
69
+ "@ai-sdk/mistral": "^4.0.70",
70
+ "@ai-sdk/openai": "^4.0.100",
71
+ "@ai-sdk/openai-compatible": "^3.0.76",
72
+ "@ai-sdk/perplexity": "^5.0.18",
73
+ "@ai-sdk/provider": "^4.0.32",
74
+ "@ai-sdk/togetherai": "^3.0.77",
75
+ "@ai-sdk/xai": "^5.0.28",
67
76
  "@clack/prompts": "^1.8.1",
68
77
  "@hono/standard-validator": "^0.4.0",
69
78
  "@hono/zod-validator": "^0.9.1",
@@ -71,53 +80,54 @@
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.4",
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.6.6",
86
+ "@opentui/solid": "^0.6.6",
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.3.7",
90
+ "@standard-community/standard-openapi": "^0.2.11",
91
+ "@zip.js/zip.js": "^2.24.0",
92
+ "ai": "^7.0.149",
93
+ "chokidar": "^6.0.0",
85
94
  "clipboardy": "^5.3.2",
86
- "command-stream": "^2.3.0",
95
+ "command-stream": "^2.4.0",
87
96
  "decimal.js": "^10.6.0",
88
97
  "diff": "^9.0.0",
89
98
  "fuzzysort": "^4.0.2",
90
99
  "glob": "^13.0.6",
91
- "gpt-tokenizer": "^4.0.0",
100
+ "google-auth-library": "^11.3.1",
101
+ "gpt-tokenizer": "^4.1.0",
92
102
  "gray-matter": "^4.0.3",
93
103
  "hono": "^4.13.13",
94
- "hono-openapi": "^1.3.5",
95
- "ignore": "^7.0.12",
104
+ "hono-openapi": "^1.5.0",
105
+ "ignore": "^7.0.13",
96
106
  "jsonc-parser": "^3.3.1",
97
107
  "lino-arguments": "^0.3.0",
98
108
  "lino-objects-codec": "^0.9.2",
99
109
  "log-lazy": "^1.0.4",
100
110
  "minimatch": "^10.2.6",
101
- "remeda": "^2.51.0",
111
+ "remeda": "^2.53.0",
102
112
  "solid-js": "1.9.12",
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.13.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,7 +148,7 @@
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
154
  "seroval": "^1.6.9",
@@ -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
  /**