obol-ai 0.1.5 → 0.1.7
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 +1 -1
- package/src/claude.js +51 -20
- package/src/cli/config.js +76 -2
- package/src/cli/init.js +3 -9
- package/src/oauth.js +3 -2
- package/src/telegram.js +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "obol-ai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Self-evolving AI assistant that learns, remembers, and acts on its own. Persistent vector memory, self-rewriting personality, proactive heartbeats.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/claude.js
CHANGED
|
@@ -47,6 +47,8 @@ function createAnthropicClient(anthropicConfig, { useOAuth = true } = {}) {
|
|
|
47
47
|
throw new Error('No Anthropic credentials configured. Run: obol config');
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
let _refreshPromise = null;
|
|
51
|
+
|
|
50
52
|
async function ensureFreshToken(anthropicConfig) {
|
|
51
53
|
if (!anthropicConfig.oauth?.accessToken) return;
|
|
52
54
|
if (!isExpired(anthropicConfig.oauth)) return;
|
|
@@ -60,27 +62,56 @@ async function ensureFreshToken(anthropicConfig) {
|
|
|
60
62
|
throw err;
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
anthropicConfig.oauth
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
anthropicConfig._oauthFailed
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
65
|
+
if (_refreshPromise) {
|
|
66
|
+
try {
|
|
67
|
+
await _refreshPromise;
|
|
68
|
+
} catch {}
|
|
69
|
+
if (!isExpired(anthropicConfig.oauth)) return;
|
|
70
|
+
if (anthropicConfig._oauthFailed) return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_refreshPromise = (async () => {
|
|
74
|
+
try {
|
|
75
|
+
const tokens = await refreshTokens(anthropicConfig.oauth.refreshToken);
|
|
76
|
+
console.log('[oauth] Refresh succeeded, new refresh token:', !!tokens.refreshToken);
|
|
77
|
+
anthropicConfig.oauth.accessToken = tokens.accessToken;
|
|
78
|
+
if (tokens.refreshToken) anthropicConfig.oauth.refreshToken = tokens.refreshToken;
|
|
79
|
+
anthropicConfig.oauth.expires = tokens.expires;
|
|
80
|
+
delete anthropicConfig._oauthFailed;
|
|
81
|
+
|
|
82
|
+
const config = loadConfig({ resolve: false });
|
|
83
|
+
if (config) {
|
|
84
|
+
config.anthropic.oauth = anthropicConfig.oauth;
|
|
85
|
+
saveConfig(config);
|
|
86
|
+
}
|
|
87
|
+
} catch (e) {
|
|
88
|
+
console.warn('[oauth] Refresh failed, checking disk for updated tokens:', e.message);
|
|
89
|
+
const diskConfig = loadConfig({ resolve: false });
|
|
90
|
+
if (diskConfig?.anthropic?.oauth?.accessToken &&
|
|
91
|
+
diskConfig.anthropic.oauth.accessToken !== anthropicConfig.oauth.accessToken &&
|
|
92
|
+
!isExpired(diskConfig.anthropic.oauth)) {
|
|
93
|
+
anthropicConfig.oauth.accessToken = diskConfig.anthropic.oauth.accessToken;
|
|
94
|
+
anthropicConfig.oauth.refreshToken = diskConfig.anthropic.oauth.refreshToken;
|
|
95
|
+
anthropicConfig.oauth.expires = diskConfig.anthropic.oauth.expires;
|
|
96
|
+
delete anthropicConfig._oauthFailed;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (anthropicConfig.apiKey) {
|
|
101
|
+
console.warn('[oauth] Token refresh failed, falling back to API key:', e.message);
|
|
102
|
+
anthropicConfig._oauthFailed = true;
|
|
103
|
+
} else {
|
|
104
|
+
const err = new Error(`OAuth token expired and refresh failed: ${e.message}`);
|
|
105
|
+
err.isOAuthExpiry = true;
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
83
108
|
}
|
|
109
|
+
})();
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
await _refreshPromise;
|
|
113
|
+
} finally {
|
|
114
|
+
_refreshPromise = null;
|
|
84
115
|
}
|
|
85
116
|
}
|
|
86
117
|
|
package/src/cli/config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const inquirer = require('inquirer');
|
|
2
2
|
const { loadConfig, saveConfig, CONFIG_FILE, ensureUserDir, getUserDir } = require('../config');
|
|
3
|
+
const { generatePKCE, buildAuthorizationUrl, exchangeCodeForTokens } = require('../oauth');
|
|
3
4
|
const { spawnSync } = require('child_process');
|
|
4
5
|
const fs = require('fs');
|
|
5
6
|
|
|
@@ -8,8 +9,9 @@ const SECTIONS = [
|
|
|
8
9
|
name: 'Anthropic',
|
|
9
10
|
fields: [
|
|
10
11
|
{ key: 'anthropic.apiKey', label: 'API Key', secret: true },
|
|
11
|
-
{ key: '
|
|
12
|
-
{ key: 'anthropic.oauth.
|
|
12
|
+
{ key: '_oauth_flow', label: 'Set up / reset OAuth', custom: 'oauth' },
|
|
13
|
+
{ key: 'anthropic.oauth.accessToken', label: 'OAuth Access Token (manual)', secret: true },
|
|
14
|
+
{ key: 'anthropic.oauth.refreshToken', label: 'OAuth Refresh Token (manual)', secret: true },
|
|
13
15
|
],
|
|
14
16
|
},
|
|
15
17
|
{
|
|
@@ -266,6 +268,59 @@ async function manageUsers(cfg) {
|
|
|
266
268
|
}
|
|
267
269
|
}
|
|
268
270
|
|
|
271
|
+
async function runOAuthFlow(cfg) {
|
|
272
|
+
console.log('\n Starting OAuth flow with Anthropic...\n');
|
|
273
|
+
|
|
274
|
+
const { verifier, challenge } = await generatePKCE();
|
|
275
|
+
const authUrl = buildAuthorizationUrl(challenge, verifier);
|
|
276
|
+
|
|
277
|
+
console.log(' 1. Open this URL in your browser:\n');
|
|
278
|
+
console.log(` ${authUrl}\n`);
|
|
279
|
+
console.log(' 2. Authorize the app, then copy the FULL redirect URL from your browser.\n');
|
|
280
|
+
console.log(' It will look like: https://console.anthropic.com/oauth/code/callback?code=XXXXX#STATE\n');
|
|
281
|
+
|
|
282
|
+
const { callbackInput } = await inquirer.prompt([{
|
|
283
|
+
type: 'input',
|
|
284
|
+
name: 'callbackInput',
|
|
285
|
+
message: 'Paste the full callback URL or just the code:',
|
|
286
|
+
validate: (v) => v.trim().length > 0 ? true : 'Required',
|
|
287
|
+
}]);
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const input = callbackInput.trim();
|
|
291
|
+
let code, state;
|
|
292
|
+
|
|
293
|
+
if (input.includes('code=')) {
|
|
294
|
+
const url = new URL(input);
|
|
295
|
+
code = url.searchParams.get('code');
|
|
296
|
+
state = url.hash?.replace('#', '') || verifier;
|
|
297
|
+
} else if (input.includes('#')) {
|
|
298
|
+
[code, state] = input.split('#');
|
|
299
|
+
} else {
|
|
300
|
+
code = input;
|
|
301
|
+
state = verifier;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (!code) {
|
|
305
|
+
console.log(' No code found\n');
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
console.log(' Exchanging code for tokens...');
|
|
310
|
+
const tokens = await exchangeCodeForTokens(code, state, verifier);
|
|
311
|
+
|
|
312
|
+
setNestedValue(cfg, 'anthropic.oauth.accessToken', tokens.accessToken);
|
|
313
|
+
setNestedValue(cfg, 'anthropic.oauth.refreshToken', tokens.refreshToken);
|
|
314
|
+
setNestedValue(cfg, 'anthropic.oauth.expires', tokens.expires);
|
|
315
|
+
saveConfig(cfg);
|
|
316
|
+
|
|
317
|
+
console.log(' OAuth configured with access + refresh token');
|
|
318
|
+
console.log(` Token expires: ${new Date(tokens.expires).toISOString()}\n`);
|
|
319
|
+
} catch (e) {
|
|
320
|
+
console.log(` OAuth flow failed: ${e.message}\n`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
269
324
|
async function config() {
|
|
270
325
|
const cfg = loadConfig({ resolve: false });
|
|
271
326
|
if (!cfg) {
|
|
@@ -301,6 +356,20 @@ async function config() {
|
|
|
301
356
|
|
|
302
357
|
const fields = sec.fields;
|
|
303
358
|
const fieldChoices = fields.map(f => {
|
|
359
|
+
if (f.custom) {
|
|
360
|
+
const hasOAuth = !!getNestedValue(cfg, 'anthropic.oauth.accessToken');
|
|
361
|
+
const hasRefresh = !!getNestedValue(cfg, 'anthropic.oauth.refreshToken');
|
|
362
|
+
const expires = getNestedValue(cfg, 'anthropic.oauth.expires');
|
|
363
|
+
const expired = expires && Date.now() >= expires;
|
|
364
|
+
let status = '';
|
|
365
|
+
if (hasOAuth && hasRefresh && !expired) status = ' ✅';
|
|
366
|
+
else if (hasOAuth && expired) status = ' ⚠️ expired';
|
|
367
|
+
else if (hasOAuth && !hasRefresh) status = ' ⚠️ no refresh token';
|
|
368
|
+
return {
|
|
369
|
+
name: `${f.label}${status}`,
|
|
370
|
+
value: f,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
304
373
|
const val = getNestedValue(cfg, f.key);
|
|
305
374
|
return {
|
|
306
375
|
name: `${f.label}: ${formatValue(val, f.secret)}`,
|
|
@@ -317,6 +386,11 @@ async function config() {
|
|
|
317
386
|
|
|
318
387
|
if (!field) continue;
|
|
319
388
|
|
|
389
|
+
if (field.custom === 'oauth') {
|
|
390
|
+
await runOAuthFlow(cfg);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
320
394
|
const currentVal = getNestedValue(cfg, field.key);
|
|
321
395
|
|
|
322
396
|
if (field.type === 'boolean') {
|
package/src/cli/init.js
CHANGED
|
@@ -389,15 +389,9 @@ async function setupAnthropicOAuth() {
|
|
|
389
389
|
const input = callbackInput.trim();
|
|
390
390
|
|
|
391
391
|
if (input.includes('sk-ant-oat')) {
|
|
392
|
-
console.log('
|
|
393
|
-
console.log('
|
|
394
|
-
return
|
|
395
|
-
oauth: {
|
|
396
|
-
accessToken: input,
|
|
397
|
-
refreshToken: null,
|
|
398
|
-
expires: Date.now() + 60 * 60 * 1000,
|
|
399
|
-
},
|
|
400
|
-
};
|
|
392
|
+
console.log(' That\'s a raw token, not a callback URL.');
|
|
393
|
+
console.log(' Paste the full URL from your browser after authorizing.\n');
|
|
394
|
+
return await setupAnthropicOAuth();
|
|
401
395
|
}
|
|
402
396
|
|
|
403
397
|
let code, state;
|
package/src/oauth.js
CHANGED
|
@@ -82,10 +82,11 @@ async function refreshTokens(refreshToken) {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
const data = await res.json();
|
|
85
|
+
console.log(`[oauth] Refresh response: expires_in=${data.expires_in}, has_refresh=${!!data.refresh_token}`);
|
|
85
86
|
return {
|
|
86
87
|
accessToken: data.access_token,
|
|
87
|
-
refreshToken: data.refresh_token,
|
|
88
|
-
expires: Date.now() + data.expires_in * 1000 - REFRESH_BUFFER_MS,
|
|
88
|
+
refreshToken: data.refresh_token || null,
|
|
89
|
+
expires: Date.now() + (data.expires_in || 3600) * 1000 - REFRESH_BUFFER_MS,
|
|
89
90
|
};
|
|
90
91
|
}
|
|
91
92
|
|
package/src/telegram.js
CHANGED
|
@@ -392,7 +392,8 @@ function createBot(telegramConfig, config) {
|
|
|
392
392
|
stopTyping();
|
|
393
393
|
console.error('Message handling error:', e.message);
|
|
394
394
|
if (e.isOAuthExpiry) {
|
|
395
|
-
|
|
395
|
+
console.error('[oauth] Full error:', e.stack || e.message);
|
|
396
|
+
await ctx.reply(`OAuth error: ${e.message}\n\nRun \`obol config\` → Anthropic → re-authenticate OAuth.`).catch(() => {});
|
|
396
397
|
} else if (e.status === 401 || e.message?.includes('401')) {
|
|
397
398
|
await ctx.reply('API key invalid or expired. Run `obol config` to update.').catch(() => {});
|
|
398
399
|
} else if (e.status === 429 || e.message?.includes('rate')) {
|