@link-assistant/agent 0.0.8 → 0.0.11

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 (104) hide show
  1. package/EXAMPLES.md +80 -1
  2. package/MODELS.md +72 -24
  3. package/README.md +95 -2
  4. package/TOOLS.md +20 -0
  5. package/package.json +36 -2
  6. package/src/agent/agent.ts +68 -54
  7. package/src/auth/claude-oauth.ts +426 -0
  8. package/src/auth/index.ts +28 -26
  9. package/src/auth/plugins.ts +876 -0
  10. package/src/bun/index.ts +53 -43
  11. package/src/bus/global.ts +5 -5
  12. package/src/bus/index.ts +59 -53
  13. package/src/cli/bootstrap.js +12 -12
  14. package/src/cli/bootstrap.ts +6 -6
  15. package/src/cli/cmd/agent.ts +97 -92
  16. package/src/cli/cmd/auth.ts +468 -0
  17. package/src/cli/cmd/cmd.ts +2 -2
  18. package/src/cli/cmd/export.ts +41 -41
  19. package/src/cli/cmd/mcp.ts +210 -53
  20. package/src/cli/cmd/models.ts +30 -29
  21. package/src/cli/cmd/run.ts +269 -213
  22. package/src/cli/cmd/stats.ts +185 -146
  23. package/src/cli/error.ts +17 -13
  24. package/src/cli/ui.ts +78 -0
  25. package/src/command/index.ts +26 -26
  26. package/src/config/config.ts +528 -288
  27. package/src/config/markdown.ts +15 -15
  28. package/src/file/ripgrep.ts +201 -169
  29. package/src/file/time.ts +21 -18
  30. package/src/file/watcher.ts +51 -42
  31. package/src/file.ts +1 -1
  32. package/src/flag/flag.ts +26 -11
  33. package/src/format/formatter.ts +206 -162
  34. package/src/format/index.ts +61 -61
  35. package/src/global/index.ts +21 -21
  36. package/src/id/id.ts +47 -33
  37. package/src/index.js +554 -332
  38. package/src/json-standard/index.ts +173 -0
  39. package/src/mcp/index.ts +135 -128
  40. package/src/patch/index.ts +336 -267
  41. package/src/project/bootstrap.ts +15 -15
  42. package/src/project/instance.ts +43 -36
  43. package/src/project/project.ts +47 -47
  44. package/src/project/state.ts +37 -33
  45. package/src/provider/models-macro.ts +5 -5
  46. package/src/provider/models.ts +32 -32
  47. package/src/provider/opencode.js +19 -19
  48. package/src/provider/provider.ts +518 -277
  49. package/src/provider/transform.ts +143 -102
  50. package/src/server/project.ts +21 -21
  51. package/src/server/server.ts +111 -105
  52. package/src/session/agent.js +66 -60
  53. package/src/session/compaction.ts +136 -111
  54. package/src/session/index.ts +189 -156
  55. package/src/session/message-v2.ts +312 -268
  56. package/src/session/message.ts +73 -57
  57. package/src/session/processor.ts +180 -166
  58. package/src/session/prompt.ts +678 -533
  59. package/src/session/retry.ts +26 -23
  60. package/src/session/revert.ts +76 -62
  61. package/src/session/status.ts +26 -26
  62. package/src/session/summary.ts +97 -76
  63. package/src/session/system.ts +77 -63
  64. package/src/session/todo.ts +22 -16
  65. package/src/snapshot/index.ts +92 -76
  66. package/src/storage/storage.ts +157 -120
  67. package/src/tool/bash.ts +116 -106
  68. package/src/tool/batch.ts +73 -59
  69. package/src/tool/codesearch.ts +60 -53
  70. package/src/tool/edit.ts +319 -263
  71. package/src/tool/glob.ts +32 -28
  72. package/src/tool/grep.ts +72 -53
  73. package/src/tool/invalid.ts +7 -7
  74. package/src/tool/ls.ts +77 -64
  75. package/src/tool/multiedit.ts +30 -21
  76. package/src/tool/patch.ts +121 -94
  77. package/src/tool/read.ts +140 -122
  78. package/src/tool/registry.ts +38 -38
  79. package/src/tool/task.ts +93 -60
  80. package/src/tool/todo.ts +16 -16
  81. package/src/tool/tool.ts +45 -36
  82. package/src/tool/webfetch.ts +97 -74
  83. package/src/tool/websearch.ts +78 -64
  84. package/src/tool/write.ts +21 -15
  85. package/src/util/binary.ts +27 -19
  86. package/src/util/context.ts +8 -8
  87. package/src/util/defer.ts +7 -5
  88. package/src/util/error.ts +24 -19
  89. package/src/util/eventloop.ts +16 -10
  90. package/src/util/filesystem.ts +37 -33
  91. package/src/util/fn.ts +11 -8
  92. package/src/util/iife.ts +1 -1
  93. package/src/util/keybind.ts +44 -44
  94. package/src/util/lazy.ts +7 -7
  95. package/src/util/locale.ts +20 -16
  96. package/src/util/lock.ts +43 -38
  97. package/src/util/log.ts +95 -85
  98. package/src/util/queue.ts +8 -8
  99. package/src/util/rpc.ts +35 -23
  100. package/src/util/scrap.ts +4 -4
  101. package/src/util/signal.ts +5 -5
  102. package/src/util/timeout.ts +6 -6
  103. package/src/util/token.ts +2 -2
  104. package/src/util/wildcard.ts +38 -27
