@ottocode/sdk 0.1.178 → 0.1.180
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/package.json +132 -130
- package/src/index.ts +18 -0
- package/src/providers/src/catalog.ts +114 -40
- package/src/providers/src/oauth-models.ts +7 -1
- package/src/tunnel/binary.ts +212 -0
- package/src/tunnel/index.ts +17 -0
- package/src/tunnel/qr.ts +20 -0
- package/src/tunnel/qrcode-terminal.d.ts +16 -0
- package/src/tunnel/tunnel.ts +231 -0
package/package.json
CHANGED
|
@@ -1,132 +1,134 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
2
|
+
"name": "@ottocode/sdk",
|
|
3
|
+
"version": "0.1.180",
|
|
4
|
+
"description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
|
|
5
|
+
"author": "nitishxyz",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/nitishxyz/otto#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/nitishxyz/otto.git",
|
|
11
|
+
"directory": "packages/sdk"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/nitishxyz/otto/issues"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "./src/index.ts",
|
|
18
|
+
"types": "./src/index.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"import": "./src/index.ts",
|
|
22
|
+
"types": "./src/index.ts"
|
|
23
|
+
},
|
|
24
|
+
"./browser": {
|
|
25
|
+
"import": "./src/browser.ts",
|
|
26
|
+
"types": "./src/browser.ts"
|
|
27
|
+
},
|
|
28
|
+
"./tools/builtin/fs": {
|
|
29
|
+
"import": "./src/core/src/tools/builtin/fs/index.ts",
|
|
30
|
+
"types": "./src/core/src/tools/builtin/fs/index.ts"
|
|
31
|
+
},
|
|
32
|
+
"./tools/builtin/git": {
|
|
33
|
+
"import": "./src/core/src/tools/builtin/git.ts",
|
|
34
|
+
"types": "./src/core/src/tools/builtin/git.ts"
|
|
35
|
+
},
|
|
36
|
+
"./tools/builtin/bash": {
|
|
37
|
+
"import": "./src/core/src/tools/builtin/bash.ts",
|
|
38
|
+
"types": "./src/core/src/tools/builtin/bash.ts"
|
|
39
|
+
},
|
|
40
|
+
"./tools/builtin/edit": {
|
|
41
|
+
"import": "./src/core/src/tools/builtin/edit.ts",
|
|
42
|
+
"types": "./src/core/src/tools/builtin/edit.ts"
|
|
43
|
+
},
|
|
44
|
+
"./tools/builtin/finish": {
|
|
45
|
+
"import": "./src/core/src/tools/builtin/finish.ts",
|
|
46
|
+
"types": "./src/core/src/tools/builtin/finish.ts"
|
|
47
|
+
},
|
|
48
|
+
"./tools/builtin/grep": {
|
|
49
|
+
"import": "./src/core/src/tools/builtin/grep.ts",
|
|
50
|
+
"types": "./src/core/src/tools/builtin/grep.ts"
|
|
51
|
+
},
|
|
52
|
+
"./tools/builtin/patch": {
|
|
53
|
+
"import": "./src/core/src/tools/builtin/patch.ts",
|
|
54
|
+
"types": "./src/core/src/tools/builtin/patch.ts"
|
|
55
|
+
},
|
|
56
|
+
"./tools/builtin/plan": {
|
|
57
|
+
"import": "./src/core/src/tools/builtin/plan.ts",
|
|
58
|
+
"types": "./src/core/src/tools/builtin/plan.ts"
|
|
59
|
+
},
|
|
60
|
+
"./tools/builtin/progress": {
|
|
61
|
+
"import": "./src/core/src/tools/builtin/progress.ts",
|
|
62
|
+
"types": "./src/core/src/tools/builtin/progress.ts"
|
|
63
|
+
},
|
|
64
|
+
"./tools/builtin/ripgrep": {
|
|
65
|
+
"import": "./src/core/src/tools/builtin/ripgrep.ts",
|
|
66
|
+
"types": "./src/core/src/tools/builtin/ripgrep.ts"
|
|
67
|
+
},
|
|
68
|
+
"./tools/builtin/websearch": {
|
|
69
|
+
"import": "./src/core/src/tools/builtin/websearch.ts",
|
|
70
|
+
"types": "./src/core/src/tools/builtin/websearch.ts"
|
|
71
|
+
},
|
|
72
|
+
"./tools/builtin/terminal": {
|
|
73
|
+
"import": "./src/core/src/tools/builtin/terminal.ts",
|
|
74
|
+
"types": "./src/core/src/tools/builtin/terminal.ts"
|
|
75
|
+
},
|
|
76
|
+
"./tools/error": {
|
|
77
|
+
"import": "./src/core/src/tools/error.ts",
|
|
78
|
+
"types": "./src/core/src/tools/error.ts"
|
|
79
|
+
},
|
|
80
|
+
"./tools/bin-manager": {
|
|
81
|
+
"import": "./src/core/src/tools/bin-manager.ts",
|
|
82
|
+
"types": "./src/core/src/tools/bin-manager.ts"
|
|
83
|
+
},
|
|
84
|
+
"./prompts/*": "./src/prompts/src/*"
|
|
85
|
+
},
|
|
86
|
+
"files": [
|
|
87
|
+
"src",
|
|
88
|
+
"README.md",
|
|
89
|
+
"LICENSE"
|
|
90
|
+
],
|
|
91
|
+
"scripts": {
|
|
92
|
+
"dev": "bun run src/index.ts",
|
|
93
|
+
"test": "bun test",
|
|
94
|
+
"typecheck": "tsc --noEmit"
|
|
95
|
+
},
|
|
96
|
+
"dependencies": {
|
|
97
|
+
"@ai-sdk/anthropic": "^3.0.0",
|
|
98
|
+
"@ai-sdk/google": "^3.0.0",
|
|
99
|
+
"@ai-sdk/openai": "^3.0.0",
|
|
100
|
+
"@ai-sdk/openai-compatible": "^2.0.0",
|
|
101
|
+
"@openauthjs/openauth": "^0.4.3",
|
|
102
|
+
"@openrouter/ai-sdk-provider": "^1.2.0",
|
|
103
|
+
"@solana/web3.js": "^1.95.2",
|
|
104
|
+
"ai": "^6.0.0",
|
|
105
|
+
"bs58": "^6.0.0",
|
|
106
|
+
"bun-pty": "^0.3.2",
|
|
107
|
+
"diff": "^8.0.2",
|
|
108
|
+
"fast-glob": "^3.3.2",
|
|
109
|
+
"hono": "^4.9.9",
|
|
110
|
+
"opencode-anthropic-auth": "^0.0.2",
|
|
111
|
+
"qrcode": "^1.5.4",
|
|
112
|
+
"qrcode-terminal": "^0.12.0",
|
|
113
|
+
"tweetnacl": "^1.0.3",
|
|
114
|
+
"x402": "^1.1.0",
|
|
115
|
+
"zod": "^4.1.8"
|
|
116
|
+
},
|
|
117
|
+
"devDependencies": {
|
|
118
|
+
"@types/bun": "latest",
|
|
119
|
+
"typescript": "^5"
|
|
120
|
+
},
|
|
121
|
+
"keywords": [
|
|
122
|
+
"ai",
|
|
123
|
+
"sdk",
|
|
124
|
+
"agents",
|
|
125
|
+
"tools",
|
|
126
|
+
"llm",
|
|
127
|
+
"anthropic",
|
|
128
|
+
"openai",
|
|
129
|
+
"development",
|
|
130
|
+
"assistant",
|
|
131
|
+
"tree-shakable",
|
|
132
|
+
"typescript"
|
|
133
|
+
]
|
|
132
134
|
}
|
package/src/index.ts
CHANGED
|
@@ -311,3 +311,21 @@ export {
|
|
|
311
311
|
buildSkillTool,
|
|
312
312
|
rebuildSkillDescription,
|
|
313
313
|
} from './skills/index.ts';
|
|
314
|
+
|
|
315
|
+
// =======================
|
|
316
|
+
// Tunnel (Cloudflare Tunnels for remote access)
|
|
317
|
+
// =======================
|
|
318
|
+
export {
|
|
319
|
+
getTunnelBinaryPath,
|
|
320
|
+
isTunnelBinaryInstalled,
|
|
321
|
+
downloadTunnelBinary,
|
|
322
|
+
ensureTunnelBinary,
|
|
323
|
+
removeTunnelBinary,
|
|
324
|
+
OttoTunnel,
|
|
325
|
+
createTunnel,
|
|
326
|
+
killStaleTunnels,
|
|
327
|
+
generateQRCode,
|
|
328
|
+
printQRCode,
|
|
329
|
+
} from './tunnel/index.ts';
|
|
330
|
+
|
|
331
|
+
export type { TunnelConnection, TunnelEvents } from './tunnel/index.ts';
|
|
@@ -678,6 +678,31 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
|
|
|
678
678
|
output: 128000,
|
|
679
679
|
},
|
|
680
680
|
},
|
|
681
|
+
{
|
|
682
|
+
id: 'gpt-5.3-codex',
|
|
683
|
+
label: 'GPT-5.3 Codex',
|
|
684
|
+
modalities: {
|
|
685
|
+
input: ['text', 'image', 'pdf'],
|
|
686
|
+
output: ['text'],
|
|
687
|
+
},
|
|
688
|
+
toolCall: true,
|
|
689
|
+
reasoningText: true,
|
|
690
|
+
attachment: true,
|
|
691
|
+
temperature: false,
|
|
692
|
+
knowledge: '2025-08-31',
|
|
693
|
+
releaseDate: '2026-02-05',
|
|
694
|
+
lastUpdated: '2026-02-05',
|
|
695
|
+
openWeights: false,
|
|
696
|
+
cost: {
|
|
697
|
+
input: 1.75,
|
|
698
|
+
output: 14,
|
|
699
|
+
cacheRead: 0.175,
|
|
700
|
+
},
|
|
701
|
+
limit: {
|
|
702
|
+
context: 400000,
|
|
703
|
+
output: 128000,
|
|
704
|
+
},
|
|
705
|
+
},
|
|
681
706
|
{
|
|
682
707
|
id: 'o1',
|
|
683
708
|
label: 'o1',
|
|
@@ -1449,6 +1474,32 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
|
|
|
1449
1474
|
output: 64000,
|
|
1450
1475
|
},
|
|
1451
1476
|
},
|
|
1477
|
+
{
|
|
1478
|
+
id: 'claude-opus-4-6',
|
|
1479
|
+
label: 'Claude Opus 4.6',
|
|
1480
|
+
modalities: {
|
|
1481
|
+
input: ['text', 'image', 'pdf'],
|
|
1482
|
+
output: ['text'],
|
|
1483
|
+
},
|
|
1484
|
+
toolCall: true,
|
|
1485
|
+
reasoningText: true,
|
|
1486
|
+
attachment: true,
|
|
1487
|
+
temperature: true,
|
|
1488
|
+
knowledge: '2025-05',
|
|
1489
|
+
releaseDate: '2026-02-05',
|
|
1490
|
+
lastUpdated: '2026-02-05',
|
|
1491
|
+
openWeights: false,
|
|
1492
|
+
cost: {
|
|
1493
|
+
input: 5,
|
|
1494
|
+
output: 25,
|
|
1495
|
+
cacheRead: 0.5,
|
|
1496
|
+
cacheWrite: 6.25,
|
|
1497
|
+
},
|
|
1498
|
+
limit: {
|
|
1499
|
+
context: 1000000,
|
|
1500
|
+
output: 128000,
|
|
1501
|
+
},
|
|
1502
|
+
},
|
|
1452
1503
|
{
|
|
1453
1504
|
id: 'claude-sonnet-4-0',
|
|
1454
1505
|
label: 'Claude Sonnet 4 (latest)',
|
|
@@ -2222,7 +2273,7 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
|
|
|
2222
2273
|
input: ['text', 'image', 'video'],
|
|
2223
2274
|
output: ['text'],
|
|
2224
2275
|
},
|
|
2225
|
-
toolCall:
|
|
2276
|
+
toolCall: false,
|
|
2226
2277
|
reasoningText: true,
|
|
2227
2278
|
attachment: false,
|
|
2228
2279
|
temperature: true,
|
|
@@ -2622,7 +2673,7 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
|
|
|
2622
2673
|
input: ['text'],
|
|
2623
2674
|
output: ['text'],
|
|
2624
2675
|
},
|
|
2625
|
-
toolCall:
|
|
2676
|
+
toolCall: false,
|
|
2626
2677
|
reasoningText: false,
|
|
2627
2678
|
attachment: false,
|
|
2628
2679
|
temperature: true,
|
|
@@ -2950,9 +3001,6 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
|
|
|
2950
3001
|
context: 163840,
|
|
2951
3002
|
output: 65536,
|
|
2952
3003
|
},
|
|
2953
|
-
provider: {
|
|
2954
|
-
npm: '@openrouter/ai-sdk-provider',
|
|
2955
|
-
},
|
|
2956
3004
|
},
|
|
2957
3005
|
{
|
|
2958
3006
|
id: 'deepseek/deepseek-v3.2-speciale',
|
|
@@ -2977,9 +3025,6 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
|
|
|
2977
3025
|
context: 163840,
|
|
2978
3026
|
output: 65536,
|
|
2979
3027
|
},
|
|
2980
|
-
provider: {
|
|
2981
|
-
npm: '@openrouter/ai-sdk-provider',
|
|
2982
|
-
},
|
|
2983
3028
|
},
|
|
2984
3029
|
{
|
|
2985
3030
|
id: 'featherless/qwerky-72b',
|
|
@@ -3253,9 +3298,6 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
|
|
|
3253
3298
|
context: 1048576,
|
|
3254
3299
|
output: 65536,
|
|
3255
3300
|
},
|
|
3256
|
-
provider: {
|
|
3257
|
-
npm: '@openrouter/ai-sdk-provider',
|
|
3258
|
-
},
|
|
3259
3301
|
},
|
|
3260
3302
|
{
|
|
3261
3303
|
id: 'google/gemini-3-pro-preview',
|
|
@@ -3280,9 +3322,6 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
|
|
|
3280
3322
|
context: 1050000,
|
|
3281
3323
|
output: 66000,
|
|
3282
3324
|
},
|
|
3283
|
-
provider: {
|
|
3284
|
-
npm: '@openrouter/ai-sdk-provider',
|
|
3285
|
-
},
|
|
3286
3325
|
},
|
|
3287
3326
|
{
|
|
3288
3327
|
id: 'google/gemma-2-9b-it',
|
|
@@ -3555,7 +3594,7 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
|
|
|
3555
3594
|
input: ['text'],
|
|
3556
3595
|
output: ['text'],
|
|
3557
3596
|
},
|
|
3558
|
-
toolCall:
|
|
3597
|
+
toolCall: false,
|
|
3559
3598
|
reasoningText: false,
|
|
3560
3599
|
attachment: false,
|
|
3561
3600
|
temperature: true,
|
|
@@ -3579,7 +3618,7 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
|
|
|
3579
3618
|
input: ['text'],
|
|
3580
3619
|
output: ['text'],
|
|
3581
3620
|
},
|
|
3582
|
-
toolCall:
|
|
3621
|
+
toolCall: false,
|
|
3583
3622
|
reasoningText: true,
|
|
3584
3623
|
attachment: false,
|
|
3585
3624
|
temperature: true,
|
|
@@ -3603,7 +3642,7 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
|
|
|
3603
3642
|
input: ['text'],
|
|
3604
3643
|
output: ['text'],
|
|
3605
3644
|
},
|
|
3606
|
-
toolCall:
|
|
3645
|
+
toolCall: false,
|
|
3607
3646
|
reasoningText: false,
|
|
3608
3647
|
attachment: true,
|
|
3609
3648
|
temperature: true,
|
|
@@ -3651,7 +3690,7 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
|
|
|
3651
3690
|
input: ['text', 'image'],
|
|
3652
3691
|
output: ['text'],
|
|
3653
3692
|
},
|
|
3654
|
-
toolCall:
|
|
3693
|
+
toolCall: false,
|
|
3655
3694
|
reasoningText: false,
|
|
3656
3695
|
attachment: true,
|
|
3657
3696
|
temperature: true,
|
|
@@ -3810,9 +3849,6 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
|
|
|
3810
3849
|
context: 196600,
|
|
3811
3850
|
output: 118000,
|
|
3812
3851
|
},
|
|
3813
|
-
provider: {
|
|
3814
|
-
npm: '@openrouter/ai-sdk-provider',
|
|
3815
|
-
},
|
|
3816
3852
|
},
|
|
3817
3853
|
{
|
|
3818
3854
|
id: 'minimax/minimax-m2.1',
|
|
@@ -3836,9 +3872,6 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
|
|
|
3836
3872
|
context: 204800,
|
|
3837
3873
|
output: 131072,
|
|
3838
3874
|
},
|
|
3839
|
-
provider: {
|
|
3840
|
-
npm: '@openrouter/ai-sdk-provider',
|
|
3841
|
-
},
|
|
3842
3875
|
},
|
|
3843
3876
|
{
|
|
3844
3877
|
id: 'mistralai/codestral-2508',
|
|
@@ -4296,9 +4329,6 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
|
|
|
4296
4329
|
context: 262144,
|
|
4297
4330
|
output: 262144,
|
|
4298
4331
|
},
|
|
4299
|
-
provider: {
|
|
4300
|
-
npm: '@openrouter/ai-sdk-provider',
|
|
4301
|
-
},
|
|
4302
4332
|
},
|
|
4303
4333
|
{
|
|
4304
4334
|
id: 'moonshotai/kimi-k2:free',
|
|
@@ -4348,9 +4378,6 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
|
|
|
4348
4378
|
context: 262144,
|
|
4349
4379
|
output: 262144,
|
|
4350
4380
|
},
|
|
4351
|
-
provider: {
|
|
4352
|
-
npm: '@openrouter/ai-sdk-provider',
|
|
4353
|
-
},
|
|
4354
4381
|
},
|
|
4355
4382
|
{
|
|
4356
4383
|
id: 'nousresearch/deephermes-3-llama-3-8b-preview',
|
|
@@ -4383,7 +4410,7 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
|
|
|
4383
4410
|
input: ['text'],
|
|
4384
4411
|
output: ['text'],
|
|
4385
4412
|
},
|
|
4386
|
-
toolCall:
|
|
4413
|
+
toolCall: false,
|
|
4387
4414
|
reasoningText: true,
|
|
4388
4415
|
attachment: false,
|
|
4389
4416
|
temperature: true,
|
|
@@ -6022,7 +6049,7 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
|
|
|
6022
6049
|
input: ['text'],
|
|
6023
6050
|
output: ['text'],
|
|
6024
6051
|
},
|
|
6025
|
-
toolCall:
|
|
6052
|
+
toolCall: true,
|
|
6026
6053
|
reasoningText: true,
|
|
6027
6054
|
attachment: false,
|
|
6028
6055
|
temperature: true,
|
|
@@ -6416,9 +6443,6 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
|
|
|
6416
6443
|
context: 204800,
|
|
6417
6444
|
output: 131072,
|
|
6418
6445
|
},
|
|
6419
|
-
provider: {
|
|
6420
|
-
npm: '@openrouter/ai-sdk-provider',
|
|
6421
|
-
},
|
|
6422
6446
|
},
|
|
6423
6447
|
{
|
|
6424
6448
|
id: 'z-ai/glm-4.7-flash',
|
|
@@ -6442,14 +6466,11 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
|
|
|
6442
6466
|
context: 200000,
|
|
6443
6467
|
output: 65535,
|
|
6444
6468
|
},
|
|
6445
|
-
provider: {
|
|
6446
|
-
npm: '@openrouter/ai-sdk-provider',
|
|
6447
|
-
},
|
|
6448
6469
|
},
|
|
6449
6470
|
],
|
|
6450
6471
|
label: 'OpenRouter',
|
|
6451
6472
|
env: ['OPENROUTER_API_KEY'],
|
|
6452
|
-
npm: '@ai-sdk
|
|
6473
|
+
npm: '@openrouter/ai-sdk-provider',
|
|
6453
6474
|
api: 'https://openrouter.ai/api/v1',
|
|
6454
6475
|
doc: 'https://openrouter.ai/models',
|
|
6455
6476
|
},
|
|
@@ -6598,6 +6619,35 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
|
|
|
6598
6619
|
npm: '@ai-sdk/anthropic',
|
|
6599
6620
|
},
|
|
6600
6621
|
},
|
|
6622
|
+
{
|
|
6623
|
+
id: 'claude-opus-4-6',
|
|
6624
|
+
label: 'Claude Opus 4.6',
|
|
6625
|
+
modalities: {
|
|
6626
|
+
input: ['text', 'image', 'pdf'],
|
|
6627
|
+
output: ['text'],
|
|
6628
|
+
},
|
|
6629
|
+
toolCall: true,
|
|
6630
|
+
reasoningText: true,
|
|
6631
|
+
attachment: true,
|
|
6632
|
+
temperature: true,
|
|
6633
|
+
knowledge: '2025-08-31',
|
|
6634
|
+
releaseDate: '2026-02-05',
|
|
6635
|
+
lastUpdated: '2026-02-05',
|
|
6636
|
+
openWeights: false,
|
|
6637
|
+
cost: {
|
|
6638
|
+
input: 5,
|
|
6639
|
+
output: 25,
|
|
6640
|
+
cacheRead: 0.5,
|
|
6641
|
+
cacheWrite: 6.25,
|
|
6642
|
+
},
|
|
6643
|
+
limit: {
|
|
6644
|
+
context: 1000000,
|
|
6645
|
+
output: 128000,
|
|
6646
|
+
},
|
|
6647
|
+
provider: {
|
|
6648
|
+
npm: '@ai-sdk/anthropic',
|
|
6649
|
+
},
|
|
6650
|
+
},
|
|
6601
6651
|
{
|
|
6602
6652
|
id: 'claude-sonnet-4',
|
|
6603
6653
|
label: 'Claude Sonnet 4',
|
|
@@ -7911,6 +7961,30 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
|
|
|
7911
7961
|
output: 16000,
|
|
7912
7962
|
},
|
|
7913
7963
|
},
|
|
7964
|
+
{
|
|
7965
|
+
id: 'claude-opus-4.6',
|
|
7966
|
+
label: 'Claude Opus 4.6',
|
|
7967
|
+
modalities: {
|
|
7968
|
+
input: ['text', 'image'],
|
|
7969
|
+
output: ['text'],
|
|
7970
|
+
},
|
|
7971
|
+
toolCall: true,
|
|
7972
|
+
reasoningText: true,
|
|
7973
|
+
attachment: true,
|
|
7974
|
+
temperature: true,
|
|
7975
|
+
knowledge: '2025-03-31',
|
|
7976
|
+
releaseDate: '2026-02-05',
|
|
7977
|
+
lastUpdated: '2026-02-05',
|
|
7978
|
+
openWeights: false,
|
|
7979
|
+
cost: {
|
|
7980
|
+
input: 0,
|
|
7981
|
+
output: 0,
|
|
7982
|
+
},
|
|
7983
|
+
limit: {
|
|
7984
|
+
context: 128000,
|
|
7985
|
+
output: 64000,
|
|
7986
|
+
},
|
|
7987
|
+
},
|
|
7914
7988
|
{
|
|
7915
7989
|
id: 'claude-opus-41',
|
|
7916
7990
|
label: 'Claude Opus 4.1',
|
|
@@ -8075,7 +8149,7 @@ export const catalog: Partial<Record<ProviderId, ProviderCatalogEntry>> = {
|
|
|
8075
8149
|
output: 0,
|
|
8076
8150
|
},
|
|
8077
8151
|
limit: {
|
|
8078
|
-
context:
|
|
8152
|
+
context: 128000,
|
|
8079
8153
|
output: 16384,
|
|
8080
8154
|
},
|
|
8081
8155
|
},
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import type { ProviderId, ModelInfo } from '../../types/src/index.ts';
|
|
2
2
|
|
|
3
3
|
const OAUTH_MODEL_PREFIXES: Partial<Record<ProviderId, string[]>> = {
|
|
4
|
-
anthropic: [
|
|
4
|
+
anthropic: [
|
|
5
|
+
'claude-haiku-4-5',
|
|
6
|
+
'claude-opus-4-5',
|
|
7
|
+
'claude-opus-4-6',
|
|
8
|
+
'claude-sonnet-4-5',
|
|
9
|
+
],
|
|
5
10
|
openai: [
|
|
6
11
|
'gpt-5.2-codex',
|
|
12
|
+
'gpt-5.3-codex',
|
|
7
13
|
'gpt-5.1-codex-max',
|
|
8
14
|
'gpt-5.1-codex-mini',
|
|
9
15
|
'gpt-5.2',
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import { createWriteStream } from 'node:fs';
|
|
4
|
+
import { getAgiBinDir } from '../core/src/tools/bin-manager.ts';
|
|
5
|
+
|
|
6
|
+
const BINARY_NAME = 'tunnel';
|
|
7
|
+
const CLOUDFLARED_VERSION = '2024.12.2';
|
|
8
|
+
|
|
9
|
+
interface PlatformInfo {
|
|
10
|
+
os: 'darwin' | 'linux' | 'windows';
|
|
11
|
+
arch: 'amd64' | 'arm64';
|
|
12
|
+
ext: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getPlatformInfo(): PlatformInfo {
|
|
16
|
+
const platform = process.platform;
|
|
17
|
+
const arch = process.arch;
|
|
18
|
+
|
|
19
|
+
const os =
|
|
20
|
+
platform === 'darwin'
|
|
21
|
+
? 'darwin'
|
|
22
|
+
: platform === 'win32'
|
|
23
|
+
? 'windows'
|
|
24
|
+
: 'linux';
|
|
25
|
+
|
|
26
|
+
const cpu = arch === 'arm64' ? 'arm64' : 'amd64';
|
|
27
|
+
const ext = platform === 'win32' ? '.exe' : '';
|
|
28
|
+
|
|
29
|
+
return { os, arch: cpu, ext };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getDownloadUrl(version: string, info: PlatformInfo): string {
|
|
33
|
+
const base = `https://github.com/cloudflare/cloudflared/releases/download/${version}`;
|
|
34
|
+
|
|
35
|
+
if (info.os === 'darwin') {
|
|
36
|
+
return `${base}/cloudflared-darwin-${info.arch}.tgz`;
|
|
37
|
+
}
|
|
38
|
+
if (info.os === 'windows') {
|
|
39
|
+
return `${base}/cloudflared-windows-${info.arch}.exe`;
|
|
40
|
+
}
|
|
41
|
+
return `${base}/cloudflared-linux-${info.arch}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function fileExists(p: string): Promise<boolean> {
|
|
45
|
+
try {
|
|
46
|
+
await fs.access(p);
|
|
47
|
+
return true;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function ensureDir(dir: string): Promise<void> {
|
|
54
|
+
await fs.mkdir(dir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function makeExecutable(p: string): Promise<void> {
|
|
58
|
+
if (process.platform === 'win32') return;
|
|
59
|
+
try {
|
|
60
|
+
await fs.chmod(p, 0o755);
|
|
61
|
+
} catch {}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function extractTarGz(tgzPath: string, destDir: string): Promise<string> {
|
|
65
|
+
const { spawn } = await import('node:child_process');
|
|
66
|
+
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
const proc = spawn('tar', ['-xzf', tgzPath, '-C', destDir], {
|
|
69
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
proc.on('close', (code) => {
|
|
73
|
+
if (code === 0) {
|
|
74
|
+
resolve(join(destDir, 'cloudflared'));
|
|
75
|
+
} else {
|
|
76
|
+
reject(new Error(`tar extraction failed with code ${code}`));
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
proc.on('error', reject);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function formatBytes(bytes: number): string {
|
|
85
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
86
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
87
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function downloadFile(
|
|
91
|
+
url: string,
|
|
92
|
+
dest: string,
|
|
93
|
+
onProgress?: (message: string) => void,
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
const response = await fetch(url, { redirect: 'follow' });
|
|
96
|
+
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Download failed: ${response.status} ${response.statusText}`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!response.body) {
|
|
104
|
+
throw new Error('No response body');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
await ensureDir(join(dest, '..'));
|
|
108
|
+
|
|
109
|
+
const contentLength = response.headers.get('content-length');
|
|
110
|
+
const totalBytes = contentLength ? parseInt(contentLength, 10) : 0;
|
|
111
|
+
|
|
112
|
+
const fileStream = createWriteStream(dest);
|
|
113
|
+
const reader = response.body.getReader();
|
|
114
|
+
|
|
115
|
+
let downloadedBytes = 0;
|
|
116
|
+
let lastProgressUpdate = 0;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
while (true) {
|
|
120
|
+
const { done, value } = await reader.read();
|
|
121
|
+
if (done) break;
|
|
122
|
+
|
|
123
|
+
fileStream.write(value);
|
|
124
|
+
downloadedBytes += value.length;
|
|
125
|
+
|
|
126
|
+
if (onProgress && totalBytes > 0) {
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
if (now - lastProgressUpdate > 200) {
|
|
129
|
+
const percent = Math.round((downloadedBytes / totalBytes) * 100);
|
|
130
|
+
onProgress(
|
|
131
|
+
`Downloading... ${formatBytes(downloadedBytes)} / ${formatBytes(totalBytes)} (${percent}%)`,
|
|
132
|
+
);
|
|
133
|
+
lastProgressUpdate = now;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
fileStream.end();
|
|
138
|
+
|
|
139
|
+
await new Promise<void>((resolve, reject) => {
|
|
140
|
+
fileStream.on('finish', resolve);
|
|
141
|
+
fileStream.on('error', reject);
|
|
142
|
+
});
|
|
143
|
+
} finally {
|
|
144
|
+
reader.releaseLock();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function getTunnelBinaryPath(): string {
|
|
149
|
+
const binDir = getAgiBinDir();
|
|
150
|
+
const ext = process.platform === 'win32' ? '.exe' : '';
|
|
151
|
+
return join(binDir, `${BINARY_NAME}${ext}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function isTunnelBinaryInstalled(): Promise<boolean> {
|
|
155
|
+
const binPath = getTunnelBinaryPath();
|
|
156
|
+
return fileExists(binPath);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function downloadTunnelBinary(
|
|
160
|
+
onProgress?: (message: string) => void,
|
|
161
|
+
): Promise<string> {
|
|
162
|
+
const binPath = getTunnelBinaryPath();
|
|
163
|
+
|
|
164
|
+
if (await fileExists(binPath)) {
|
|
165
|
+
return binPath;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const info = getPlatformInfo();
|
|
169
|
+
const url = getDownloadUrl(CLOUDFLARED_VERSION, info);
|
|
170
|
+
const binDir = getAgiBinDir();
|
|
171
|
+
|
|
172
|
+
await ensureDir(binDir);
|
|
173
|
+
|
|
174
|
+
onProgress?.('Downloading tunnel binary (one-time setup)...');
|
|
175
|
+
|
|
176
|
+
if (info.os === 'darwin') {
|
|
177
|
+
const tgzPath = join(binDir, 'cloudflared.tgz');
|
|
178
|
+
await downloadFile(url, tgzPath, onProgress);
|
|
179
|
+
|
|
180
|
+
onProgress?.('Extracting...');
|
|
181
|
+
const extractedPath = await extractTarGz(tgzPath, binDir);
|
|
182
|
+
|
|
183
|
+
await fs.rename(extractedPath, binPath);
|
|
184
|
+
await fs.unlink(tgzPath);
|
|
185
|
+
} else {
|
|
186
|
+
await downloadFile(url, binPath, onProgress);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await makeExecutable(binPath);
|
|
190
|
+
onProgress?.('Tunnel binary ready.');
|
|
191
|
+
|
|
192
|
+
return binPath;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function ensureTunnelBinary(
|
|
196
|
+
onProgress?: (message: string) => void,
|
|
197
|
+
): Promise<string> {
|
|
198
|
+
const binPath = getTunnelBinaryPath();
|
|
199
|
+
|
|
200
|
+
if (await fileExists(binPath)) {
|
|
201
|
+
return binPath;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return downloadTunnelBinary(onProgress);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function removeTunnelBinary(): Promise<void> {
|
|
208
|
+
const binPath = getTunnelBinaryPath();
|
|
209
|
+
if (await fileExists(binPath)) {
|
|
210
|
+
await fs.unlink(binPath);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export {
|
|
2
|
+
getTunnelBinaryPath,
|
|
3
|
+
isTunnelBinaryInstalled,
|
|
4
|
+
downloadTunnelBinary,
|
|
5
|
+
ensureTunnelBinary,
|
|
6
|
+
removeTunnelBinary,
|
|
7
|
+
} from './binary.ts';
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
OttoTunnel,
|
|
11
|
+
createTunnel,
|
|
12
|
+
killStaleTunnels,
|
|
13
|
+
type TunnelConnection,
|
|
14
|
+
type TunnelEvents,
|
|
15
|
+
} from './tunnel.ts';
|
|
16
|
+
|
|
17
|
+
export { generateQRCode, printQRCode } from './qr.ts';
|
package/src/tunnel/qr.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import qrcode from 'qrcode-terminal';
|
|
2
|
+
|
|
3
|
+
export function generateQRCode(data: string): Promise<string> {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
qrcode.generate(data, { small: true }, (qr: string) => {
|
|
6
|
+
resolve(qr);
|
|
7
|
+
});
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function printQRCode(data: string, label?: string): Promise<void> {
|
|
12
|
+
const qr = await generateQRCode(data);
|
|
13
|
+
console.log('');
|
|
14
|
+
if (label) {
|
|
15
|
+
console.log(` ${label}`);
|
|
16
|
+
}
|
|
17
|
+
console.log(qr);
|
|
18
|
+
console.log(` ${data}`);
|
|
19
|
+
console.log('');
|
|
20
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
declare module 'qrcode-terminal' {
|
|
2
|
+
interface Options {
|
|
3
|
+
small?: boolean;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function generate(
|
|
7
|
+
text: string,
|
|
8
|
+
opts?: Options,
|
|
9
|
+
callback?: (qrcode: string) => void,
|
|
10
|
+
): void;
|
|
11
|
+
|
|
12
|
+
function setErrorLevel(level: 'L' | 'M' | 'Q' | 'H'): void;
|
|
13
|
+
|
|
14
|
+
export { generate, setErrorLevel };
|
|
15
|
+
export default { generate, setErrorLevel };
|
|
16
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { ensureTunnelBinary } from './binary.ts';
|
|
4
|
+
|
|
5
|
+
export interface TunnelConnection {
|
|
6
|
+
id: string;
|
|
7
|
+
ip: string;
|
|
8
|
+
location: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TunnelEvents {
|
|
12
|
+
url: (url: string) => void;
|
|
13
|
+
connected: (connection: TunnelConnection) => void;
|
|
14
|
+
disconnected: (connection: TunnelConnection) => void;
|
|
15
|
+
error: (error: Error) => void;
|
|
16
|
+
exit: (code: number | null, signal: NodeJS.Signals | null) => void;
|
|
17
|
+
stdout: (data: string) => void;
|
|
18
|
+
stderr: (data: string) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const URL_REGEX = /https:\/\/([a-z0-9-]+)\.trycloudflare\.com/;
|
|
22
|
+
const CONN_REGEX = /Connection ([a-f0-9-]+)/;
|
|
23
|
+
const IP_REGEX = /(\d+\.\d+\.\d+\.\d+)/;
|
|
24
|
+
const LOCATION_REGEX = /location=([a-z0-9]+)/i;
|
|
25
|
+
const INDEX_REGEX = /connIndex=(\d+)/;
|
|
26
|
+
|
|
27
|
+
const RATE_LIMIT_REGEX = /429 Too Many Requests|error code: 1015/i;
|
|
28
|
+
const FAILED_UNMARSHAL_REGEX = /failed to unmarshal quick Tunnel/i;
|
|
29
|
+
|
|
30
|
+
export class OttoTunnel extends EventEmitter {
|
|
31
|
+
private process: ChildProcess | null = null;
|
|
32
|
+
private connections: (TunnelConnection | undefined)[] = [];
|
|
33
|
+
private _url: string | null = null;
|
|
34
|
+
private _stopped = false;
|
|
35
|
+
|
|
36
|
+
get url(): string | null {
|
|
37
|
+
return this._url;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get isRunning(): boolean {
|
|
41
|
+
return this.process !== null && !this._stopped;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private handleOutput(output: string): void {
|
|
45
|
+
const urlMatch = output.match(URL_REGEX);
|
|
46
|
+
if (urlMatch && !this._url) {
|
|
47
|
+
this._url = urlMatch[0];
|
|
48
|
+
this.emit('url', this._url);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const connMatch = output.match(CONN_REGEX);
|
|
52
|
+
const ipMatch = output.match(IP_REGEX);
|
|
53
|
+
const locationMatch = output.match(LOCATION_REGEX);
|
|
54
|
+
const indexMatch = output.match(INDEX_REGEX);
|
|
55
|
+
|
|
56
|
+
if (connMatch && ipMatch && locationMatch && indexMatch) {
|
|
57
|
+
const connection: TunnelConnection = {
|
|
58
|
+
id: connMatch[1],
|
|
59
|
+
ip: ipMatch[1],
|
|
60
|
+
location: locationMatch[1],
|
|
61
|
+
};
|
|
62
|
+
const index = Number(indexMatch[1]);
|
|
63
|
+
this.connections[index] = connection;
|
|
64
|
+
this.emit('connected', connection);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (output.includes('terminated') && indexMatch) {
|
|
68
|
+
const index = Number(indexMatch[1]);
|
|
69
|
+
const conn = this.connections[index];
|
|
70
|
+
if (conn) {
|
|
71
|
+
this.emit('disconnected', conn);
|
|
72
|
+
this.connections[index] = undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private checkForRateLimit(output: string): boolean {
|
|
78
|
+
if (RATE_LIMIT_REGEX.test(output) || FAILED_UNMARSHAL_REGEX.test(output)) {
|
|
79
|
+
const error: Error & { code?: string } = new Error(
|
|
80
|
+
'Rate limited by Cloudflare. Please wait 5-10 minutes before trying again.',
|
|
81
|
+
);
|
|
82
|
+
error.code = 'RATE_LIMITED';
|
|
83
|
+
this.emit('error', error);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async start(
|
|
90
|
+
port: number,
|
|
91
|
+
onProgress?: (message: string) => void,
|
|
92
|
+
): Promise<string> {
|
|
93
|
+
if (this.process) {
|
|
94
|
+
throw new Error('Tunnel is already running');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const binPath = await ensureTunnelBinary(onProgress);
|
|
98
|
+
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
const args = ['tunnel', '--url', `http://localhost:${port}`];
|
|
101
|
+
|
|
102
|
+
this.process = spawn(binPath, args, {
|
|
103
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
this.process.on('error', (error) => {
|
|
107
|
+
this.emit('error', error);
|
|
108
|
+
reject(error);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
this.process.on('exit', (code, signal) => {
|
|
112
|
+
this._stopped = true;
|
|
113
|
+
this.process = null;
|
|
114
|
+
this.emit('exit', code, signal);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
this.process.stdout?.on('data', (data: Buffer) => {
|
|
118
|
+
const output = data.toString();
|
|
119
|
+
this.emit('stdout', output);
|
|
120
|
+
if (this.checkForRateLimit(output)) {
|
|
121
|
+
this.stop();
|
|
122
|
+
reject(
|
|
123
|
+
new Error(
|
|
124
|
+
'Rate limited by Cloudflare. Please wait 5-10 minutes before trying again.',
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
this.handleOutput(output);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
this.process.stderr?.on('data', (data: Buffer) => {
|
|
133
|
+
const output = data.toString();
|
|
134
|
+
this.emit('stderr', output);
|
|
135
|
+
if (this.checkForRateLimit(output)) {
|
|
136
|
+
this.stop();
|
|
137
|
+
reject(
|
|
138
|
+
new Error(
|
|
139
|
+
'Rate limited by Cloudflare. Please wait 5-10 minutes before trying again.',
|
|
140
|
+
),
|
|
141
|
+
);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
this.handleOutput(output);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const timeout = setTimeout(() => {
|
|
148
|
+
if (!this._url) {
|
|
149
|
+
this.stop();
|
|
150
|
+
reject(new Error('Tunnel startup timed out'));
|
|
151
|
+
}
|
|
152
|
+
}, 30000);
|
|
153
|
+
|
|
154
|
+
this.once('url', (url) => {
|
|
155
|
+
clearTimeout(timeout);
|
|
156
|
+
resolve(url);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
this.once('error', (error) => {
|
|
160
|
+
clearTimeout(timeout);
|
|
161
|
+
reject(error);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
stop(): boolean {
|
|
167
|
+
if (!this.process) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this._stopped = true;
|
|
172
|
+
const killed = this.process.kill('SIGINT');
|
|
173
|
+
|
|
174
|
+
setTimeout(() => {
|
|
175
|
+
if (this.process && !this.process.killed) {
|
|
176
|
+
this.process.kill('SIGKILL');
|
|
177
|
+
}
|
|
178
|
+
}, 5000);
|
|
179
|
+
|
|
180
|
+
return killed;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
on<K extends keyof TunnelEvents>(event: K, listener: TunnelEvents[K]): this {
|
|
184
|
+
return super.on(event, listener);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
once<K extends keyof TunnelEvents>(
|
|
188
|
+
event: K,
|
|
189
|
+
listener: TunnelEvents[K],
|
|
190
|
+
): this {
|
|
191
|
+
return super.once(event, listener);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
emit<K extends keyof TunnelEvents>(
|
|
195
|
+
event: K,
|
|
196
|
+
...args: Parameters<TunnelEvents[K]>
|
|
197
|
+
): boolean {
|
|
198
|
+
return super.emit(event, ...args);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Kill any existing tunnel processes to prevent stale tunnels
|
|
204
|
+
* from interfering with new ones
|
|
205
|
+
*/
|
|
206
|
+
export async function killStaleTunnels(): Promise<void> {
|
|
207
|
+
try {
|
|
208
|
+
const { exec } = await import('node:child_process');
|
|
209
|
+
const { promisify } = await import('node:util');
|
|
210
|
+
const execAsync = promisify(exec);
|
|
211
|
+
|
|
212
|
+
// Kill any existing tunnel processes (but not the parent otto process)
|
|
213
|
+
await execAsync('pkill -f "tunnel tunnel --url" 2>/dev/null || true');
|
|
214
|
+
// Give processes time to die
|
|
215
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
216
|
+
} catch {
|
|
217
|
+
// Ignore errors - pkill might not find any processes
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function createTunnel(
|
|
222
|
+
port: number,
|
|
223
|
+
onProgress?: (message: string) => void,
|
|
224
|
+
): Promise<{ url: string; tunnel: OttoTunnel }> {
|
|
225
|
+
// Kill any stale tunnel processes first
|
|
226
|
+
await killStaleTunnels();
|
|
227
|
+
|
|
228
|
+
const tunnel = new OttoTunnel();
|
|
229
|
+
const url = await tunnel.start(port, onProgress);
|
|
230
|
+
return { url, tunnel };
|
|
231
|
+
}
|