vibekit-auth 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.js +134 -0
- package/dist/keychain.d.ts +18 -0
- package/dist/keychain.js +64 -0
- package/dist/types.d.ts +38 -0
- package/dist/types.js +2 -0
- package/dist/upload.d.ts +13 -0
- package/dist/upload.js +92 -0
- package/package.json +35 -0
- package/src/index.ts +149 -0
- package/src/keychain.ts +80 -0
- package/src/types.ts +41 -0
- package/src/upload.ts +115 -0
- package/tsconfig.json +17 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const prompts_1 = __importDefault(require("prompts"));
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const ora_1 = __importDefault(require("ora"));
|
|
10
|
+
const keychain_1 = require("./keychain");
|
|
11
|
+
const upload_1 = require("./upload");
|
|
12
|
+
const VIBEKIT_SERVER = process.env.VIBEKIT_SERVER || 'https://vibekit.bot';
|
|
13
|
+
async function main() {
|
|
14
|
+
console.log();
|
|
15
|
+
console.log(chalk_1.default.bold.cyan(' VibeKit Auth'));
|
|
16
|
+
console.log(chalk_1.default.gray(' Upload your Claude credentials for Auto Mode'));
|
|
17
|
+
console.log();
|
|
18
|
+
// Step 1: Extract credentials from Keychain
|
|
19
|
+
const extractSpinner = (0, ora_1.default)('Looking for Claude credentials...').start();
|
|
20
|
+
let credentials;
|
|
21
|
+
try {
|
|
22
|
+
credentials = (0, keychain_1.extractClaudeCredentials)();
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
extractSpinner.fail(chalk_1.default.red('Failed to extract credentials'));
|
|
26
|
+
if (error instanceof Error) {
|
|
27
|
+
console.log(chalk_1.default.red(` ${error.message}`));
|
|
28
|
+
}
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
if (!credentials) {
|
|
32
|
+
extractSpinner.fail(chalk_1.default.red('Claude credentials not found'));
|
|
33
|
+
console.log();
|
|
34
|
+
console.log(chalk_1.default.yellow(' Make sure you have Claude Code installed and logged in:'));
|
|
35
|
+
console.log(chalk_1.default.gray(' 1. Install: npm install -g @anthropic-ai/claude-code'));
|
|
36
|
+
console.log(chalk_1.default.gray(' 2. Run: claude'));
|
|
37
|
+
console.log(chalk_1.default.gray(' 3. Complete the login flow in your browser'));
|
|
38
|
+
console.log();
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
// Check expiry
|
|
42
|
+
const expiry = (0, keychain_1.checkCredentialExpiry)(credentials);
|
|
43
|
+
if (expiry.expired) {
|
|
44
|
+
extractSpinner.fail(chalk_1.default.red('Claude credentials have expired'));
|
|
45
|
+
console.log();
|
|
46
|
+
console.log(chalk_1.default.yellow(' Please re-authenticate with Claude Code:'));
|
|
47
|
+
console.log(chalk_1.default.gray(' Run: claude --logout && claude'));
|
|
48
|
+
console.log();
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
const subType = (0, keychain_1.formatSubscriptionType)(credentials.claudeAiOauth.subscriptionType);
|
|
52
|
+
extractSpinner.succeed(chalk_1.default.green(`Found: ${subType}`));
|
|
53
|
+
// Show expiry info
|
|
54
|
+
if (expiry.expiringSoon) {
|
|
55
|
+
console.log(chalk_1.default.yellow(` ⚠ Expires in ${expiry.daysUntilExpiry} days (${expiry.expiresAt.toLocaleDateString()})`));
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
console.log(chalk_1.default.gray(` Expires: ${expiry.expiresAt.toLocaleDateString()} (${expiry.daysUntilExpiry} days)`));
|
|
59
|
+
}
|
|
60
|
+
console.log();
|
|
61
|
+
// Step 2: Get Telegram ID
|
|
62
|
+
const response = await (0, prompts_1.default)([
|
|
63
|
+
{
|
|
64
|
+
type: 'text',
|
|
65
|
+
name: 'telegramId',
|
|
66
|
+
message: 'Enter your Telegram ID or username:',
|
|
67
|
+
hint: 'You can find your ID by messaging @userinfobot on Telegram',
|
|
68
|
+
validate: (value) => {
|
|
69
|
+
if (!value.trim()) {
|
|
70
|
+
return 'Telegram ID is required';
|
|
71
|
+
}
|
|
72
|
+
// Allow numeric ID or @username
|
|
73
|
+
if (!/^(@?[\w]+|\d+)$/.test(value.trim())) {
|
|
74
|
+
return 'Enter a valid Telegram ID (number) or username (@username)';
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
]);
|
|
80
|
+
if (!response.telegramId) {
|
|
81
|
+
console.log(chalk_1.default.yellow('\n Cancelled.'));
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
let telegramId = response.telegramId.trim();
|
|
85
|
+
// If they entered a username, we need to resolve it
|
|
86
|
+
// For now, require numeric ID
|
|
87
|
+
if (telegramId.startsWith('@') || isNaN(Number(telegramId))) {
|
|
88
|
+
console.log();
|
|
89
|
+
console.log(chalk_1.default.yellow(' Please enter your numeric Telegram ID, not username.'));
|
|
90
|
+
console.log(chalk_1.default.gray(' To find your ID:'));
|
|
91
|
+
console.log(chalk_1.default.gray(' 1. Open Telegram'));
|
|
92
|
+
console.log(chalk_1.default.gray(' 2. Message @userinfobot'));
|
|
93
|
+
console.log(chalk_1.default.gray(' 3. It will reply with your numeric ID'));
|
|
94
|
+
console.log();
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
const numericTelegramId = parseInt(telegramId, 10);
|
|
98
|
+
// Step 3: Upload to server
|
|
99
|
+
console.log();
|
|
100
|
+
const uploadSpinner = (0, ora_1.default)('Uploading credentials to VibeKit...').start();
|
|
101
|
+
try {
|
|
102
|
+
const result = await (0, upload_1.uploadCredentials)(numericTelegramId, credentials, VIBEKIT_SERVER);
|
|
103
|
+
if (result.success) {
|
|
104
|
+
uploadSpinner.succeed(chalk_1.default.green('Credentials uploaded successfully!'));
|
|
105
|
+
console.log();
|
|
106
|
+
console.log(chalk_1.default.bold.green(' Auto Mode is ready!'));
|
|
107
|
+
console.log();
|
|
108
|
+
console.log(chalk_1.default.white(' Open Telegram and use /auto to start.'));
|
|
109
|
+
console.log(chalk_1.default.gray(' Your Claude subscription will be used for AI - no credits needed.'));
|
|
110
|
+
console.log();
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
uploadSpinner.fail(chalk_1.default.red('Upload failed'));
|
|
114
|
+
console.log(chalk_1.default.red(` ${result.message}`));
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
uploadSpinner.fail(chalk_1.default.red('Upload failed'));
|
|
120
|
+
if (error instanceof Error) {
|
|
121
|
+
console.log(chalk_1.default.red(` ${error.message}`));
|
|
122
|
+
}
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Handle Ctrl+C gracefully
|
|
127
|
+
process.on('SIGINT', () => {
|
|
128
|
+
console.log(chalk_1.default.yellow('\n Cancelled.'));
|
|
129
|
+
process.exit(0);
|
|
130
|
+
});
|
|
131
|
+
main().catch((error) => {
|
|
132
|
+
console.error(chalk_1.default.red('Unexpected error:'), error);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ClaudeOAuthCredentials } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Extract Claude OAuth credentials from macOS Keychain
|
|
4
|
+
*/
|
|
5
|
+
export declare function extractClaudeCredentials(): ClaudeOAuthCredentials | null;
|
|
6
|
+
/**
|
|
7
|
+
* Check if credentials are expired or expiring soon
|
|
8
|
+
*/
|
|
9
|
+
export declare function checkCredentialExpiry(credentials: ClaudeOAuthCredentials): {
|
|
10
|
+
expired: boolean;
|
|
11
|
+
expiringSoon: boolean;
|
|
12
|
+
expiresAt: Date;
|
|
13
|
+
daysUntilExpiry: number;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Format subscription type for display
|
|
17
|
+
*/
|
|
18
|
+
export declare function formatSubscriptionType(type: string): string;
|
package/dist/keychain.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractClaudeCredentials = extractClaudeCredentials;
|
|
4
|
+
exports.checkCredentialExpiry = checkCredentialExpiry;
|
|
5
|
+
exports.formatSubscriptionType = formatSubscriptionType;
|
|
6
|
+
const child_process_1 = require("child_process");
|
|
7
|
+
const KEYCHAIN_SERVICE_NAME = 'Claude Code-credentials';
|
|
8
|
+
/**
|
|
9
|
+
* Extract Claude OAuth credentials from macOS Keychain
|
|
10
|
+
*/
|
|
11
|
+
function extractClaudeCredentials() {
|
|
12
|
+
// Check if running on macOS
|
|
13
|
+
if (process.platform !== 'darwin') {
|
|
14
|
+
throw new Error('vibekit-auth currently only supports macOS. ' +
|
|
15
|
+
'Linux and Windows support coming soon.');
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
// Use security command to get the password from Keychain
|
|
19
|
+
const result = (0, child_process_1.execSync)(`security find-generic-password -s "${KEYCHAIN_SERVICE_NAME}" -w`, {
|
|
20
|
+
encoding: 'utf8',
|
|
21
|
+
stdio: ['pipe', 'pipe', 'pipe'] // Suppress stderr
|
|
22
|
+
});
|
|
23
|
+
const credentials = JSON.parse(result.trim());
|
|
24
|
+
return credentials;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
// Check if it's a "not found" error
|
|
28
|
+
if (error instanceof Error && error.message.includes('could not be found')) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
// Check for user cancellation (denied Keychain access)
|
|
32
|
+
if (error instanceof Error && error.message.includes('User canceled')) {
|
|
33
|
+
throw new Error('Keychain access was denied. Please allow access when prompted.');
|
|
34
|
+
}
|
|
35
|
+
// Re-throw other errors
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Check if credentials are expired or expiring soon
|
|
41
|
+
*/
|
|
42
|
+
function checkCredentialExpiry(credentials) {
|
|
43
|
+
const expiresAt = new Date(credentials.claudeAiOauth.expiresAt);
|
|
44
|
+
const now = new Date();
|
|
45
|
+
const msUntilExpiry = expiresAt.getTime() - now.getTime();
|
|
46
|
+
const daysUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60 * 24));
|
|
47
|
+
return {
|
|
48
|
+
expired: msUntilExpiry <= 0,
|
|
49
|
+
expiringSoon: daysUntilExpiry <= 7,
|
|
50
|
+
expiresAt,
|
|
51
|
+
daysUntilExpiry
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Format subscription type for display
|
|
56
|
+
*/
|
|
57
|
+
function formatSubscriptionType(type) {
|
|
58
|
+
const types = {
|
|
59
|
+
'pro': 'Claude Pro',
|
|
60
|
+
'max': 'Claude Max',
|
|
61
|
+
'free': 'Claude Free'
|
|
62
|
+
};
|
|
63
|
+
return types[type.toLowerCase()] || `Claude ${type}`;
|
|
64
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude OAuth credentials as stored in macOS Keychain
|
|
3
|
+
*/
|
|
4
|
+
export interface ClaudeOAuthCredentials {
|
|
5
|
+
claudeAiOauth: {
|
|
6
|
+
accessToken: string;
|
|
7
|
+
refreshToken: string;
|
|
8
|
+
expiresAt: number;
|
|
9
|
+
scopes: string[];
|
|
10
|
+
subscriptionType: 'pro' | 'max' | string;
|
|
11
|
+
rateLimitTier?: string;
|
|
12
|
+
};
|
|
13
|
+
organizationUuid?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Credentials to upload to server (subset of full credentials)
|
|
17
|
+
*/
|
|
18
|
+
export interface UploadPayload {
|
|
19
|
+
telegramId: number;
|
|
20
|
+
credentials: string;
|
|
21
|
+
subscriptionType: string;
|
|
22
|
+
expiresAt: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Server response after credential upload
|
|
26
|
+
*/
|
|
27
|
+
export interface UploadResponse {
|
|
28
|
+
success: boolean;
|
|
29
|
+
message: string;
|
|
30
|
+
autoModeEnabled?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Configuration for the CLI
|
|
34
|
+
*/
|
|
35
|
+
export interface Config {
|
|
36
|
+
serverUrl: string;
|
|
37
|
+
apiEndpoint: string;
|
|
38
|
+
}
|
package/dist/types.js
ADDED
package/dist/upload.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ClaudeOAuthCredentials, UploadResponse } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Upload credentials to VibeKit server
|
|
4
|
+
*/
|
|
5
|
+
export declare function uploadCredentials(telegramId: number, credentials: ClaudeOAuthCredentials, serverUrl?: string): Promise<UploadResponse>;
|
|
6
|
+
/**
|
|
7
|
+
* Check if user has existing auto mode credentials on server
|
|
8
|
+
*/
|
|
9
|
+
export declare function checkAutoModeStatus(telegramId: number, serverUrl?: string): Promise<{
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
subscriptionType?: string;
|
|
12
|
+
expiresAt?: number;
|
|
13
|
+
}>;
|
package/dist/upload.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.uploadCredentials = uploadCredentials;
|
|
7
|
+
exports.checkAutoModeStatus = checkAutoModeStatus;
|
|
8
|
+
const https_1 = __importDefault(require("https"));
|
|
9
|
+
const http_1 = __importDefault(require("http"));
|
|
10
|
+
const url_1 = require("url");
|
|
11
|
+
const DEFAULT_SERVER_URL = 'https://vibekit.bot';
|
|
12
|
+
/**
|
|
13
|
+
* Upload credentials to VibeKit server
|
|
14
|
+
*/
|
|
15
|
+
async function uploadCredentials(telegramId, credentials, serverUrl = DEFAULT_SERVER_URL) {
|
|
16
|
+
const url = new url_1.URL('/api/auto/credentials', serverUrl);
|
|
17
|
+
// Prepare payload - base64 encode the full credentials JSON
|
|
18
|
+
const payload = JSON.stringify({
|
|
19
|
+
telegramId,
|
|
20
|
+
credentials: Buffer.from(JSON.stringify(credentials)).toString('base64'),
|
|
21
|
+
subscriptionType: credentials.claudeAiOauth.subscriptionType,
|
|
22
|
+
expiresAt: credentials.claudeAiOauth.expiresAt
|
|
23
|
+
});
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const protocol = url.protocol === 'https:' ? https_1.default : http_1.default;
|
|
26
|
+
const req = protocol.request(url, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: {
|
|
29
|
+
'Content-Type': 'application/json',
|
|
30
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
31
|
+
'User-Agent': 'vibekit-auth/1.0.0'
|
|
32
|
+
}
|
|
33
|
+
}, (res) => {
|
|
34
|
+
let data = '';
|
|
35
|
+
res.on('data', (chunk) => {
|
|
36
|
+
data += chunk;
|
|
37
|
+
});
|
|
38
|
+
res.on('end', () => {
|
|
39
|
+
try {
|
|
40
|
+
const response = JSON.parse(data);
|
|
41
|
+
if (res.statusCode === 200 || res.statusCode === 201) {
|
|
42
|
+
resolve(response);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
reject(new Error(response.message || `Server returned ${res.statusCode}`));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
reject(new Error(`Invalid server response: ${data}`));
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
req.on('error', (error) => {
|
|
54
|
+
reject(new Error(`Failed to connect to server: ${error.message}`));
|
|
55
|
+
});
|
|
56
|
+
req.write(payload);
|
|
57
|
+
req.end();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Check if user has existing auto mode credentials on server
|
|
62
|
+
*/
|
|
63
|
+
async function checkAutoModeStatus(telegramId, serverUrl = DEFAULT_SERVER_URL) {
|
|
64
|
+
const url = new url_1.URL(`/api/auto/status?telegramId=${telegramId}`, serverUrl);
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const protocol = url.protocol === 'https:' ? https_1.default : http_1.default;
|
|
67
|
+
const req = protocol.request(url, {
|
|
68
|
+
method: 'GET',
|
|
69
|
+
headers: {
|
|
70
|
+
'User-Agent': 'vibekit-auth/1.0.0'
|
|
71
|
+
}
|
|
72
|
+
}, (res) => {
|
|
73
|
+
let data = '';
|
|
74
|
+
res.on('data', (chunk) => {
|
|
75
|
+
data += chunk;
|
|
76
|
+
});
|
|
77
|
+
res.on('end', () => {
|
|
78
|
+
try {
|
|
79
|
+
const response = JSON.parse(data);
|
|
80
|
+
resolve(response);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
resolve({ enabled: false });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
req.on('error', () => {
|
|
88
|
+
resolve({ enabled: false });
|
|
89
|
+
});
|
|
90
|
+
req.end();
|
|
91
|
+
});
|
|
92
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vibekit-auth",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Upload your Claude credentials to VibeKit for Auto Mode",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"vibekit-auth": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"start": "node dist/index.js",
|
|
12
|
+
"dev": "ts-node src/index.ts"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"claude",
|
|
16
|
+
"vibekit",
|
|
17
|
+
"telegram",
|
|
18
|
+
"ai"
|
|
19
|
+
],
|
|
20
|
+
"author": "",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"chalk": "^4.1.2",
|
|
24
|
+
"ora": "^5.4.1",
|
|
25
|
+
"prompts": "^2.4.2"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^20.10.0",
|
|
29
|
+
"@types/prompts": "^2.4.9",
|
|
30
|
+
"typescript": "^5.3.0"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18.0.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import prompts from 'prompts';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import {
|
|
7
|
+
extractClaudeCredentials,
|
|
8
|
+
checkCredentialExpiry,
|
|
9
|
+
formatSubscriptionType
|
|
10
|
+
} from './keychain';
|
|
11
|
+
import { uploadCredentials } from './upload';
|
|
12
|
+
|
|
13
|
+
const VIBEKIT_SERVER = process.env.VIBEKIT_SERVER || 'https://vibekit.bot';
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
console.log();
|
|
17
|
+
console.log(chalk.bold.cyan(' VibeKit Auth'));
|
|
18
|
+
console.log(chalk.gray(' Upload your Claude credentials for Auto Mode'));
|
|
19
|
+
console.log();
|
|
20
|
+
|
|
21
|
+
// Step 1: Extract credentials from Keychain
|
|
22
|
+
const extractSpinner = ora('Looking for Claude credentials...').start();
|
|
23
|
+
|
|
24
|
+
let credentials;
|
|
25
|
+
try {
|
|
26
|
+
credentials = extractClaudeCredentials();
|
|
27
|
+
} catch (error: unknown) {
|
|
28
|
+
extractSpinner.fail(chalk.red('Failed to extract credentials'));
|
|
29
|
+
if (error instanceof Error) {
|
|
30
|
+
console.log(chalk.red(` ${error.message}`));
|
|
31
|
+
}
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!credentials) {
|
|
36
|
+
extractSpinner.fail(chalk.red('Claude credentials not found'));
|
|
37
|
+
console.log();
|
|
38
|
+
console.log(chalk.yellow(' Make sure you have Claude Code installed and logged in:'));
|
|
39
|
+
console.log(chalk.gray(' 1. Install: npm install -g @anthropic-ai/claude-code'));
|
|
40
|
+
console.log(chalk.gray(' 2. Run: claude'));
|
|
41
|
+
console.log(chalk.gray(' 3. Complete the login flow in your browser'));
|
|
42
|
+
console.log();
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check expiry
|
|
47
|
+
const expiry = checkCredentialExpiry(credentials);
|
|
48
|
+
|
|
49
|
+
if (expiry.expired) {
|
|
50
|
+
extractSpinner.fail(chalk.red('Claude credentials have expired'));
|
|
51
|
+
console.log();
|
|
52
|
+
console.log(chalk.yellow(' Please re-authenticate with Claude Code:'));
|
|
53
|
+
console.log(chalk.gray(' Run: claude --logout && claude'));
|
|
54
|
+
console.log();
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const subType = formatSubscriptionType(credentials.claudeAiOauth.subscriptionType);
|
|
59
|
+
extractSpinner.succeed(chalk.green(`Found: ${subType}`));
|
|
60
|
+
|
|
61
|
+
// Show expiry info
|
|
62
|
+
if (expiry.expiringSoon) {
|
|
63
|
+
console.log(chalk.yellow(` ⚠ Expires in ${expiry.daysUntilExpiry} days (${expiry.expiresAt.toLocaleDateString()})`));
|
|
64
|
+
} else {
|
|
65
|
+
console.log(chalk.gray(` Expires: ${expiry.expiresAt.toLocaleDateString()} (${expiry.daysUntilExpiry} days)`));
|
|
66
|
+
}
|
|
67
|
+
console.log();
|
|
68
|
+
|
|
69
|
+
// Step 2: Get Telegram ID
|
|
70
|
+
const response = await prompts([
|
|
71
|
+
{
|
|
72
|
+
type: 'text',
|
|
73
|
+
name: 'telegramId',
|
|
74
|
+
message: 'Enter your Telegram ID or username:',
|
|
75
|
+
hint: 'You can find your ID by messaging @userinfobot on Telegram',
|
|
76
|
+
validate: (value: string) => {
|
|
77
|
+
if (!value.trim()) {
|
|
78
|
+
return 'Telegram ID is required';
|
|
79
|
+
}
|
|
80
|
+
// Allow numeric ID or @username
|
|
81
|
+
if (!/^(@?[\w]+|\d+)$/.test(value.trim())) {
|
|
82
|
+
return 'Enter a valid Telegram ID (number) or username (@username)';
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
if (!response.telegramId) {
|
|
90
|
+
console.log(chalk.yellow('\n Cancelled.'));
|
|
91
|
+
process.exit(0);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let telegramId = response.telegramId.trim();
|
|
95
|
+
|
|
96
|
+
// If they entered a username, we need to resolve it
|
|
97
|
+
// For now, require numeric ID
|
|
98
|
+
if (telegramId.startsWith('@') || isNaN(Number(telegramId))) {
|
|
99
|
+
console.log();
|
|
100
|
+
console.log(chalk.yellow(' Please enter your numeric Telegram ID, not username.'));
|
|
101
|
+
console.log(chalk.gray(' To find your ID:'));
|
|
102
|
+
console.log(chalk.gray(' 1. Open Telegram'));
|
|
103
|
+
console.log(chalk.gray(' 2. Message @userinfobot'));
|
|
104
|
+
console.log(chalk.gray(' 3. It will reply with your numeric ID'));
|
|
105
|
+
console.log();
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const numericTelegramId = parseInt(telegramId, 10);
|
|
110
|
+
|
|
111
|
+
// Step 3: Upload to server
|
|
112
|
+
console.log();
|
|
113
|
+
const uploadSpinner = ora('Uploading credentials to VibeKit...').start();
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const result = await uploadCredentials(numericTelegramId, credentials, VIBEKIT_SERVER);
|
|
117
|
+
|
|
118
|
+
if (result.success) {
|
|
119
|
+
uploadSpinner.succeed(chalk.green('Credentials uploaded successfully!'));
|
|
120
|
+
console.log();
|
|
121
|
+
console.log(chalk.bold.green(' Auto Mode is ready!'));
|
|
122
|
+
console.log();
|
|
123
|
+
console.log(chalk.white(' Open Telegram and use /auto to start.'));
|
|
124
|
+
console.log(chalk.gray(' Your Claude subscription will be used for AI - no credits needed.'));
|
|
125
|
+
console.log();
|
|
126
|
+
} else {
|
|
127
|
+
uploadSpinner.fail(chalk.red('Upload failed'));
|
|
128
|
+
console.log(chalk.red(` ${result.message}`));
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
} catch (error: unknown) {
|
|
132
|
+
uploadSpinner.fail(chalk.red('Upload failed'));
|
|
133
|
+
if (error instanceof Error) {
|
|
134
|
+
console.log(chalk.red(` ${error.message}`));
|
|
135
|
+
}
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Handle Ctrl+C gracefully
|
|
141
|
+
process.on('SIGINT', () => {
|
|
142
|
+
console.log(chalk.yellow('\n Cancelled.'));
|
|
143
|
+
process.exit(0);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
main().catch((error) => {
|
|
147
|
+
console.error(chalk.red('Unexpected error:'), error);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
});
|
package/src/keychain.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { ClaudeOAuthCredentials } from './types';
|
|
3
|
+
|
|
4
|
+
const KEYCHAIN_SERVICE_NAME = 'Claude Code-credentials';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extract Claude OAuth credentials from macOS Keychain
|
|
8
|
+
*/
|
|
9
|
+
export function extractClaudeCredentials(): ClaudeOAuthCredentials | null {
|
|
10
|
+
// Check if running on macOS
|
|
11
|
+
if (process.platform !== 'darwin') {
|
|
12
|
+
throw new Error(
|
|
13
|
+
'vibekit-auth currently only supports macOS. ' +
|
|
14
|
+
'Linux and Windows support coming soon.'
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
// Use security command to get the password from Keychain
|
|
20
|
+
const result = execSync(
|
|
21
|
+
`security find-generic-password -s "${KEYCHAIN_SERVICE_NAME}" -w`,
|
|
22
|
+
{
|
|
23
|
+
encoding: 'utf8',
|
|
24
|
+
stdio: ['pipe', 'pipe', 'pipe'] // Suppress stderr
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const credentials = JSON.parse(result.trim()) as ClaudeOAuthCredentials;
|
|
29
|
+
return credentials;
|
|
30
|
+
} catch (error: unknown) {
|
|
31
|
+
// Check if it's a "not found" error
|
|
32
|
+
if (error instanceof Error && error.message.includes('could not be found')) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check for user cancellation (denied Keychain access)
|
|
37
|
+
if (error instanceof Error && error.message.includes('User canceled')) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
'Keychain access was denied. Please allow access when prompted.'
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Re-throw other errors
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if credentials are expired or expiring soon
|
|
50
|
+
*/
|
|
51
|
+
export function checkCredentialExpiry(credentials: ClaudeOAuthCredentials): {
|
|
52
|
+
expired: boolean;
|
|
53
|
+
expiringSoon: boolean;
|
|
54
|
+
expiresAt: Date;
|
|
55
|
+
daysUntilExpiry: number;
|
|
56
|
+
} {
|
|
57
|
+
const expiresAt = new Date(credentials.claudeAiOauth.expiresAt);
|
|
58
|
+
const now = new Date();
|
|
59
|
+
const msUntilExpiry = expiresAt.getTime() - now.getTime();
|
|
60
|
+
const daysUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60 * 24));
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
expired: msUntilExpiry <= 0,
|
|
64
|
+
expiringSoon: daysUntilExpiry <= 7,
|
|
65
|
+
expiresAt,
|
|
66
|
+
daysUntilExpiry
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Format subscription type for display
|
|
72
|
+
*/
|
|
73
|
+
export function formatSubscriptionType(type: string): string {
|
|
74
|
+
const types: Record<string, string> = {
|
|
75
|
+
'pro': 'Claude Pro',
|
|
76
|
+
'max': 'Claude Max',
|
|
77
|
+
'free': 'Claude Free'
|
|
78
|
+
};
|
|
79
|
+
return types[type.toLowerCase()] || `Claude ${type}`;
|
|
80
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude OAuth credentials as stored in macOS Keychain
|
|
3
|
+
*/
|
|
4
|
+
export interface ClaudeOAuthCredentials {
|
|
5
|
+
claudeAiOauth: {
|
|
6
|
+
accessToken: string;
|
|
7
|
+
refreshToken: string;
|
|
8
|
+
expiresAt: number; // Unix timestamp in milliseconds
|
|
9
|
+
scopes: string[];
|
|
10
|
+
subscriptionType: 'pro' | 'max' | string;
|
|
11
|
+
rateLimitTier?: string;
|
|
12
|
+
};
|
|
13
|
+
organizationUuid?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Credentials to upload to server (subset of full credentials)
|
|
18
|
+
*/
|
|
19
|
+
export interface UploadPayload {
|
|
20
|
+
telegramId: number;
|
|
21
|
+
credentials: string; // Base64 encoded JSON
|
|
22
|
+
subscriptionType: string;
|
|
23
|
+
expiresAt: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Server response after credential upload
|
|
28
|
+
*/
|
|
29
|
+
export interface UploadResponse {
|
|
30
|
+
success: boolean;
|
|
31
|
+
message: string;
|
|
32
|
+
autoModeEnabled?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Configuration for the CLI
|
|
37
|
+
*/
|
|
38
|
+
export interface Config {
|
|
39
|
+
serverUrl: string;
|
|
40
|
+
apiEndpoint: string;
|
|
41
|
+
}
|
package/src/upload.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import https from 'https';
|
|
2
|
+
import http from 'http';
|
|
3
|
+
import { URL } from 'url';
|
|
4
|
+
import { ClaudeOAuthCredentials, UploadResponse } from './types';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_SERVER_URL = 'https://vibekit.bot';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Upload credentials to VibeKit server
|
|
10
|
+
*/
|
|
11
|
+
export async function uploadCredentials(
|
|
12
|
+
telegramId: number,
|
|
13
|
+
credentials: ClaudeOAuthCredentials,
|
|
14
|
+
serverUrl: string = DEFAULT_SERVER_URL
|
|
15
|
+
): Promise<UploadResponse> {
|
|
16
|
+
const url = new URL('/api/auto/credentials', serverUrl);
|
|
17
|
+
|
|
18
|
+
// Prepare payload - base64 encode the full credentials JSON
|
|
19
|
+
const payload = JSON.stringify({
|
|
20
|
+
telegramId,
|
|
21
|
+
credentials: Buffer.from(JSON.stringify(credentials)).toString('base64'),
|
|
22
|
+
subscriptionType: credentials.claudeAiOauth.subscriptionType,
|
|
23
|
+
expiresAt: credentials.claudeAiOauth.expiresAt
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const protocol = url.protocol === 'https:' ? https : http;
|
|
28
|
+
|
|
29
|
+
const req = protocol.request(
|
|
30
|
+
url,
|
|
31
|
+
{
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: {
|
|
34
|
+
'Content-Type': 'application/json',
|
|
35
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
36
|
+
'User-Agent': 'vibekit-auth/1.0.0'
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
(res) => {
|
|
40
|
+
let data = '';
|
|
41
|
+
|
|
42
|
+
res.on('data', (chunk) => {
|
|
43
|
+
data += chunk;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
res.on('end', () => {
|
|
47
|
+
try {
|
|
48
|
+
const response = JSON.parse(data) as UploadResponse;
|
|
49
|
+
|
|
50
|
+
if (res.statusCode === 200 || res.statusCode === 201) {
|
|
51
|
+
resolve(response);
|
|
52
|
+
} else {
|
|
53
|
+
reject(new Error(response.message || `Server returned ${res.statusCode}`));
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
reject(new Error(`Invalid server response: ${data}`));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
req.on('error', (error) => {
|
|
63
|
+
reject(new Error(`Failed to connect to server: ${error.message}`));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
req.write(payload);
|
|
67
|
+
req.end();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if user has existing auto mode credentials on server
|
|
73
|
+
*/
|
|
74
|
+
export async function checkAutoModeStatus(
|
|
75
|
+
telegramId: number,
|
|
76
|
+
serverUrl: string = DEFAULT_SERVER_URL
|
|
77
|
+
): Promise<{ enabled: boolean; subscriptionType?: string; expiresAt?: number }> {
|
|
78
|
+
const url = new URL(`/api/auto/status?telegramId=${telegramId}`, serverUrl);
|
|
79
|
+
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
const protocol = url.protocol === 'https:' ? https : http;
|
|
82
|
+
|
|
83
|
+
const req = protocol.request(
|
|
84
|
+
url,
|
|
85
|
+
{
|
|
86
|
+
method: 'GET',
|
|
87
|
+
headers: {
|
|
88
|
+
'User-Agent': 'vibekit-auth/1.0.0'
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
(res) => {
|
|
92
|
+
let data = '';
|
|
93
|
+
|
|
94
|
+
res.on('data', (chunk) => {
|
|
95
|
+
data += chunk;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
res.on('end', () => {
|
|
99
|
+
try {
|
|
100
|
+
const response = JSON.parse(data);
|
|
101
|
+
resolve(response);
|
|
102
|
+
} catch {
|
|
103
|
+
resolve({ enabled: false });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
req.on('error', () => {
|
|
110
|
+
resolve({ enabled: false });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
req.end();
|
|
114
|
+
});
|
|
115
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|