@@ -1,18 +1,18 @@
1
- import { Config } from "../config/config"
2
- import z from "zod"
3
- import { Provider } from "../provider/provider"
4
- import { generateObject, type ModelMessage } from "ai"
5
- import PROMPT_GENERATE from "./generate.txt"
6
- import { SystemPrompt } from "../session/system"
7
- import { Instance } from "../project/instance"
8
- import { mergeDeep } from "remeda"
1
+ import { Config } from '../config/config';
2
+ import z from 'zod';
3
+ import { Provider } from '../provider/provider';
4
+ import { generateObject, type ModelMessage } from 'ai';
5
+ import PROMPT_GENERATE from './generate.txt';
6
+ import { SystemPrompt } from '../session/system';
7
+ import { Instance } from '../project/instance';
8
+ import { mergeDeep } from 'remeda';
9
9
 
10
10
  export namespace Agent {
11
11
  export const Info = z
12
12
  .object({
13
13
  name: z.string(),
14
14
  description: z.string().optional(),
15
- mode: z.enum(["subagent", "primary", "all"]),
15
+ mode: z.enum(['subagent', 'primary', 'all']),
16
16
  builtIn: z.boolean(),
17
17
  topP: z.number().optional(),
18
18
  temperature: z.number().optional(),
@@ -28,112 +28,126 @@ export namespace Agent {
28
28
  options: z.record(z.string(), z.any()),
29
29
  })
30
30
  .meta({
31
- ref: "Agent",
32
- })
33
- export type Info = z.infer<typeof Info>
31
+ ref: 'Agent',
32
+ });
33
+ export type Info = z.infer<typeof Info>;
34
34
 
