@ottocode/sdk 0.1.178 → 0.1.179

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 CHANGED
@@ -1,132 +1,134 @@
1
1
  {
2
- "name": "@ottocode/sdk",
3
- "version": "0.1.178",
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
- "tweetnacl": "^1.0.3",
112
- "x402": "^1.1.0",
113
- "zod": "^4.1.8"
114
- },
115
- "devDependencies": {
116
- "@types/bun": "latest",
117
- "typescript": "^5"
118
- },
119
- "keywords": [
120
- "ai",
121
- "sdk",
122
- "agents",
123
- "tools",
124
- "llm",
125
- "anthropic",
126
- "openai",
127
- "development",
128
- "assistant",
129
- "tree-shakable",
130
- "typescript"
131
- ]
2
+ "name": "@ottocode/sdk",
3
+ "version": "0.1.179",
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: true,
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: true,
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: true,
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: true,
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: true,
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: true,
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: true,
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: false,
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/openai-compatible',
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: 64000,
8152
+ context: 128000,
8079
8153
  output: 16384,
8080
8154
  },
8081
8155
  },
@@ -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';
@@ -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
+ }