shortcutxl 0.2.12 → 0.2.13

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 (110) hide show
  1. package/README.md +26 -26
  2. package/agent-docs/README.md +397 -397
  3. package/agent-docs/docs/compaction.md +390 -390
  4. package/agent-docs/docs/custom-provider.md +580 -580
  5. package/agent-docs/docs/extensions.md +1971 -1971
  6. package/agent-docs/docs/packages.md +209 -209
  7. package/agent-docs/docs/rpc.md +1317 -1317
  8. package/agent-docs/docs/sdk.md +962 -962
  9. package/agent-docs/docs/session.md +412 -412
  10. package/agent-docs/docs/termux.md +127 -127
  11. package/agent-docs/docs/tui.md +887 -887
  12. package/agent-docs/examples/README.md +25 -25
  13. package/agent-docs/examples/extensions/README.md +205 -205
  14. package/agent-docs/examples/extensions/antigravity-image-gen.ts +447 -447
  15. package/agent-docs/examples/extensions/auto-commit-on-exit.ts +49 -49
  16. package/agent-docs/examples/extensions/bash-spawn-hook.ts +30 -30
  17. package/agent-docs/examples/extensions/bookmark.ts +50 -50
  18. package/agent-docs/examples/extensions/built-in-tool-renderer.ts +256 -256
  19. package/agent-docs/examples/extensions/claude-rules.ts +86 -86
  20. package/agent-docs/examples/extensions/commands.ts +75 -75
  21. package/agent-docs/examples/extensions/confirm-destructive.ts +59 -59
  22. package/agent-docs/examples/extensions/custom-compaction.ts +126 -126
  23. package/agent-docs/examples/extensions/custom-footer.ts +63 -63
  24. package/agent-docs/examples/extensions/custom-header.ts +73 -73
  25. package/agent-docs/examples/extensions/custom-provider-anthropic/index.ts +660 -660
  26. package/agent-docs/examples/extensions/custom-provider-gitlab-duo/index.ts +362 -362
  27. package/agent-docs/examples/extensions/custom-provider-gitlab-duo/test.ts +88 -88
  28. package/agent-docs/examples/extensions/custom-provider-qwen-cli/index.ts +349 -349
  29. package/agent-docs/examples/extensions/dirty-repo-guard.ts +56 -56
  30. package/agent-docs/examples/extensions/doom-overlay/doom-component.ts +133 -133
  31. package/agent-docs/examples/extensions/doom-overlay/doom-keys.ts +108 -108
  32. package/agent-docs/examples/extensions/doom-overlay/index.ts +74 -74
  33. package/agent-docs/examples/extensions/dynamic-resources/index.ts +15 -15
  34. package/agent-docs/examples/extensions/dynamic-tools.ts +77 -77
  35. package/agent-docs/examples/extensions/event-bus.ts +43 -43
  36. package/agent-docs/examples/extensions/file-trigger.ts +41 -41
  37. package/agent-docs/examples/extensions/git-checkpoint.ts +53 -53
  38. package/agent-docs/examples/extensions/handoff.ts +155 -155
  39. package/agent-docs/examples/extensions/hello.ts +25 -25
  40. package/agent-docs/examples/extensions/inline-bash.ts +94 -94
  41. package/agent-docs/examples/extensions/input-transform.ts +43 -43
  42. package/agent-docs/examples/extensions/interactive-shell.ts +209 -209
  43. package/agent-docs/examples/extensions/mac-system-theme.ts +47 -47
  44. package/agent-docs/examples/extensions/message-renderer.ts +59 -59
  45. package/agent-docs/examples/extensions/minimal-mode.ts +430 -430
  46. package/agent-docs/examples/extensions/modal-editor.ts +90 -90
  47. package/agent-docs/examples/extensions/model-status.ts +31 -31
  48. package/agent-docs/examples/extensions/notify.ts +55 -55
  49. package/agent-docs/examples/extensions/overlay-qa-tests.ts +936 -936
  50. package/agent-docs/examples/extensions/overlay-test.ts +159 -159
  51. package/agent-docs/examples/extensions/permission-gate.ts +37 -37
  52. package/agent-docs/examples/extensions/pirate.ts +47 -47
  53. package/agent-docs/examples/extensions/plan-mode/index.ts +363 -363
  54. package/agent-docs/examples/extensions/preset.ts +418 -418
  55. package/agent-docs/examples/extensions/protected-paths.ts +30 -30
  56. package/agent-docs/examples/extensions/qna.ts +122 -122
  57. package/agent-docs/examples/extensions/question.ts +278 -278
  58. package/agent-docs/examples/extensions/questionnaire.ts +440 -440
  59. package/agent-docs/examples/extensions/rainbow-editor.ts +90 -90
  60. package/agent-docs/examples/extensions/reload-runtime.ts +37 -37
  61. package/agent-docs/examples/extensions/rpc-demo.ts +124 -124
  62. package/agent-docs/examples/extensions/sandbox/index.ts +324 -324
  63. package/agent-docs/examples/extensions/send-user-message.ts +97 -97
  64. package/agent-docs/examples/extensions/session-name.ts +27 -27
  65. package/agent-docs/examples/extensions/shutdown-command.ts +69 -69
  66. package/agent-docs/examples/extensions/snake.ts +343 -343
  67. package/agent-docs/examples/extensions/space-invaders.ts +566 -566
  68. package/agent-docs/examples/extensions/ssh.ts +233 -233
  69. package/agent-docs/examples/extensions/status-line.ts +40 -40
  70. package/agent-docs/examples/extensions/subagent/agents.ts +130 -130
  71. package/agent-docs/examples/extensions/subagent/index.ts +1068 -1068
  72. package/agent-docs/examples/extensions/summarize.ts +206 -206
  73. package/agent-docs/examples/extensions/system-prompt-header.ts +17 -17
  74. package/agent-docs/examples/extensions/timed-confirm.ts +72 -72
  75. package/agent-docs/examples/extensions/titlebar-spinner.ts +58 -58
  76. package/agent-docs/examples/extensions/todo.ts +314 -314
  77. package/agent-docs/examples/extensions/tool-override.ts +146 -146
  78. package/agent-docs/examples/extensions/tools.ts +145 -145
  79. package/agent-docs/examples/extensions/trigger-compact.ts +40 -40
  80. package/agent-docs/examples/extensions/truncated-tool.ts +194 -194
  81. package/agent-docs/examples/extensions/widget-placement.ts +17 -17
  82. package/agent-docs/examples/extensions/with-deps/index.ts +37 -37
  83. package/agent-docs/examples/rpc-extension-ui.ts +654 -654
  84. package/agent-docs/examples/sdk/01-minimal.ts +22 -22
  85. package/agent-docs/examples/sdk/02-custom-model.ts +48 -48
  86. package/agent-docs/examples/sdk/03-custom-prompt.ts +55 -55
  87. package/agent-docs/examples/sdk/04-skills.ts +53 -53
  88. package/agent-docs/examples/sdk/05-tools.ts +56 -56
  89. package/agent-docs/examples/sdk/06-extensions.ts +88 -88
  90. package/agent-docs/examples/sdk/07-context-files.ts +40 -40
  91. package/agent-docs/examples/sdk/08-prompt-templates.ts +47 -47
  92. package/agent-docs/examples/sdk/09-api-keys-and-oauth.ts +48 -48
  93. package/agent-docs/examples/sdk/10-settings.ts +54 -54
  94. package/agent-docs/examples/sdk/11-sessions.ts +48 -48
  95. package/agent-docs/examples/sdk/12-full-control.ts +82 -82
  96. package/agent-docs/examples/sdk/README.md +144 -144
  97. package/agent-docs/xll-spec.md +110 -110
  98. package/dist/core/auth-storage.js +21 -2
  99. package/package.json +1 -1
  100. package/xll/ShortcutXL.xll +0 -0
  101. package/xll/modules/debug_render.py +272 -272
  102. package/xll/modules/gameboy.py +241 -241
  103. package/xll/modules/pong.py +188 -188
  104. package/xll/modules/shortcut_xl/_diff_highlight.py +176 -0
  105. package/xll/modules/shortcut_xl/_log.py +12 -12
  106. package/xll/modules/shortcut_xl/_registry.py +44 -44
  107. package/xll/modules/stocks.py +100 -100
  108. /package/skills/{com-advanced-api → COM-advanced-api}/SKILL.md +0 -0
  109. /package/skills/{com-advanced-api → COM-advanced-api}/excel-type-library.py +0 -0
  110. /package/skills/{com-advanced-api → COM-advanced-api}/office-type-library.py +0 -0