35
35
  const state = Instance.state(async () => {
36
- const cfg = await Config.get()
37
- const defaultTools = cfg.tools ?? {}
36
+ const cfg = await Config.get();
37
+ const defaultTools = cfg.tools ?? {};
38
38
 
39
39
  const result: Record<string, Info> = {
40
40
  general: {
41
- name: "general",
41
+ name: 'general',
42
42
  description:
43
- "General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you.",
43
+ 'General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you.',
44
44
  tools: {
45
45
  todoread: false,
46
46
  todowrite: false,
47
47
  ...defaultTools,
48
48
  },
49
49
  options: {},
50
- mode: "subagent",
50
+ mode: 'subagent',
51
51
  builtIn: true,
52
52
  },
53
53
  build: {
54
- name: "build",
54
+ name: 'build',
55
55
  tools: { ...defaultTools },
56
56
  options: {},
57
- mode: "primary",
57
+ mode: 'primary',
58
58
  builtIn: true,
59
59
  },
60
60
  plan: {
61
- name: "plan",
61
+ name: 'plan',
62
62
  options: {},
63
63
  tools: {
64
64
  ...defaultTools,
65
65
  },
66
- mode: "primary",
66
+ mode: 'primary',
67
67
  builtIn: true,
68
68
  },
69
- }
69
+ };
70
70
  for (const [key, value] of Object.entries(cfg.agent ?? {})) {
71
71
  if (value.disable) {
72
- delete result[key]
73
- continue
72
+ delete result[key];
73
+ continue;
74
74
  }
75
- let item = result[key]
75
+ let item = result[key];
76
76
  if (!item)
77
77
  item = result[key] = {
78
78
  name: key,
79
- mode: "all",
79
+ mode: 'all',
80
80
  options: {},
81
81
  tools: {},
82
82
  builtIn: false,
83
- }
84
- const { name, model, prompt, tools, description, temperature, top_p, mode, color, ...extra } = value
83
+ };
84
+ const {
85
+ name,
86
+ model,
87
+ prompt,
88
+ tools,
89
+ description,
90
+ temperature,
91
+ top_p,
92
+ mode,
93
+ color,
94
+ ...extra
95
+ } = value;
85
96
  item.options = {
86
97
  ...item.options,
87
98
  ...extra,
88
- }
89
- if (model) item.model = Provider.parseModel(model)
90
- if (prompt) item.prompt = prompt
99
+ };
100
+ if (model) item.model = Provider.parseModel(model);
101
+ if (prompt) item.prompt = prompt;
91
102
  if (tools)
92
103
  item.tools = {
93
104
  ...item.tools,
94
105
  ...tools,
95
- }
106
+ };
96
107
  item.tools = {
97
108
  ...defaultTools,
98
109
  ...item.tools,
99
- }
100
- if (description) item.description = description
101
- if (temperature != undefined) item.temperature = temperature
102
- if (top_p != undefined) item.topP = top_p
103
- if (mode) item.mode = mode
104
- if (color) item.color = color
110
+ };
111
+ if (description) item.description = description;
112
+ if (temperature != undefined) item.temperature = temperature;
113
+ if (top_p != undefined) item.topP = top_p;
114
+ if (mode) item.mode = mode;
115
+ if (color) item.color = color;
105
116
  // just here for consistency & to prevent it from being added as an option
106
- if (name) item.name = name
117
+ if (name) item.name = name;
107
118
  }
108
- return result
109
- })
119
+ return result;
120
+ });
110
121
 
111
122
  export async function get(agent: string) {
112
- return state().then((x) => x[agent])
123
+ return state().then((x) => x[agent]);
113
124
  }
114
125
 
115
126
  export async function list() {
116
- return state().then((x) => Object.values(x))
127
+ return state().then((x) => Object.values(x));
117
128
  }
118
129
 
119
130
  export async function generate(input: { description: string }) {
120
- const defaultModel = await Provider.defaultModel()
121
- const model = await Provider.getModel(defaultModel.providerID, defaultModel.modelID)
122
- const system = SystemPrompt.header(defaultModel.providerID)
123
- system.push(PROMPT_GENERATE)
124
- const existing = await list()
131
+ const defaultModel = await Provider.defaultModel();
132
+ const model = await Provider.getModel(
133
+ defaultModel.providerID,
134
+ defaultModel.modelID
135
+ );
136
+ const system = SystemPrompt.header(defaultModel.providerID);
137
+ system.push(PROMPT_GENERATE);
138
+ const existing = await list();
125
139
  const result = await generateObject({
126
140
  temperature: 0.3,
127
141
  prompt: [
128
142
  ...system.map(
129
143
  (item): ModelMessage => ({
130
- role: "system",
144
+ role: 'system',
131
145
  content: item,
132
- }),
146
+ })
133
147
  ),
134
148
  {
135
- role: "user",
136
- content: `Create an agent configuration based on this request: \"${input.description}\".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(", ")}\n Return ONLY the JSON object, no other text, do not wrap in backticks`,
149
+ role: 'user',
150
+ content: `Create an agent configuration based on this request: \"${input.description}\".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(', ')}\n Return ONLY the JSON object, no other text, do not wrap in backticks`,
137
151
  },
138
152
  ],
139
153
  model: model.language,
@@ -142,8 +156,8 @@ export namespace Agent {
142
156
  whenToUse: z.string(),
143
157
  systemPrompt: z.string(),
144
158
  }),