@@ -1,660 +1,660 @@
1
- /**
2
- * Custom Provider Example
3
- *
4
- * Demonstrates registering a custom provider with:
5
- * - Custom API identifier ("custom-anthropic-api")
6
- * - Custom streamSimple implementation
7
- * - OAuth support for /login
8
- * - API key support via environment variable
9
- * - Two model definitions
10
- *
11
- * Usage:
12
- * # First install dependencies
13
- * cd packages/coding-agent/examples/extensions/custom-provider && npm install
14
- *
15
- * # With OAuth (run /login custom-anthropic first)
16
- * shortcut -e ./packages/coding-agent/examples/extensions/custom-provider
17
- *
18
- * # With API key
19
- * CUSTOM_ANTHROPIC_API_KEY=sk-ant-... shortcut -e ./packages/coding-agent/examples/extensions/custom-provider
20
- *
21
- * Then use /model to select custom-anthropic/claude-sonnet-4-5
22
- */
23
-
24
- import Anthropic from '@anthropic-ai/sdk';
25
- import type {
26
- ContentBlockParam,
27
- MessageCreateParamsStreaming
28
- } from '@anthropic-ai/sdk/resources/messages.js';
29
- import type { ExtensionAPI } from 'shortcutxl';
30
- import {
31
- calculateCost,
32
- createAssistantMessageEventStream,
33
- type Api,
34
- type AssistantMessage,
35
- type AssistantMessageEventStream,
36
- type Context,
37
- type ImageContent,
38
- type Message,
39
- type Model,
40
- type OAuthCredentials,
41
- type OAuthLoginCallbacks,
42
- type SimpleStreamOptions,
43
- type StopReason,
44
- type TextContent,
45
- type ThinkingContent,
46
- type Tool,
47
- type ToolCall,
48
- type ToolResultMessage
49
- } from 'shortcutxl';
50
-
51
- // =============================================================================
52
- // OAuth Implementation (copied from packages/ai/src/utils/oauth/anthropic.ts)
53
- // =============================================================================
54
-
55
- const decode = (s: string) => atob(s);
56
- const CLIENT_ID = decode('OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl');
57
- const AUTHORIZE_URL = 'https://claude.ai/oauth/authorize';
58
- const TOKEN_URL = 'https://console.anthropic.com/v1/oauth/token';
59
- const REDIRECT_URI = 'https://console.anthropic.com/oauth/code/callback';
60
- const SCOPES = 'org:create_api_key user:profile user:inference';
61
-
62
- async function generatePKCE(): Promise<{ verifier: string; challenge: string }> {
63
- const array = new Uint8Array(32);
64
- crypto.getRandomValues(array);
65
- const verifier = btoa(String.fromCharCode(...array))
66
- .replace(/\+/g, '-')
67
- .replace(/\//g, '_')
68
- .replace(/=+$/, '');
69
-
70
- const encoder = new TextEncoder();
71
- const data = encoder.encode(verifier);
72
- const hash = await crypto.subtle.digest('SHA-256', data);
73
- const challenge = btoa(String.fromCharCode(...new Uint8Array(hash)))
74
- .replace(/\+/g, '-')
75
- .replace(/\//g, '_')
76
- .replace(/=+$/, '');
77
-
78
- return { verifier, challenge };
79
- }
80
-
81
- async function loginAnthropic(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
82
- const { verifier, challenge } = await generatePKCE();
83
-
84
- const authParams = new URLSearchParams({
85
- code: 'true',
86
- client_id: CLIENT_ID,
87
- response_type: 'code',
88
- redirect_uri: REDIRECT_URI,
89
- scope: SCOPES,
90
- code_challenge: challenge,
91
- code_challenge_method: 'S256',
92
- state: verifier
93
- });
94
-
95
- callbacks.onAuth({ url: `${AUTHORIZE_URL}?${authParams.toString()}` });
96
-
97
- const authCode = await callbacks.onPrompt({ message: 'Paste the authorization code:' });
98
- const [code, state] = authCode.split('#');
99
-
100
- const tokenResponse = await fetch(TOKEN_URL, {
101
- method: 'POST',
102
- headers: { 'Content-Type': 'application/json' },
103
- body: JSON.stringify({
104
- grant_type: 'authorization_code',
105
- client_id: CLIENT_ID,
106
- code,
107
- state,
108
- redirect_uri: REDIRECT_URI,
109
- code_verifier: verifier
110
- })
111
- });
112
-
113
- if (!tokenResponse.ok) {
114
- throw new Error(`Token exchange failed: ${await tokenResponse.text()}`);
115
- }
116
-
117
- const data = (await tokenResponse.json()) as {
118
- access_token: string;
119
- refresh_token: string;
120
- expires_in: number;
121
- };
122
-
123
- return {
124
- refresh: data.refresh_token,
125
- access: data.access_token,
126
- expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000
127
- };
128
- }
129
-
130
- async function refreshAnthropicToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
131
- const response = await fetch(TOKEN_URL, {
132
- method: 'POST',
133
- headers: { 'Content-Type': 'application/json' },
134
- body: JSON.stringify({
135
- grant_type: 'refresh_token',
136
- client_id: CLIENT_ID,
137
- refresh_token: credentials.refresh
138
- })
139
- });
140
-
141
- if (!response.ok) {
142
- throw new Error(`Token refresh failed: ${await response.text()}`);
143
- }
144
-
145
- const data = (await response.json()) as {
146
- access_token: string;
147
- refresh_token: string;
148
- expires_in: number;
149
- };
150
-
151
- return {
152
- refresh: data.refresh_token,
153
- access: data.access_token,
154
- expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000
155
- };
156
- }
157
-
158
- // =============================================================================
159
- // Streaming Implementation (simplified from packages/ai/src/providers/anthropic.ts)
160
- // =============================================================================
161
-
162
- // Claude Code tool names for OAuth stealth mode
163
- const claudeCodeTools = [
164
- 'Read',
165
- 'Write',
166
- 'Edit',
167
- 'Bash',
168
- 'Grep',
169
- 'Glob',
170
- 'AskUserQuestion',
171
- 'TodoWrite',
172
- 'WebFetch',
173
- 'WebSearch'
174
- ];
175
- const ccToolLookup = new Map(claudeCodeTools.map((t) => [t.toLowerCase(), t]));
176
- const toClaudeCodeName = (name: string) => ccToolLookup.get(name.toLowerCase()) ?? name;
177
- const fromClaudeCodeName = (name: string, tools?: Tool[]) => {
178
- const lowerName = name.toLowerCase();
179
- const matched = tools?.find((t) => t.name.toLowerCase() === lowerName);
180
- return matched?.name ?? name;
181
- };
182
-
183
- function isOAuthToken(apiKey: string): boolean {
184
- return apiKey.includes('sk-ant-oat');
185
- }
186
-
187
- function sanitizeSurrogates(text: string): string {
188
- return text.replace(/[\uD800-\uDFFF]/g, '\uFFFD');
189
- }
190
-
191
- function convertContentBlocks(
192
- content: (TextContent | ImageContent)[]
193
- ): string | Array<{ type: 'text'; text: string } | { type: 'image'; source: any }> {
194
- const hasImages = content.some((c) => c.type === 'image');
195
- if (!hasImages) {
196
- return sanitizeSurrogates(content.map((c) => (c as TextContent).text).join('\n'));
197
- }
198
-
199
- const blocks = content.map((block) => {
200
- if (block.type === 'text') {
201
- return { type: 'text' as const, text: sanitizeSurrogates(block.text) };
202
- }
203
- return {
204
- type: 'image' as const,
205
- source: {
206
- type: 'base64' as const,
207
- media_type: block.mimeType,
208
- data: block.data
209
- }
210
- };
211
- });
212
-
213
- if (!blocks.some((b) => b.type === 'text')) {
214
- blocks.unshift({ type: 'text' as const, text: '(see attached image)' });
215
- }
216
-
217
- return blocks;
218
- }
219
-
220
- function convertMessages(messages: Message[], isOAuth: boolean, _tools?: Tool[]): any[] {
221
- const params: any[] = [];
222
-
223
- for (let i = 0; i < messages.length; i++) {
224
- const msg = messages[i];
225
-
226
- if (msg.role === 'user') {
227
- if (typeof msg.content === 'string') {
228
- if (msg.content.trim()) {
229
- params.push({ role: 'user', content: sanitizeSurrogates(msg.content) });
230
- }
231
- } else {
232
- const blocks: ContentBlockParam[] = msg.content.map((item) =>
233
- item.type === 'text'
234
- ? { type: 'text' as const, text: sanitizeSurrogates(item.text) }
235
- : {
236
- type: 'image' as const,
237
- source: {
238
- type: 'base64' as const,
239
- media_type: item.mimeType as any,
240
- data: item.data
241
- }
242
- }
243
- );
244
- if (blocks.length > 0) {
245
- params.push({ role: 'user', content: blocks });
246
- }
247
- }
248
- } else if (msg.role === 'assistant') {
249
- const blocks: ContentBlockParam[] = [];
250
- for (const block of msg.content) {
251
- if (block.type === 'text' && block.text.trim()) {
252
- blocks.push({ type: 'text', text: sanitizeSurrogates(block.text) });
253
- } else if (block.type === 'thinking' && block.thinking.trim()) {
254
- if ((block as ThinkingContent).thinkingSignature) {
255
- blocks.push({
256
- type: 'thinking' as any,
257
- thinking: sanitizeSurrogates(block.thinking),
258
- signature: (block as ThinkingContent).thinkingSignature!
259
- });
260
- } else {
261
- blocks.push({ type: 'text', text: sanitizeSurrogates(block.thinking) });
262
- }
263
- } else if (block.type === 'toolCall') {
264
- blocks.push({
265
- type: 'tool_use',
266
- id: block.id,
267
- name: isOAuth ? toClaudeCodeName(block.name) : block.name,
268
- input: block.arguments
269
- });
270
- }
271
- }
272
- if (blocks.length > 0) {
273
- params.push({ role: 'assistant', content: blocks });
274
- }
275
- } else if (msg.role === 'toolResult') {
276
- const toolResults: any[] = [];
277
- toolResults.push({
278
- type: 'tool_result',
279
- tool_use_id: msg.toolCallId,
280
- content: convertContentBlocks(msg.content),
281
- is_error: msg.isError
282
- });
283
-
284
- let j = i + 1;
285
- while (j < messages.length && messages[j].role === 'toolResult') {
286
- const nextMsg = messages[j] as ToolResultMessage;
287
- toolResults.push({
288
- type: 'tool_result',
289
- tool_use_id: nextMsg.toolCallId,
290
- content: convertContentBlocks(nextMsg.content),
291
- is_error: nextMsg.isError
292
- });
293
- j++;
294
- }
295
- i = j - 1;
296
- params.push({ role: 'user', content: toolResults });
297
- }
298
- }
299
-
300
- // Add cache control to last user message
301
- if (params.length > 0) {
302
- const last = params[params.length - 1];
303
- if (last.role === 'user' && Array.isArray(last.content)) {
304
- const lastBlock = last.content[last.content.length - 1];
305
- if (lastBlock) {
306
- lastBlock.cache_control = { type: 'ephemeral' };
307
- }
308
- }
309
- }
310
-
311
- return params;
312
- }
313
-
314
- function convertTools(tools: Tool[], isOAuth: boolean): any[] {
315
- return tools.map((tool) => ({
316
- name: isOAuth ? toClaudeCodeName(tool.name) : tool.name,
317
- description: tool.description,
318
- input_schema: {
319
- type: 'object',
320
- properties: (tool.parameters as any).properties || {},
321
- required: (tool.parameters as any).required || []
322
- }
323
- }));
324
- }
325
-
326
- function mapStopReason(reason: string): StopReason {
327
- switch (reason) {
328
- case 'end_turn':
329
- case 'pause_turn':
330
- case 'stop_sequence':
331
- return 'stop';
332
- case 'max_tokens':
333
- return 'length';
334
- case 'tool_use':
335
- return 'toolUse';
336
- default:
337
- return 'error';
338
- }
339
- }
340
-
341
- function streamCustomAnthropic(
342
- model: Model<Api>,
343
- context: Context,
344
- options?: SimpleStreamOptions
345
- ): AssistantMessageEventStream {
346
- const stream = createAssistantMessageEventStream();
347
-
348
- (async () => {
349
- const output: AssistantMessage = {
350
- role: 'assistant',
351
- content: [],
352
- api: model.api,
353
- provider: model.provider,
354
- model: model.id,
355
- usage: {
356
- input: 0,
357
- output: 0,
358
- cacheRead: 0,
359
- cacheWrite: 0,
360
- totalTokens: 0,
361
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
362
- },
363
- stopReason: 'stop',
364
- timestamp: Date.now()
365
- };
366
-
367
- try {
368
- const apiKey = options?.apiKey ?? '';
369
- const isOAuth = isOAuthToken(apiKey);
370
-
371
- // Configure client based on auth type
372
- const betaFeatures = [
373
- 'fine-grained-tool-streaming-2025-05-14',
374
- 'interleaved-thinking-2025-05-14'
375
- ];
376
- const clientOptions: any = {
377
- baseURL: model.baseUrl,
378
- dangerouslyAllowBrowser: true
379
- };
380
-
381
- if (isOAuth) {
382
- clientOptions.apiKey = null;
383
- clientOptions.authToken = apiKey;
384
- clientOptions.defaultHeaders = {
385
- accept: 'application/json',
386
- 'anthropic-dangerous-direct-browser-access': 'true',
387
- 'anthropic-beta': `claude-code-20250219,oauth-2025-04-20,${betaFeatures.join(',')}`,
388
- 'user-agent': 'claude-cli/2.1.2 (external, cli)',
389
- 'x-app': 'cli'
390
- };
391
- } else {
392
- clientOptions.apiKey = apiKey;
393
- clientOptions.defaultHeaders = {
394
- accept: 'application/json',
395
- 'anthropic-dangerous-direct-browser-access': 'true',
396
- 'anthropic-beta': betaFeatures.join(',')
397
- };
398
- }
399
-
400
- const client = new Anthropic(clientOptions);
401
-
402
- // Build request params
403
- const params: MessageCreateParamsStreaming = {
404
- model: model.id,
405
- messages: convertMessages(context.messages, isOAuth, context.tools),
406
- max_tokens: options?.maxTokens || Math.floor(model.maxTokens / 3),
407
- stream: true
408
- };
409
-
410
- // System prompt with Claude Code identity for OAuth
411
- if (isOAuth) {
412
- params.system = [
413
- {
414
- type: 'text',
415
- text: "You are Claude Code, Anthropic's official CLI for Claude.",
416
- cache_control: { type: 'ephemeral' }
417
- }
418
- ];
419
- if (context.systemPrompt) {
420
- params.system.push({
421
- type: 'text',
422
- text: sanitizeSurrogates(context.systemPrompt),
423
- cache_control: { type: 'ephemeral' }
424
- });
425
- }
426
- } else if (context.systemPrompt) {
427
- params.system = [
428
- {
429
- type: 'text',
430
- text: sanitizeSurrogates(context.systemPrompt),
431
- cache_control: { type: 'ephemeral' }
432
- }
433
- ];
434
- }
435
-
436
- if (context.tools) {
437
- params.tools = convertTools(context.tools, isOAuth);
438
- }
439
-
440
- // Handle thinking/reasoning
441
- if (options?.reasoning && model.reasoning) {
442
- const defaultBudgets: Record<string, number> = {
443
- minimal: 1024,
444
- low: 4096,
445
- medium: 10240,
446
- high: 20480
447
- };
448
- const customBudget =
449
- options.thinkingBudgets?.[options.reasoning as keyof typeof options.thinkingBudgets];
450
- params.thinking = {
451
- type: 'enabled',
452
- budget_tokens: customBudget ?? defaultBudgets[options.reasoning] ?? 10240
453
- };
454
- }
455
-
456
- const anthropicStream = client.messages.stream({ ...params }, { signal: options?.signal });
457
- stream.push({ type: 'start', partial: output });
458
-
459
- type Block = (ThinkingContent | TextContent | (ToolCall & { partialJson: string })) & {
460
- index: number;
461
- };
462
- const blocks = output.content as Block[];
463
-
464
- for await (const event of anthropicStream) {
465
- if (event.type === 'message_start') {
466
- output.usage.input = event.message.usage.input_tokens || 0;
467
- output.usage.output = event.message.usage.output_tokens || 0;
468
- output.usage.cacheRead = (event.message.usage as any).cache_read_input_tokens || 0;
469
- output.usage.cacheWrite = (event.message.usage as any).cache_creation_input_tokens || 0;
470
- output.usage.totalTokens =
471
- output.usage.input +
472
- output.usage.output +
473
- output.usage.cacheRead +
474
- output.usage.cacheWrite;
475
- calculateCost(model, output.usage);
476
- } else if (event.type === 'content_block_start') {
477
- if (event.content_block.type === 'text') {
478
- output.content.push({ type: 'text', text: '', index: event.index } as any);
479
- stream.push({
480
- type: 'text_start',
481
- contentIndex: output.content.length - 1,
482
- partial: output
483
- });
484
- } else if (event.content_block.type === 'thinking') {
485
- output.content.push({
486
- type: 'thinking',
487
- thinking: '',
488
- thinkingSignature: '',
489
- index: event.index
490
- } as any);
491
- stream.push({
492
- type: 'thinking_start',
493
- contentIndex: output.content.length - 1,
494
- partial: output
495
- });
496
- } else if (event.content_block.type === 'tool_use') {
497
- output.content.push({
498
- type: 'toolCall',
499
- id: event.content_block.id,
500
- name: isOAuth
501
- ? fromClaudeCodeName(event.content_block.name, context.tools)
502
- : event.content_block.name,
503
- arguments: {},
504
- partialJson: '',
505
- index: event.index
506
- } as any);
507
- stream.push({
508
- type: 'toolcall_start',
509
- contentIndex: output.content.length - 1,
510
- partial: output
511
- });
512
- }
513
- } else if (event.type === 'content_block_delta') {
514
- const index = blocks.findIndex((b) => b.index === event.index);
515
- const block = blocks[index];
516
- if (!block) continue;
517
-
518
- if (event.delta.type === 'text_delta' && block.type === 'text') {
519
- block.text += event.delta.text;
520
- stream.push({
521
- type: 'text_delta',
522
- contentIndex: index,
523
- delta: event.delta.text,
524
- partial: output
525
- });
526
- } else if (event.delta.type === 'thinking_delta' && block.type === 'thinking') {
527
- block.thinking += event.delta.thinking;
528
- stream.push({
529
- type: 'thinking_delta',
530
- contentIndex: index,
531
- delta: event.delta.thinking,
532
- partial: output
533
- });
534
- } else if (event.delta.type === 'input_json_delta' && block.type === 'toolCall') {
535
- (block as any).partialJson += event.delta.partial_json;
536
- try {
537
- block.arguments = JSON.parse((block as any).partialJson);
538
- } catch {}
539
- stream.push({
540
- type: 'toolcall_delta',
541
- contentIndex: index,
542
- delta: event.delta.partial_json,
543
- partial: output
544
- });
545
- } else if (event.delta.type === 'signature_delta' && block.type === 'thinking') {
546
- block.thinkingSignature =
547
- (block.thinkingSignature || '') + (event.delta as any).signature;
548
- }
549
- } else if (event.type === 'content_block_stop') {
550
- const index = blocks.findIndex((b) => b.index === event.index);
551
- const block = blocks[index];
552
- if (!block) continue;
553
-
554
- delete (block as any).index;
555
- if (block.type === 'text') {
556
- stream.push({
557
- type: 'text_end',
558
- contentIndex: index,
559
- content: block.text,
560
- partial: output
561
- });
562
- } else if (block.type === 'thinking') {
563
- stream.push({
564
- type: 'thinking_end',
565
- contentIndex: index,
566
- content: block.thinking,
567
- partial: output
568
- });
569
- } else if (block.type === 'toolCall') {
570
- try {
571
- block.arguments = JSON.parse((block as any).partialJson);
572
- } catch {}
573
- delete (block as any).partialJson;
574
- stream.push({
575
- type: 'toolcall_end',
576
- contentIndex: index,
577
- toolCall: block,
578
- partial: output
579
- });
580
- }
581
- } else if (event.type === 'message_delta') {
582
- if ((event.delta as any).stop_reason) {
583
- output.stopReason = mapStopReason((event.delta as any).stop_reason);
584
- }
585
- output.usage.input = (event.usage as any).input_tokens || 0;
586
- output.usage.output = (event.usage as any).output_tokens || 0;
587
- output.usage.cacheRead = (event.usage as any).cache_read_input_tokens || 0;
588
- output.usage.cacheWrite = (event.usage as any).cache_creation_input_tokens || 0;
589
- output.usage.totalTokens =
590
- output.usage.input +
591
- output.usage.output +
592
- output.usage.cacheRead +
593
- output.usage.cacheWrite;
594
- calculateCost(model, output.usage);
595
- }
596
- }
597
-
598
- if (options?.signal?.aborted) {
599
- throw new Error('Request was aborted');
600
- }
601
-
602
- stream.push({
603
- type: 'done',
604
- reason: output.stopReason as 'stop' | 'length' | 'toolUse',
605
- message: output
606
- });
607
- stream.end();
608
- } catch (error) {
609
- for (const block of output.content) delete (block as any).index;
610
- output.stopReason = options?.signal?.aborted ? 'aborted' : 'error';
611
- output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
612
- stream.push({ type: 'error', reason: output.stopReason, error: output });
613
- stream.end();
614
- }
615
- })();
616
-
617
- return stream;
618
- }
619
-
620
- // =============================================================================
621
- // Extension Entry Point
622
- // =============================================================================
623
-
624
- export default function (shortcut: ExtensionAPI) {
625
- shortcut.registerProvider('custom-anthropic', {
626
- baseUrl: 'https://api.anthropic.com',
627
- apiKey: 'CUSTOM_ANTHROPIC_API_KEY',
628
- api: 'custom-anthropic-api',
629
-
630
- models: [
631
- {
632
- id: 'claude-opus-4-5',
633
- name: 'Claude Opus 4.5 (Custom)',
634
- reasoning: true,
635
- input: ['text', 'image'],
636
- cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
637
- contextWindow: 200000,
638
- maxTokens: 64000
639
- },
640
- {
641
- id: 'claude-sonnet-4-5',
642
- name: 'Claude Sonnet 4.5 (Custom)',
643
- reasoning: true,
644
- input: ['text', 'image'],
645
- cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
646
- contextWindow: 200000,
647
- maxTokens: 64000
648
- }
649
- ],
650
-
651
- oauth: {
652
- name: 'Custom Anthropic (Claude Pro/Max)',
653
- login: loginAnthropic,
654
- refreshToken: refreshAnthropicToken,
655
- getApiKey: (cred) => cred.access
656
- },
657
-
658
- streamSimple: streamCustomAnthropic
659
- });
660
- }
1
+ /**
2
+ * Custom Provider Example
3
+ *
4
+ * Demonstrates registering a custom provider with:
5
+ * - Custom API identifier ("custom-anthropic-api")
6
+ * - Custom streamSimple implementation
7
+ * - OAuth support for /login
8
+ * - API key support via environment variable
9
+ * - Two model definitions
10
+ *
11
+ * Usage:
12
+ * # First install dependencies
13
+ * cd packages/coding-agent/examples/extensions/custom-provider && npm install
14
+ *
15
+ * # With OAuth (run /login custom-anthropic first)
16
+ * shortcut -e ./packages/coding-agent/examples/extensions/custom-provider
17
+ *
18
+ * # With API key
19
+ * CUSTOM_ANTHROPIC_API_KEY=sk-ant-... shortcut -e ./packages/coding-agent/examples/extensions/custom-provider
20
+ *
21
+ * Then use /model to select custom-anthropic/claude-sonnet-4-5
22
+ */
23
+
24
+ import Anthropic from '@anthropic-ai/sdk';
25
+ import type {
26
+ ContentBlockParam,
27
+ MessageCreateParamsStreaming
28
+ } from '@anthropic-ai/sdk/resources/messages.js';
29
+ import type { ExtensionAPI } from 'shortcutxl';
30
+ import {
31
+ calculateCost,
32
+ createAssistantMessageEventStream,
33
+ type Api,
34
+ type AssistantMessage,
35
+ type AssistantMessageEventStream,
36
+ type Context,
37
+ type ImageContent,
38
+ type Message,
39
+ type Model,
40
+ type OAuthCredentials,
41
+ type OAuthLoginCallbacks,
42
+ type SimpleStreamOptions,
43
+ type StopReason,
44
+ type TextContent,
45
+ type ThinkingContent,
46
+ type Tool,
47
+ type ToolCall,
48
+ type ToolResultMessage
49
+ } from 'shortcutxl';
50
+
51
+ // =============================================================================
52
+ // OAuth Implementation (copied from packages/ai/src/utils/oauth/anthropic.ts)
53
+ // =============================================================================
54
+
55
+ const decode = (s: string) => atob(s);
56
+ const CLIENT_ID = decode('OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl');
57
+ const AUTHORIZE_URL = 'https://claude.ai/oauth/authorize';
58
+ const TOKEN_URL = 'https://console.anthropic.com/v1/oauth/token';
59
+ const REDIRECT_URI = 'https://console.anthropic.com/oauth/code/callback';
60
+ const SCOPES = 'org:create_api_key user:profile user:inference';
61
+
62
+ async function generatePKCE(): Promise<{ verifier: string; challenge: string }> {
63
+ const array = new Uint8Array(32);
64
+ crypto.getRandomValues(array);
65
+ const verifier = btoa(String.fromCharCode(...array))
66
+ .replace(/\+/g, '-')
67
+ .replace(/\//g, '_')
68
+ .replace(/=+$/, '');
69
+
70
+ const encoder = new TextEncoder();
71
+ const data = encoder.encode(verifier);
72
+ const hash = await crypto.subtle.digest('SHA-256', data);
73
+ const challenge = btoa(String.fromCharCode(...new Uint8Array(hash)))
74
+ .replace(/\+/g, '-')
75
+ .replace(/\//g, '_')
76
+ .replace(/=+$/, '');
77
+
78
+ return { verifier, challenge };
79
+ }
80
+
81
+ async function loginAnthropic(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
82
+ const { verifier, challenge } = await generatePKCE();
83
+
84
+ const authParams = new URLSearchParams({
85
+ code: 'true',
86
+ client_id: CLIENT_ID,
87
+ response_type: 'code',
88
+ redirect_uri: REDIRECT_URI,
89
+ scope: SCOPES,
90
+ code_challenge: challenge,
91
+ code_challenge_method: 'S256',
92
+ state: verifier
93
+ });
94
+
95
+ callbacks.onAuth({ url: `${AUTHORIZE_URL}?${authParams.toString()}` });
96
+
97
+ const authCode = await callbacks.onPrompt({ message: 'Paste the authorization code:' });
98
+ const [code, state] = authCode.split('#');
99
+
100
+ const tokenResponse = await fetch(TOKEN_URL, {
101
+ method: 'POST',
102
+ headers: { 'Content-Type': 'application/json' },
103
+ body: JSON.stringify({
104
+ grant_type: 'authorization_code',
105
+ client_id: CLIENT_ID,
106
+ code,
107
+ state,
108
+ redirect_uri: REDIRECT_URI,
109
+ code_verifier: verifier
110
+ })
111
+ });
112
+
113
+ if (!tokenResponse.ok) {
114
+ throw new Error(`Token exchange failed: ${await tokenResponse.text()}`);
115
+ }
116
+
117
+ const data = (await tokenResponse.json()) as {
118
+ access_token: string;
119
+ refresh_token: string;
120
+ expires_in: number;
121
+ };
122
+
123
+ return {
124
+ refresh: data.refresh_token,
125
+ access: data.access_token,
126
+ expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000
127
+ };
128
+ }
129
+
130
+ async function refreshAnthropicToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
131
+ const response = await fetch(TOKEN_URL, {
132
+ method: 'POST',
133
+ headers: { 'Content-Type': 'application/json' },
134
+ body: JSON.stringify({
135
+ grant_type: 'refresh_token',
136
+ client_id: CLIENT_ID,
137
+ refresh_token: credentials.refresh
138
+ })
139
+ });
140
+
141
+ if (!response.ok) {
142
+ throw new Error(`Token refresh failed: ${await response.text()}`);
143
+ }
144
+
145
+ const data = (await response.json()) as {
146
+ access_token: string;
147
+ refresh_token: string;
148
+ expires_in: number;
149
+ };
150
+
151
+ return {
152
+ refresh: data.refresh_token,
153
+ access: data.access_token,
154
+ expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000
155
+ };
156
+ }
157
+
158
+ // =============================================================================
159
+ // Streaming Implementation (simplified from packages/ai/src/providers/anthropic.ts)
160
+ // =============================================================================
161
+
162
+ // Claude Code tool names for OAuth stealth mode
163
+ const claudeCodeTools = [
164
+ 'Read',
165
+ 'Write',
166
+ 'Edit',
167
+ 'Bash',
168
+ 'Grep',
169
+ 'Glob',
170
+ 'AskUserQuestion',
171
+ 'TodoWrite',
172
+ 'WebFetch',
173
+ 'WebSearch'
174
+ ];
175
+ const ccToolLookup = new Map(claudeCodeTools.map((t) => [t.toLowerCase(), t]));
176
+ const toClaudeCodeName = (name: string) => ccToolLookup.get(name.toLowerCase()) ?? name;
177
+ const fromClaudeCodeName = (name: string, tools?: Tool[]) => {
178
+ const lowerName = name.toLowerCase();
179
+ const matched = tools?.find((t) => t.name.toLowerCase() === lowerName);
180
+ return matched?.name ?? name;
181
+ };
182
+
183
+ function isOAuthToken(apiKey: string): boolean {
184
+ return apiKey.includes('sk-ant-oat');
185
+ }
186
+
187
+ function sanitizeSurrogates(text: string): string {
188
+ return text.replace(/[\uD800-\uDFFF]/g, '\uFFFD');
189
+ }
190
+
191
+ function convertContentBlocks(
192
+ content: (TextContent | ImageContent)[]
193
+ ): string | Array<{ type: 'text'; text: string } | { type: 'image'; source: any }> {
194
+ const hasImages = content.some((c) => c.type === 'image');
195
+ if (!hasImages) {
196
+ return sanitizeSurrogates(content.map((c) => (c as TextContent).text).join('\n'));
197
+ }
198
+
199
+ const blocks = content.map((block) => {
200
+ if (block.type === 'text') {
201
+ return { type: 'text' as const, text: sanitizeSurrogates(block.text) };
202
+ }
203
+ return {
204
+ type: 'image' as const,
205
+ source: {
206
+ type: 'base64' as const,
207
+ media_type: block.mimeType,
208
+ data: block.data
209
+ }
210
+ };
211
+ });
212
+
213
+ if (!blocks.some((b) => b.type === 'text')) {
214
+ blocks.unshift({ type: 'text' as const, text: '(see attached image)' });
215
+ }
216
+
217
+ return blocks;
218
+ }
219
+
220
+ function convertMessages(messages: Message[], isOAuth: boolean, _tools?: Tool[]): any[] {
221
+ const params: any[] = [];
222
+
223
+ for (let i = 0; i < messages.length; i++) {
224
+ const msg = messages[i];
225
+
226
+ if (msg.role === 'user') {
227
+ if (typeof msg.content === 'string') {
228
+ if (msg.content.trim()) {
229
+ params.push({ role: 'user', content: sanitizeSurrogates(msg.content) });
230
+ }
231
+ } else {
232
+ const blocks: ContentBlockParam[] = msg.content.map((item) =>
233
+ item.type === 'text'
234
+ ? { type: 'text' as const, text: sanitizeSurrogates(item.text) }
235
+ : {
236
+ type: 'image' as const,
237
+ source: {
238
+ type: 'base64' as const,
239
+ media_type: item.mimeType as any,
240
+ data: item.data
241
+ }
242
+ }
243
+ );
244
+ if (blocks.length > 0) {
245
+ params.push({ role: 'user', content: blocks });
246
+ }
247
+ }
248
+ } else if (msg.role === 'assistant') {
249
+ const blocks: ContentBlockParam[] = [];
250
+ for (const block of msg.content) {
251
+ if (block.type === 'text' && block.text.trim()) {
252
+ blocks.push({ type: 'text', text: sanitizeSurrogates(block.text) });
253
+ } else if (block.type === 'thinking' && block.thinking.trim()) {
254
+ if ((block as ThinkingContent).thinkingSignature) {
255
+ blocks.push({
256
+ type: 'thinking' as any,
257
+ thinking: sanitizeSurrogates(block.thinking),
258
+ signature: (block as ThinkingContent).thinkingSignature!
259
+ });
260
+ } else {
261
+ blocks.push({ type: 'text', text: sanitizeSurrogates(block.thinking) });
262
+ }
263
+ } else if (block.type === 'toolCall') {
264
+ blocks.push({
265
+ type: 'tool_use',
266
+ id: block.id,
267
+ name: isOAuth ? toClaudeCodeName(block.name) : block.name,
268
+ input: block.arguments
269
+ });
270
+ }
271
+ }
272
+ if (blocks.length > 0) {
273
+ params.push({ role: 'assistant', content: blocks });
274
+ }
275
+ } else if (msg.role === 'toolResult') {
276
+ const toolResults: any[] = [];
277
+ toolResults.push({
278
+ type: 'tool_result',
279
+ tool_use_id: msg.toolCallId,
280
+ content: convertContentBlocks(msg.content),
281
+ is_error: msg.isError
282
+ });
283
+
284
+ let j = i + 1;
285
+ while (j < messages.length && messages[j].role === 'toolResult') {
286
+ const nextMsg = messages[j] as ToolResultMessage;
287
+ toolResults.push({
288
+ type: 'tool_result',
289
+ tool_use_id: nextMsg.toolCallId,
290
+ content: convertContentBlocks(nextMsg.content),
291
+ is_error: nextMsg.isError
292
+ });
293
+ j++;
294
+ }
295
+ i = j - 1;
296
+ params.push({ role: 'user', content: toolResults });
297
+ }
298
+ }
299
+
300
+ // Add cache control to last user message
301
+ if (params.length > 0) {
302
+ const last = params[params.length - 1];
303
+ if (last.role === 'user' && Array.isArray(last.content)) {
304
+ const lastBlock = last.content[last.content.length - 1];
305
+ if (lastBlock) {
306
+ lastBlock.cache_control = { type: 'ephemeral' };
307
+ }
308
+ }
309
+ }
310
+
311
+ return params;
312
+ }
313
+
314
+ function convertTools(tools: Tool[], isOAuth: boolean): any[] {
315
+ return tools.map((tool) => ({
316
+ name: isOAuth ? toClaudeCodeName(tool.name) : tool.name,
317
+ description: tool.description,
318
+ input_schema: {
319
+ type: 'object',
320
+ properties: (tool.parameters as any).properties || {},
321
+ required: (tool.parameters as any).required || []
322
+ }
323
+ }));
324
+ }
325
+
326
+ function mapStopReason(reason: string): StopReason {
327
+ switch (reason) {
328
+ case 'end_turn':
329
+ case 'pause_turn':
330
+ case 'stop_sequence':
331
+ return 'stop';
332
+ case 'max_tokens':
333
+ return 'length';
334
+ case 'tool_use':
335
+ return 'toolUse';
336
+ default:
337
+ return 'error';
338
+ }
339
+ }
340
+
341
+ function streamCustomAnthropic(
342
+ model: Model<Api>,
343
+ context: Context,
344
+ options?: SimpleStreamOptions
345
+ ): AssistantMessageEventStream {
346
+ const stream = createAssistantMessageEventStream();
347
+
348
+ (async () => {
349
+ const output: AssistantMessage = {
350
+ role: 'assistant',
351
+ content: [],
352
+ api: model.api,
353
+ provider: model.provider,
354
+ model: model.id,
355
+ usage: {
356
+ input: 0,
357
+ output: 0,
358
+ cacheRead: 0,
359
+ cacheWrite: 0,
360
+ totalTokens: 0,
361
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
362
+ },
363
+ stopReason: 'stop',
364
+ timestamp: Date.now()
365
+ };
366
+
367
+ try {
368
+ const apiKey = options?.apiKey ?? '';
369
+ const isOAuth = isOAuthToken(apiKey);
370
+
371
+ // Configure client based on auth type
372
+ const betaFeatures = [
373
+ 'fine-grained-tool-streaming-2025-05-14',
374
+ 'interleaved-thinking-2025-05-14'
375
+ ];
376
+ const clientOptions: any = {
377
+ baseURL: model.baseUrl,
378
+ dangerouslyAllowBrowser: true
379
+ };
380
+
381
+ if (isOAuth) {
382
+ clientOptions.apiKey = null;
383
+ clientOptions.authToken = apiKey;
384
+ clientOptions.defaultHeaders = {
385
+ accept: 'application/json',
386
+ 'anthropic-dangerous-direct-browser-access': 'true',
387
+ 'anthropic-beta': `claude-code-20250219,oauth-2025-04-20,${betaFeatures.join(',')}`,
388
+ 'user-agent': 'claude-cli/2.1.2 (external, cli)',
389
+ 'x-app': 'cli'
390
+ };
391
+ } else {
392
+ clientOptions.apiKey = apiKey;
393
+ clientOptions.defaultHeaders = {
394
+ accept: 'application/json',
395
+ 'anthropic-dangerous-direct-browser-access': 'true',
396
+ 'anthropic-beta': betaFeatures.join(',')
397
+ };
398
+ }
399
+
400
+ const client = new Anthropic(clientOptions);
401
+
402
+ // Build request params
403
+ const params: MessageCreateParamsStreaming = {
404
+ model: model.id,
405
+ messages: convertMessages(context.messages, isOAuth, context.tools),
406
+ max_tokens: options?.maxTokens || Math.floor(model.maxTokens / 3),
407
+ stream: true
408
+ };
409
+
410
+ // System prompt with Claude Code identity for OAuth
411
+ if (isOAuth) {
412
+ params.system = [
413
+ {
414
+ type: 'text',
415
+ text: "You are Claude Code, Anthropic's official CLI for Claude.",
416
+ cache_control: { type: 'ephemeral' }
417
+ }
418
+ ];
419
+ if (context.systemPrompt) {
420
+ params.system.push({
421
+ type: 'text',
422
+ text: sanitizeSurrogates(context.systemPrompt),
423
+ cache_control: { type: 'ephemeral' }
424
+ });
425
+ }
426
+ } else if (context.systemPrompt) {
427
+ params.system = [
428
+ {
429
+ type: 'text',
430
+ text: sanitizeSurrogates(context.systemPrompt),
431
+ cache_control: { type: 'ephemeral' }
432
+ }
433
+ ];
434
+ }
435
+
436
+ if (context.tools) {
437
+ params.tools = convertTools(context.tools, isOAuth);
438
+ }
439
+
440
+ // Handle thinking/reasoning
441
+ if (options?.reasoning && model.reasoning) {
442
+ const defaultBudgets: Record<string, number> = {
443
+ minimal: 1024,
444
+ low: 4096,
445
+ medium: 10240,
446
+ high: 20480
447
+ };
448
+ const customBudget =
449
+ options.thinkingBudgets?.[options.reasoning as keyof typeof options.thinkingBudgets];
450
+ params.thinking = {
451
+ type: 'enabled',
452
+ budget_tokens: customBudget ?? defaultBudgets[options.reasoning] ?? 10240
453
+ };
454
+ }
455
+
456
+ const anthropicStream = client.messages.stream({ ...params }, { signal: options?.signal });
457
+ stream.push({ type: 'start', partial: output });
458
+
459
+ type Block = (ThinkingContent | TextContent | (ToolCall & { partialJson: string })) & {
460
+ index: number;
461
+ };
462
+ const blocks = output.content as Block[];
463
+
464
+ for await (const event of anthropicStream) {
465
+ if (event.type === 'message_start') {
466
+ output.usage.input = event.message.usage.input_tokens || 0;
467
+ output.usage.output = event.message.usage.output_tokens || 0;
468
+ output.usage.cacheRead = (event.message.usage as any).cache_read_input_tokens || 0;
469
+ output.usage.cacheWrite = (event.message.usage as any).cache_creation_input_tokens || 0;
470
+ output.usage.totalTokens =
471
+ output.usage.input +
472
+ output.usage.output +
473
+ output.usage.cacheRead +
474
+ output.usage.cacheWrite;
475
+ calculateCost(model, output.usage);
476
+ } else if (event.type === 'content_block_start') {
477
+ if (event.content_block.type === 'text') {
478
+ output.content.push({ type: 'text', text: '', index: event.index } as any);
479
+ stream.push({
480
+ type: 'text_start',
481
+ contentIndex: output.content.length - 1,
482
+ partial: output
483
+ });
484
+ } else if (event.content_block.type === 'thinking') {
485
+ output.content.push({
486
+ type: 'thinking',
487
+ thinking: '',
488
+ thinkingSignature: '',
489
+ index: event.index
490
+ } as any);
491
+ stream.push({
492
+ type: 'thinking_start',
493
+ contentIndex: output.content.length - 1,
494
+ partial: output
495
+ });
496
+ } else if (event.content_block.type === 'tool_use') {
497
+ output.content.push({
498
+ type: 'toolCall',
499
+ id: event.content_block.id,
500
+ name: isOAuth
501
+ ? fromClaudeCodeName(event.content_block.name, context.tools)
502
+ : event.content_block.name,
503
+ arguments: {},
504
+ partialJson: '',
505
+ index: event.index
506
+ } as any);
507
+ stream.push({
508
+ type: 'toolcall_start',
509
+ contentIndex: output.content.length - 1,
510
+ partial: output
511
+ });
512
+ }
513
+ } else if (event.type === 'content_block_delta') {
514
+ const index = blocks.findIndex((b) => b.index === event.index);
515
+ const block = blocks[index];
516
+ if (!block) continue;
517
+
518
+ if (event.delta.type === 'text_delta' && block.type === 'text') {
519
+ block.text += event.delta.text;
520
+ stream.push({
521
+ type: 'text_delta',
522
+ contentIndex: index,
523
+ delta: event.delta.text,
524
+ partial: output
525
+ });
526
+ } else if (event.delta.type === 'thinking_delta' && block.type === 'thinking') {
527
+ block.thinking += event.delta.thinking;
528
+ stream.push({
529
+ type: 'thinking_delta',
530
+ contentIndex: index,
531
+ delta: event.delta.thinking,
532
+ partial: output
533
+ });
534
+ } else if (event.delta.type === 'input_json_delta' && block.type === 'toolCall') {
535
+ (block as any).partialJson += event.delta.partial_json;
536
+ try {
537
+ block.arguments = JSON.parse((block as any).partialJson);
538
+ } catch {}
539
+ stream.push({
540
+ type: 'toolcall_delta',
541
+ contentIndex: index,
542
+ delta: event.delta.partial_json,
543
+ partial: output
544
+ });
545
+ } else if (event.delta.type === 'signature_delta' && block.type === 'thinking') {
546
+ block.thinkingSignature =
547
+ (block.thinkingSignature || '') + (event.delta as any).signature;
548
+ }
549
+ } else if (event.type === 'content_block_stop') {
550
+ const index = blocks.findIndex((b) => b.index === event.index);
551
+ const block = blocks[index];
552
+ if (!block) continue;
553
+
554
+ delete (block as any).index;
555
+ if (block.type === 'text') {
556
+ stream.push({
557
+ type: 'text_end',
558
+ contentIndex: index,
559
+ content: block.text,
560
+ partial: output
561
+ });
562
+ } else if (block.type === 'thinking') {
563
+ stream.push({
564
+ type: 'thinking_end',
565
+ contentIndex: index,
566
+ content: block.thinking,
567
+ partial: output
568
+ });
569
+ } else if (block.type === 'toolCall') {
570
+ try {
571
+ block.arguments = JSON.parse((block as any).partialJson);
572
+ } catch {}
573
+ delete (block as any).partialJson;
574
+ stream.push({
575
+ type: 'toolcall_end',
576
+ contentIndex: index,
577
+ toolCall: block,
578
+ partial: output
579
+ });
580
+ }
581
+ } else if (event.type === 'message_delta') {
582
+ if ((event.delta as any).stop_reason) {
583
+ output.stopReason = mapStopReason((event.delta as any).stop_reason);
584
+ }
585
+ output.usage.input = (event.usage as any).input_tokens || 0;
586
+ output.usage.output = (event.usage as any).output_tokens || 0;
587
+ output.usage.cacheRead = (event.usage as any).cache_read_input_tokens || 0;
588
+ output.usage.cacheWrite = (event.usage as any).cache_creation_input_tokens || 0;
589
+ output.usage.totalTokens =
590
+ output.usage.input +
591
+ output.usage.output +
592
+ output.usage.cacheRead +
593
+ output.usage.cacheWrite;
594
+ calculateCost(model, output.usage);
595
+ }
596
+ }
597
+
598
+ if (options?.signal?.aborted) {
599
+ throw new Error('Request was aborted');
600
+ }
601
+
602
+ stream.push({
603
+ type: 'done',
604
+ reason: output.stopReason as 'stop' | 'length' | 'toolUse',
605
+ message: output
606
+ });
607
+ stream.end();
608
+ } catch (error) {
609
+ for (const block of output.content) delete (block as any).index;
610
+ output.stopReason = options?.signal?.aborted ? 'aborted' : 'error';
611
+ output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
612
+ stream.push({ type: 'error', reason: output.stopReason, error: output });
613
+ stream.end();
614
+ }
615
+ })();
616
+
617
+ return stream;
618
+ }
619
+
620
+ // =============================================================================
621
+ // Extension Entry Point
622
+ // =============================================================================
623
+
624
+ export default function (shortcut: ExtensionAPI) {
625
+ shortcut.registerProvider('custom-anthropic', {
626
+ baseUrl: 'https://api.anthropic.com',
627
+ apiKey: 'CUSTOM_ANTHROPIC_API_KEY',
628
+ api: 'custom-anthropic-api',
629
+
630
+ models: [
631
+ {
632
+ id: 'claude-opus-4-5',
633
+ name: 'Claude Opus 4.5 (Custom)',
634
+ reasoning: true,
635
+ input: ['text', 'image'],
636
+ cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
637
+ contextWindow: 200000,
638
+ maxTokens: 64000
639
+ },
640
+ {
641
+ id: 'claude-sonnet-4-5',
642
+ name: 'Claude Sonnet 4.5 (Custom)',
643
+ reasoning: true,
644
+ input: ['text', 'image'],
645
+ cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
646
+ contextWindow: 200000,
647
+ maxTokens: 64000
648
+ }
649
+ ],
650
+
651
+ oauth: {
652
+ name: 'Custom Anthropic (Claude Pro/Max)',
653
+ login: loginAnthropic,
654
+ refreshToken: refreshAnthropicToken,
655
+ getApiKey: (cred) => cred.access
656
+ },
657
+
658
+ streamSimple: streamCustomAnthropic
659
+ });
660
+ }