145
- })
146
- return result.object
159
+ });
160
+ return result.object;
147
161
  }
148
162
  }
149
163
 
@@ -0,0 +1,426 @@
1
+ import crypto from 'crypto';
2
+ import path from 'path';
3
+ import { Global } from '../global';
4
+ import { Log } from '../util/log';
5
+ import z from 'zod';
6
+
7
+ /**
8
+ * Claude OAuth Module
9
+ *
10
+ * Implements OAuth 2.0 with PKCE for Claude authentication.
11
+ * This allows using Claude Pro/Max subscription for API access.
12
+ *
13
+ * OAuth Flow:
14
+ * 1. Generate PKCE code verifier and challenge
15
+ * 2. Open browser to authorization URL
16
+ * 3. User authenticates and authorizes
17
+ * 4. Receive authorization code via redirect
18
+ * 5. Exchange code for tokens
19
+ * 6. Store tokens in ~/.claude/.credentials.json
20
+ *
21
+ * References:
22
+ * - Claude Code CLI uses the same OAuth flow
23
+ * - https://github.com/grll/claude-code-login for implementation details
24
+ */
25
+ export namespace ClaudeOAuth {
26
+ const log = Log.create({ service: 'claude-oauth' });
27
+
28
+ /**
29
+ * OAuth Configuration
30
+ * These values are from the Claude Code CLI OAuth implementation
31
+ */
32
+ export const Config = {
33
+ // OAuth endpoints
34
+ authorizationUrl: 'https://claude.ai/oauth/authorize',
35
+ tokenUrl: 'https://console.anthropic.com/v1/oauth/token',
36
+ redirectUri: 'https://console.anthropic.com/oauth/code/callback',
37
+
38
+ // OAuth client - this is the same client ID used by Claude Code CLI
39
+ clientId: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
40
+
41
+ // Requested scopes for API access
42
+ scopes: ['org:create_api_key', 'user:profile', 'user:inference'],
43
+
44
+ // API beta header for OAuth authentication
45
+ betaHeader: 'oauth-2025-04-20',
46
+ } as const;
47
+
48
+ /**
49
+ * Schema for OAuth credentials stored in ~/.claude/.credentials.json
50
+ */
51
+ export const Credentials = z.object({
52
+ claudeAiOauth: z
53
+ .object({
54
+ accessToken: z.string(),
55
+ refreshToken: z.string(),
56
+ expiresAt: z.number(),
57
+ scopes: z.array(z.string()).optional(),
58
+ subscriptionType: z.string().optional(),
59
+ rateLimitTier: z.string().optional(),
60
+ })
61
+ .optional(),
62
+ });
63
+
64
+ export type Credentials = z.infer<typeof Credentials>;
65
+
66
+ /**
67
+ * Schema for OAuth state file (temporary, during auth flow)
68
+ */
69
+ export const OAuthState = z.object({
70
+ state: z.string(),
71
+ codeVerifier: z.string(),
72
+ expiresAt: z.number(),
73
+ });
74
+
75
+ export type OAuthState = z.infer<typeof OAuthState>;
76
+
77
+ /**
78
+ * Schema for token response from OAuth server
79
+ */
80
+ export const TokenResponse = z.object({
81
+ access_token: z.string(),
82
+ refresh_token: z.string(),
83
+ expires_in: z.number(),
84
+ token_type: z.string(),
85
+ scope: z.string().optional(),
86
+ });
87
+
88
+ export type TokenResponse = z.infer<typeof TokenResponse>;
89
+
90
+ /**
91
+ * Paths for credential and state storage
92
+ */
93
+ const claudeDir = path.join(Global.Path.home, '.claude');
94
+ const credentialsPath = path.join(claudeDir, '.credentials.json');
95
+ const statePath = path.join(claudeDir, '.oauth_state.json');
96
+
97
+ /**
98
+ * Generate a cryptographically secure random string
99
+ */
100
+ function generateRandomString(length: number): string {
101
+ return crypto.randomBytes(length).toString('base64url');
102
+ }
103
+
104
+ /**
105
+ * Generate PKCE code challenge from code verifier
106
+ */
107
+ function generateCodeChallenge(verifier: string): string {
108
+ return crypto.createHash('sha256').update(verifier).digest('base64url');
109
+ }
110
+
111
+ /**
112
+ * Generate authorization URL with PKCE parameters
113
+ *
114
+ * @returns Object containing the authorization URL and state data to save
115
+ */
116
+ export function generateAuthUrl(): { url: string; state: OAuthState } {
117
+ // Generate PKCE values
118
+ const codeVerifier = generateRandomString(32);
119
+ const codeChallenge = generateCodeChallenge(codeVerifier);
120
+ const state = generateRandomString(32);
121
+
122
+ // Build authorization URL
123
+ const params = new URLSearchParams({
124
+ client_id: Config.clientId,
125
+ redirect_uri: Config.redirectUri,
126
+ response_type: 'code',
127
+ scope: Config.scopes.join(' '),
128
+ state,
129
+ code_challenge: codeChallenge,
130
+ code_challenge_method: 'S256',
131
+ });
132
+
133
+ const url = `${Config.authorizationUrl}?${params.toString()}`;
134
+
135
+ // State expires in 10 minutes
136
+ const oauthState: OAuthState = {
137
+ state,
138
+ codeVerifier,
139
+ expiresAt: Date.now() + 10 * 60 * 1000,
140
+ };
141
+
142
+ return { url, state: oauthState };
143
+ }
144
+
145
+ /**
146
+ * Save OAuth state for later code exchange
147
+ */
148
+ export async function saveState(state: OAuthState): Promise<void> {
149
+ await Bun.write(statePath, JSON.stringify(state, null, 2));
150
+ log.info('saved oauth state', {
151
+ expiresAt: new Date(state.expiresAt).toISOString(),
152
+ });
153
+ }
154
+
155
+ /**
156
+ * Load saved OAuth state
157
+ */
158
+ export async function loadState(): Promise<OAuthState | undefined> {
159
+ try {
160
+ const file = Bun.file(statePath);
161
+ if (!(await file.exists())) {
162
+ return undefined;
163
+ }
164
+ const content = await file.json();
165
+ const parsed = OAuthState.parse(content);
166
+
167
+ if (parsed.expiresAt < Date.now()) {
168
+ log.warn('oauth state expired');
169
+ await clearState();
170
+ return undefined;
171
+ }
172
+
173
+ return parsed;
174
+ } catch (error) {
175
+ log.error('failed to load oauth state', { error });
176
+ return undefined;
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Clear saved OAuth state
182
+ */
183
+ export async function clearState(): Promise<void> {
184
+ try {
185
+ const file = Bun.file(statePath);
186
+ if (await file.exists()) {
187
+ await Bun.write(statePath, '');
188
+ // Delete the file
189
+ const fs = await import('fs/promises');
190
+ await fs.unlink(statePath).catch(() => {});
191
+ }
192
+ } catch (error) {
193
+ log.error('failed to clear oauth state', { error });
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Exchange authorization code for tokens
199
+ *
200
+ * @param code - Authorization code from OAuth callback
201
+ * @param codeVerifier - PKCE code verifier
202
+ * @returns Token response from OAuth server
203
+ */
204
+ export async function exchangeCode(
205
+ code: string,
206
+ codeVerifier: string
207
+ ): Promise<TokenResponse> {
208
+ const body = new URLSearchParams({
209
+ grant_type: 'authorization_code',
210
+ client_id: Config.clientId,
211
+ redirect_uri: Config.redirectUri,
212
+ code,
213
+ code_verifier: codeVerifier,
214
+ });
215
+
216
+ log.info('exchanging authorization code for tokens');
217
+
218
+ const response = await fetch(Config.tokenUrl, {
219
+ method: 'POST',
220
+ headers: {
221
+ 'Content-Type': 'application/x-www-form-urlencoded',
222
+ },
223
+ body: body.toString(),
224
+ });
225
+
226
+ if (!response.ok) {
227
+ const error = await response.text();
228
+ log.error('token exchange failed', { status: response.status, error });
229
+ throw new Error(`Token exchange failed: ${response.status} ${error}`);
230
+ }
231
+
232
+ const data = await response.json();
233
+ return TokenResponse.parse(data);
234
+ }
235
+
236
+ /**
237
+ * Save credentials to ~/.claude/.credentials.json
238
+ *
239
+ * @param tokens - Token response from OAuth server
240
+ */
241
+ export async function saveCredentials(tokens: TokenResponse): Promise<void> {
242
+ // Ensure .claude directory exists
243
+ const fs = await import('fs/promises');
244
+ await fs.mkdir(claudeDir, { recursive: true });
245
+
246
+ // Load existing credentials if any
247
+ let existing: Credentials = {};
248
+ try {
249
+ const file = Bun.file(credentialsPath);
250
+ if (await file.exists()) {
251
+ existing = await file.json();
252
+ }
253
+ } catch {
254
+ // Ignore errors reading existing credentials
255
+ }
256
+
257
+ // Update with new OAuth credentials
258
+ const credentials: Credentials = {
259
+ ...existing,
260
+ claudeAiOauth: {
261
+ accessToken: tokens.access_token,
262
+ refreshToken: tokens.refresh_token,
263
+ expiresAt: Date.now() + tokens.expires_in * 1000,
264
+ scopes: tokens.scope?.split(' '),
265
+ subscriptionType: 'unknown', // Will be populated from API response
266
+ },
267
+ };
268
+
269
+ await Bun.write(credentialsPath, JSON.stringify(credentials, null, 2));
270
+ log.info('saved credentials', {
271
+ expiresAt: new Date(credentials.claudeAiOauth!.expiresAt).toISOString(),
272
+ });
273
+ }
274
+
275
+ /**
276
+ * Get stored OAuth credentials
277
+ *
278
+ * @returns OAuth credentials if available, undefined otherwise
279
+ */
280
+ export async function getCredentials(): Promise<
281
+ Credentials['claudeAiOauth'] | undefined
282
+ > {
283
+ try {
284
+ const file = Bun.file(credentialsPath);
285
+ if (!(await file.exists())) {
286
+ log.info('credentials file not found', { path: credentialsPath });
287
+ return undefined;
288
+ }
289
+
290
+ const content = await file.json();
291
+ const parsed = Credentials.parse(content);
292
+
293
+ if (!parsed.claudeAiOauth) {
294
+ log.info('no claudeAiOauth credentials found');
295
+ return undefined;
296
+ }
297
+
298
+ // Check if token is expired
299
+ if (parsed.claudeAiOauth.expiresAt < Date.now()) {
300
+ log.warn('token expired', {
301
+ expiresAt: new Date(parsed.claudeAiOauth.expiresAt).toISOString(),
302
+ });
303
+ // TODO: Implement token refresh using refreshToken
304
+ // For now, user needs to re-authenticate
305
+ }
306
+
307
+ log.info('loaded oauth credentials', {
308
+ subscriptionType: parsed.claudeAiOauth.subscriptionType,
309
+ scopes: parsed.claudeAiOauth.scopes,
310
+ });
311
+
312
+ return parsed.claudeAiOauth;
313
+ } catch (error) {
314
+ log.error('failed to read credentials', { error });
315
+ return undefined;
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Get access token for API requests
321
+ */
322
+ export async function getAccessToken(): Promise<string | undefined> {
323
+ const creds = await getCredentials();
324
+ return creds?.accessToken;
325
+ }
326
+
327
+ /**
328
+ * Check if OAuth credentials are available and valid
329
+ */
330
+ export async function isAuthenticated(): Promise<boolean> {
331
+ const creds = await getCredentials();
332
+ if (!creds?.accessToken) return false;
333
+
334
+ // Check if not expired (with 5 minute buffer)
335
+ return creds.expiresAt > Date.now() + 5 * 60 * 1000;
336
+ }
337
+
338
+ /**
339
+ * Complete the OAuth flow by exchanging the authorization code
340
+ *
341
+ * @param code - Authorization code from OAuth callback
342
+ * @returns Success status
343
+ */
344
+ export async function completeAuth(code: string): Promise<boolean> {
345
+ const state = await loadState();
346
+ if (!state) {
347
+ log.error('no oauth state found - please start login flow first');
348
+ return false;
349
+ }
350
+
351
+ try {
352
+ const tokens = await exchangeCode(code, state.codeVerifier);
353
+ await saveCredentials(tokens);
354
+ await clearState();
355
+ log.info('authentication completed successfully');
356
+ return true;
357
+ } catch (error) {
358
+ log.error('failed to complete authentication', { error });
359
+ await clearState();
360
+ return false;
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Refresh the access token using the refresh token
366
+ */
367
+ export async function refreshToken(): Promise<boolean> {
368
+ const creds = await getCredentials();
369
+ if (!creds?.refreshToken) {
370
+ log.error('no refresh token available');
371
+ return false;
372
+ }
373
+
374
+ const body = new URLSearchParams({
375
+ grant_type: 'refresh_token',
376
+ client_id: Config.clientId,
377
+ refresh_token: creds.refreshToken,
378
+ });
379
+
380
+ log.info('refreshing access token');
381
+
382
+ try {
383
+ const response = await fetch(Config.tokenUrl, {
384
+ method: 'POST',
385
+ headers: {
386
+ 'Content-Type': 'application/x-www-form-urlencoded',
387
+ },
388
+ body: body.toString(),
389
+ });
390
+
391
+ if (!response.ok) {
392
+ const error = await response.text();
393
+ log.error('token refresh failed', { status: response.status, error });
394
+ return false;
395
+ }
396
+
397
+ const tokens = TokenResponse.parse(await response.json());
398
+ await saveCredentials(tokens);
399
+ log.info('token refreshed successfully');
400
+ return true;
401
+ } catch (error) {
402
+ log.error('failed to refresh token', { error });
403
+ return false;
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Create a custom fetch function that uses OAuth Bearer token authentication
409
+ */
410
+ export function createAuthenticatedFetch(accessToken: string): typeof fetch {
411
+ return async (url, init) => {
412
+ const headers = new Headers(init?.headers);
413
+ // Remove x-api-key if present and add Authorization Bearer
414
+ headers.delete('x-api-key');
415
+ headers.set('Authorization', `Bearer ${accessToken}`);
416
+ // Add OAuth beta header
417
+ const existingBeta = headers.get('anthropic-beta');
418
+ if (existingBeta) {
419
+ headers.set('anthropic-beta', `${Config.betaHeader},${existingBeta}`);
420
+ } else {
421
+ headers.set('anthropic-beta', Config.betaHeader);
422
+ }
423
+ return fetch(url, { ...init, headers });
424
+ };
425
+ }
426
+ }