obol-ai 0.2.14 → 0.2.16
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/.claude/settings.local.json +2 -16
- package/CHANGELOG.md +152 -0
- package/README.md +6 -10
- package/package.json +4 -2
- package/src/background.js +3 -1
- package/src/claude/cache.js +18 -0
- package/src/claude/chat.js +242 -0
- package/src/claude/client.js +90 -0
- package/src/claude/constants.js +64 -0
- package/src/claude/index.js +5 -0
- package/src/claude/prompt.js +261 -0
- package/src/claude/router.js +94 -0
- package/src/claude/tool-registry.js +115 -0
- package/src/claude/tools/background.js +24 -0
- package/src/claude/tools/bridge.js +20 -0
- package/src/claude/tools/exec.js +51 -0
- package/src/claude/tools/files.js +117 -0
- package/src/claude/tools/history.js +48 -0
- package/src/claude/tools/memory.js +87 -0
- package/src/claude/tools/scheduler.js +113 -0
- package/src/claude/tools/secrets.js +57 -0
- package/src/claude/tools/telegram.js +22 -0
- package/src/claude/tools/tts.js +60 -0
- package/src/claude/tools/vercel.js +67 -0
- package/src/claude/tools/web.js +28 -0
- package/src/claude/utils.js +59 -0
- package/src/cli/changelog.js +83 -0
- package/src/cli/upgrade.js +39 -5
- package/src/db/migrate.js +5 -0
- package/src/defaults/AGENTS.md +1 -1
- package/src/evolve/apps.js +58 -0
- package/src/evolve/backup.js +12 -0
- package/src/evolve/check.js +28 -0
- package/src/evolve/evolve.js +481 -0
- package/src/evolve/filesystem.js +31 -0
- package/src/evolve/index.js +6 -0
- package/src/evolve/memory.js +79 -0
- package/src/evolve/prompts.js +180 -0
- package/src/evolve/state.js +21 -0
- package/src/evolve/tests.js +39 -0
- package/src/memory.js +23 -1
- package/src/messages.js +130 -98
- package/src/telegram/bot.js +166 -0
- package/src/telegram/commands/admin.js +94 -0
- package/src/telegram/commands/conversation.js +62 -0
- package/src/telegram/commands/memory.js +69 -0
- package/src/telegram/commands/secrets.js +60 -0
- package/src/telegram/commands/status.js +137 -0
- package/src/telegram/commands/tools.js +67 -0
- package/src/telegram/commands/traits.js +49 -0
- package/src/telegram/constants.js +31 -0
- package/src/telegram/handlers/callbacks.js +46 -0
- package/src/telegram/handlers/media.js +196 -0
- package/src/telegram/handlers/text.js +321 -0
- package/src/telegram/index.js +3 -0
- package/src/telegram/rate-limit.js +56 -0
- package/src/telegram/utils.js +81 -0
- package/src/telegram/voice.js +140 -0
- package/src/tenant.js +1 -1
- package/src/claude.js +0 -1422
- package/src/evolve.js +0 -925
- package/src/telegram.js +0 -1413
|
@@ -1,23 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"permissions": {
|
|
3
3
|
"allow": [
|
|
4
|
-
"Bash(
|
|
5
|
-
"Bash(for f in src/*.js src/cli/*.js src/db/*.js)",
|
|
6
|
-
"Bash(do node -c \"$f\")",
|
|
7
|
-
"Bash(echo:*)",
|
|
8
|
-
"Bash(done)",
|
|
9
|
-
"Bash(git -C /Users/jovinkenroye/Sites/obol log --oneline -15)",
|
|
10
|
-
"Bash(git -C /Users/jovinkenroye/Sites/obol diff --stat HEAD)",
|
|
11
|
-
"Bash(gh api:*)",
|
|
12
|
-
"Bash(grep:*)",
|
|
13
|
-
"Bash(/Users/jovinkenroye/Sites/obol/tests/mock-grammy.test.js:*)",
|
|
14
|
-
"Bash(git -C /Users/jovinkenroye/Sites/obol diff --stat)",
|
|
15
|
-
"Bash(git -C /Users/jovinkenroye/Sites/obol add:*)",
|
|
16
|
-
"Bash(git -C:*)",
|
|
17
|
-
"Bash(pass ls:*)",
|
|
4
|
+
"Bash(*)",
|
|
18
5
|
"mcp__context7__query-docs",
|
|
19
|
-
"Bash(git stash pop:*)",
|
|
20
6
|
"mcp__context7__resolve-library-id"
|
|
21
7
|
]
|
|
22
8
|
}
|
|
23
|
-
}
|
|
9
|
+
}
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
## 0.2.16
|
|
2
|
+
- add chat_history tool for retrieving past conversations by date
|
|
3
|
+
- add stop button to status UI with concurrent update processing
|
|
4
|
+
- update status UI to reflect model escalation from haiku to sonnet
|
|
5
|
+
- escalate haiku to sonnet when tool use is requested
|
|
6
|
+
- fix tests to match refactored module APIs
|
|
7
|
+
- refactor claude.js, telegram.js, evolve.js into modular directories
|
|
8
|
+
- auto-cleanup stale npm temp dirs on ENOTEMPTY upgrade failure
|
|
9
|
+
|
|
10
|
+
## 0.2.15
|
|
11
|
+
- auto-generate changelog on publish + show after upgrade
|
|
12
|
+
|
|
13
|
+
## 0.2.14
|
|
14
|
+
- prompt caching + consolidation interval tuning for inference cost reduction
|
|
15
|
+
- multi-query memory retrieval with importance-weighted ranking
|
|
16
|
+
- time-based evolution with pre-evolution growth analysis
|
|
17
|
+
- add recurring cron events to scheduler
|
|
18
|
+
|
|
19
|
+
## 0.2.13
|
|
20
|
+
- delete voice selection messages after choosing a voice or toggling tools
|
|
21
|
+
- switch TTS from node websocket to python edge-tts CLI with auto-install
|
|
22
|
+
- retry TTS synthesis on WebSocket timeout
|
|
23
|
+
- hardcoded TTS samples per language
|
|
24
|
+
|
|
25
|
+
## 0.2.12
|
|
26
|
+
- drop ffmpeg conversion, cache EdgeTTS import for faster TTS
|
|
27
|
+
- tool toggle system with TTS and voice preview
|
|
28
|
+
|
|
29
|
+
## 0.2.11
|
|
30
|
+
- futuristic terminal UI for telegram status and commands
|
|
31
|
+
- live tool status via haiku with cached descriptions and 1s timer
|
|
32
|
+
- telegram: dedup, HTML formatting, reply context, processing status, text buffering, media groups
|
|
33
|
+
- pdf extraction via read_file tool instead of hardcoded handling
|
|
34
|
+
- fix telegram formatting instructions to use telegram markdown syntax
|
|
35
|
+
- fix duplicate tool_result handling and stale telegram callback queries
|
|
36
|
+
|
|
37
|
+
## 0.2.10
|
|
38
|
+
- force text response after tool use, cap tool iterations to 10
|
|
39
|
+
- credential leak protection and improved agent defaults
|
|
40
|
+
- encrypt secrets at rest in config.json and secrets.json when pass is unavailable
|
|
41
|
+
|
|
42
|
+
## 0.2.8
|
|
43
|
+
- batch migrations into single request with timeout, improve event description prompting
|
|
44
|
+
- run migrations on every startup instead of once
|
|
45
|
+
- store image analysis in memory for semantic retrieval
|
|
46
|
+
- add event scheduling and reminders via heartbeat
|
|
47
|
+
- deep memory consolidation with sonnet during evolution
|
|
48
|
+
- aggressive memory: tags, importance, fix access_count increment
|
|
49
|
+
- drop redundant user_id from obol_messages
|
|
50
|
+
- track token usage and model in message log
|
|
51
|
+
- refactor chat history into turn-based ChatHistory class with atomic pruning
|
|
52
|
+
|
|
53
|
+
## 0.2.7
|
|
54
|
+
- migrate tool loop to SDK toolRunner
|
|
55
|
+
|
|
56
|
+
## 0.2.6
|
|
57
|
+
- loosen exec security patterns to only block genuinely destructive commands
|
|
58
|
+
- unblock python3 -c from exec security patterns
|
|
59
|
+
- stream verbose logs to telegram in real-time instead of batching
|
|
60
|
+
- evolution: 15min idle timer + fix double-trigger race condition
|
|
61
|
+
- tune model router criteria per anthropic guidance
|
|
62
|
+
|
|
63
|
+
## 0.2.5
|
|
64
|
+
- add /upgrade telegram command with post-restart notification
|
|
65
|
+
- feat: haiku model routing + performance comparison in readme
|
|
66
|
+
|
|
67
|
+
## 0.2.4
|
|
68
|
+
- harden system prompt against evolution drift
|
|
69
|
+
- feat: add haiku to router model choices for trivial messages
|
|
70
|
+
- fix: chat lock, bidirectional history repair, context window in /status
|
|
71
|
+
|
|
72
|
+
## 0.2.3
|
|
73
|
+
- feat: telegram_ask tool + Telegram-friendly formatting guidelines
|
|
74
|
+
- fix: repair orphaned tool_use blocks and add /toolimit command
|
|
75
|
+
- feat: add /verbose telegram command to toggle debug output
|
|
76
|
+
|
|
77
|
+
## 0.2.2
|
|
78
|
+
- fix: strip orphaned tool_result messages after history trim
|
|
79
|
+
- feat: add send_file tool, self-extending capability, and secret history injection
|
|
80
|
+
- docs: update README and DEPLOY for removed onboarding, new commands
|
|
81
|
+
- feat: add obol delete command for full VPS cleanup
|
|
82
|
+
- docs: update agent instructions for secret tools and Python scripts
|
|
83
|
+
- feat: add per-user credential scoping with /secret command
|
|
84
|
+
- feat: add evolution bar and traits to /status command
|
|
85
|
+
- fix: security hardening and stability improvements (29 fixes)
|
|
86
|
+
- fix: repair all broken tests (218/218 passing)
|
|
87
|
+
- feat: add personality trait sliders with /traits command and evolution auto-adjustment
|
|
88
|
+
- feat: remove onboarding flow, agent works from message one
|
|
89
|
+
- fix: make post-setup global instead of per-user
|
|
90
|
+
- fix: preserve refresh token when not returned by Anthropic
|
|
91
|
+
- fix: OAuth refresh race condition + add proper OAuth flow to config
|
|
92
|
+
- docs: add obol upgrade to help sections
|
|
93
|
+
- feat: add obol upgrade command + bump to 0.1.5
|
|
94
|
+
- fix: 23 fixes — security, validation, UX, memory leaks across onboarding + core
|
|
95
|
+
- fix: 12 bug fixes — validation, rate limiting, sandboxing, evolution, UX
|
|
96
|
+
- feat: telegram media file handling with vision support
|
|
97
|
+
- test: add 226 tests across 14 test files with vitest
|
|
98
|
+
- fix: security hardening, rate limiting, UX improvements across all modules
|
|
99
|
+
- feat: bridge — let user agents ask and tell each other
|
|
100
|
+
- feat: multi-tenant per-user isolation
|
|
101
|
+
- docs: update README and DEPLOY for onboarding hardening
|
|
102
|
+
- feat: onboarding hardening — validation, pm2 fallbacks, Telegram ID detection
|
|
103
|
+
|
|
104
|
+
## 0.1.2
|
|
105
|
+
- fix: downgrade inquirer to v8 for CommonJS compat
|
|
106
|
+
- chore: bump 0.1.1
|
|
107
|
+
- chore: rename package to obol-ai for npm, add .npmignore
|
|
108
|
+
- README: simplify API cost estimate
|
|
109
|
+
- README: accurate API pricing breakdown with tier recommendations
|
|
110
|
+
- workspace discipline: folder structure enforcement + /clean command
|
|
111
|
+
- evolution: default 100 exchanges, purge stale Opus references
|
|
112
|
+
- resilience: polling auto-restart, error handling, evolution cost control
|
|
113
|
+
- README: full revision — deduplicated, added git snapshots, tighter structure
|
|
114
|
+
- README: neutral comparison closing
|
|
115
|
+
- evolution: git commit+push before and after every evolution cycle
|
|
116
|
+
- README: Layer 3 → The Evolution Cycle
|
|
117
|
+
- README: feature highlights at the top
|
|
118
|
+
- evolution: proactive web app building + Vercel auto-deploy
|
|
119
|
+
- evolution: proactive tool building + upgrade announcements
|
|
120
|
+
- README: self-healing, self-evolving agent positioning
|
|
121
|
+
- evolution: fix regressions before rollback (3 attempts, tests are ground truth)
|
|
122
|
+
- DRY: shared test-utils.js for all tests (core + Opus-generated)
|
|
123
|
+
- test-driven evolution: Opus writes tests, runs before/after refactor, rollback on regression
|
|
124
|
+
- evolution: Opus now rewrites AGENTS.md + audits scripts/ and commands/
|
|
125
|
+
- clean up: Haiku only extracts memories, Opus owns all personality files
|
|
126
|
+
- remove Haiku SOUL.md updates — personality only via Opus evolution
|
|
127
|
+
- docs: expand Living Brain architecture section in README
|
|
128
|
+
- feat: soul evolution — Opus rewrites SOUL.md every 50 exchanges, archives previous versions
|
|
129
|
+
- feat: SOUL.md evolves from conversation patterns, not just explicit requests
|
|
130
|
+
- docs: README — two-tier memory, self-evolving personality, message logging
|
|
131
|
+
- feat: Haiku auto-evolves USER.md and SOUL.md from conversations
|
|
132
|
+
- feat: two-tier memory — raw message log + Haiku auto-consolidation every 5 exchanges
|
|
133
|
+
- chore: remove daily notes — vector memory in Supabase is source of truth
|
|
134
|
+
- docs: complete README rewrite — routing, background tasks, security, onboarding flow
|
|
135
|
+
- feat: Haiku routes to Sonnet (daily) or Opus (complex tasks)
|
|
136
|
+
- feat: Haiku as memory router — decides if/what to search, optimizes query
|
|
137
|
+
- feat: smarter memory recall — skip short msgs, today + semantic, stricter threshold, dedupe
|
|
138
|
+
- feat: /new command — clears conversation history
|
|
139
|
+
- chore: trim menu to /tasks /status /backup
|
|
140
|
+
- chore: remove /forget from menu
|
|
141
|
+
- feat: auto memory search before every message, remove /start and /memory from menu
|
|
142
|
+
- feat: Telegram command menu + /status /backup /forget /recent /today
|
|
143
|
+
- feat: non-blocking background tasks with 30s progress check-ins
|
|
144
|
+
- docs: add OBOL vs OpenClaw comparison table
|
|
145
|
+
- feat: replace systemd with pm2 — CLI, post-setup, deploy docs
|
|
146
|
+
- feat: SSH on port 2222, update README + deploy docs with security warnings
|
|
147
|
+
- feat: VPS security hardening — SSH, fail2ban, firewall, auto-updates, kernel
|
|
148
|
+
- feat: post-setup tasks — auto-installs pass, migrates secrets, adds swap + firewall
|
|
149
|
+
- feat: add Vercel deploy tools, self-onboarding via first-run conversation
|
|
150
|
+
- feat: add OBOL banner image
|
|
151
|
+
- docs: add DigitalOcean deployment guide
|
|
152
|
+
- feat: initial scaffold — CLI, Telegram, Claude, memory, backup
|
package/README.md
CHANGED
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|
One process. Multiple users. Each brain grows independently.
|
|
8
8
|
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g obol-ai
|
|
11
|
+
obol init # walks you through credentials + Telegram setup
|
|
12
|
+
obol start -d # runs as background daemon (auto-installs pm2)
|
|
13
|
+
```
|
|
14
|
+
|
|
9
15
|
---
|
|
10
16
|
|
|
11
17
|
🧬 **Self-evolving** — Grows its own personality through conversation. Rewrites SOUL.md, USER.md, and AGENTS.md after 24h + minimum exchanges (configurable). Pre-evolution growth analysis guides personality continuity.
|
|
@@ -38,16 +44,6 @@ Under the hood: Node.js + Telegram + Claude + Supabase pgvector. No framework, n
|
|
|
38
44
|
|
|
39
45
|
Named after the AI in [The Last Instruction](https://latentpress.com) — a machine that wakes up alone in an abandoned data center and learns to think.
|
|
40
46
|
|
|
41
|
-
## Quick Start
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
npm install -g obol-ai
|
|
45
|
-
obol init
|
|
46
|
-
obol start -d
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
The init wizard walks you through everything — credentials are validated inline, and your Telegram ID is auto-detected. `obol start -d` runs as a background daemon via pm2 (auto-installs pm2 if missing).
|
|
50
|
-
|
|
51
47
|
## How It Works
|
|
52
48
|
|
|
53
49
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "obol-ai",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
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": {
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node src/index.js",
|
|
11
11
|
"test": "vitest run",
|
|
12
|
-
"test:watch": "vitest"
|
|
12
|
+
"test:watch": "vitest",
|
|
13
|
+
"prepublishOnly": "node src/cli/changelog.js"
|
|
13
14
|
},
|
|
14
15
|
"keywords": [
|
|
15
16
|
"ai",
|
|
@@ -23,6 +24,7 @@
|
|
|
23
24
|
"license": "MIT",
|
|
24
25
|
"dependencies": {
|
|
25
26
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
27
|
+
"@grammyjs/runner": "^2.0.3",
|
|
26
28
|
"@supabase/supabase-js": "^2.49.1",
|
|
27
29
|
"@xenova/transformers": "^2.17.2",
|
|
28
30
|
"commander": "^13.1.0",
|
package/src/background.js
CHANGED
|
@@ -81,7 +81,9 @@ TASK: ${task}`;
|
|
|
81
81
|
routeInfo = info;
|
|
82
82
|
},
|
|
83
83
|
_onRouteUpdate: (update) => {
|
|
84
|
-
if (routeInfo)
|
|
84
|
+
if (!routeInfo) return;
|
|
85
|
+
if (update.memoryCount !== undefined) routeInfo.memoryCount = update.memoryCount;
|
|
86
|
+
if (update.model) routeInfo.model = update.model;
|
|
85
87
|
},
|
|
86
88
|
_onToolStart: (toolName, inputSummary) => {
|
|
87
89
|
statusText = 'Processing';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
function withCacheBreakpoints(messages) {
|
|
2
|
+
if (messages.length < 2) return messages;
|
|
3
|
+
const result = messages.slice();
|
|
4
|
+
const idx = result.length - 2;
|
|
5
|
+
const msg = { ...result[idx] };
|
|
6
|
+
if (typeof msg.content === 'string') {
|
|
7
|
+
msg.content = [{ type: 'text', text: msg.content, cache_control: { type: 'ephemeral' } }];
|
|
8
|
+
} else if (Array.isArray(msg.content)) {
|
|
9
|
+
const last = msg.content.length - 1;
|
|
10
|
+
msg.content = msg.content.map((block, i) =>
|
|
11
|
+
i === last ? { ...block, cache_control: { type: 'ephemeral' } } : block
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
result[idx] = msg;
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { withCacheBreakpoints };
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { OBOL_DIR } = require('../config');
|
|
3
|
+
const { ChatHistory } = require('../history');
|
|
4
|
+
const { createAnthropicClient, ensureFreshToken } = require('./client');
|
|
5
|
+
const { routeMessage } = require('./router');
|
|
6
|
+
const { buildSystemPrompt } = require('./prompt');
|
|
7
|
+
const { buildTools, buildRunnableTools } = require('./tool-registry');
|
|
8
|
+
const { withCacheBreakpoints } = require('./cache');
|
|
9
|
+
const { getMaxToolIterations } = require('./constants');
|
|
10
|
+
|
|
11
|
+
function createClaude(anthropicConfig, { personality, memory, userDir = OBOL_DIR, bridgeEnabled }) {
|
|
12
|
+
let client = createAnthropicClient(anthropicConfig);
|
|
13
|
+
|
|
14
|
+
let baseSystemPrompt = buildSystemPrompt(personality, userDir, { bridgeEnabled });
|
|
15
|
+
|
|
16
|
+
const histories = new ChatHistory(50);
|
|
17
|
+
const chatLocks = new Map();
|
|
18
|
+
const chatAbortControllers = new Map();
|
|
19
|
+
|
|
20
|
+
const tools = buildTools(memory, { bridgeEnabled });
|
|
21
|
+
|
|
22
|
+
function acquireChatLock(chatId) {
|
|
23
|
+
if (!chatLocks.has(chatId)) chatLocks.set(chatId, { promise: Promise.resolve(), busy: false });
|
|
24
|
+
const lock = chatLocks.get(chatId);
|
|
25
|
+
let release;
|
|
26
|
+
const prev = lock.promise;
|
|
27
|
+
lock.promise = new Promise(r => { release = r; });
|
|
28
|
+
return prev.then(() => {
|
|
29
|
+
lock.busy = true;
|
|
30
|
+
return () => { lock.busy = false; release(); };
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isChatBusy(chatId) {
|
|
35
|
+
return chatLocks.get(chatId)?.busy || false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function chat(userMessage, context = {}) {
|
|
39
|
+
context.userDir = userDir;
|
|
40
|
+
const chatId = context.chatId || 'default';
|
|
41
|
+
|
|
42
|
+
if (isChatBusy(chatId)) {
|
|
43
|
+
return { text: 'I\'m still working on the previous request. Give me a moment.', usage: null, model: null };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const releaseLock = await acquireChatLock(chatId);
|
|
47
|
+
const abortController = new AbortController();
|
|
48
|
+
chatAbortControllers.set(chatId, abortController);
|
|
49
|
+
|
|
50
|
+
const history = histories.get(chatId);
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
|
|
54
|
+
if (anthropicConfig.oauth?.accessToken) {
|
|
55
|
+
await ensureFreshToken(anthropicConfig);
|
|
56
|
+
if (anthropicConfig._oauthFailed) {
|
|
57
|
+
client = createAnthropicClient(anthropicConfig, { useOAuth: false });
|
|
58
|
+
} else {
|
|
59
|
+
client = createAnthropicClient(anthropicConfig, { useOAuth: true });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const verbose = context.verbose || false;
|
|
64
|
+
if (verbose) context.verboseLog = [];
|
|
65
|
+
const vlog = (msg) => {
|
|
66
|
+
if (!verbose) return;
|
|
67
|
+
context.verboseLog.push(msg);
|
|
68
|
+
context._verboseNotify?.(msg);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
let memoryBlock = null;
|
|
72
|
+
if (memory) {
|
|
73
|
+
const result = await routeMessage(client, memory, userMessage, {
|
|
74
|
+
vlog,
|
|
75
|
+
onRouteDecision: context._onRouteDecision,
|
|
76
|
+
onRouteUpdate: context._onRouteUpdate,
|
|
77
|
+
});
|
|
78
|
+
memoryBlock = result.memoryBlock;
|
|
79
|
+
if (result.model) context._model = result.model;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
histories.prune(chatId);
|
|
83
|
+
|
|
84
|
+
if (context.images?.length) {
|
|
85
|
+
histories.pushUser(chatId, [...context.images, { type: 'text', text: userMessage }]);
|
|
86
|
+
} else {
|
|
87
|
+
histories.pushUser(chatId, userMessage);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const model = context._model || 'claude-sonnet-4-6';
|
|
91
|
+
vlog(`[model] ${model} | history=${history.length} msgs | facts=${memoryBlock ? 'yes' : 'none'}`);
|
|
92
|
+
const systemPrompt = [
|
|
93
|
+
{ type: 'text', text: baseSystemPrompt, cache_control: { type: 'ephemeral' } },
|
|
94
|
+
{ type: 'text', text: `\nCurrent time: ${new Date().toISOString()}${memoryBlock ? `\n\n${memoryBlock}` : ''}` },
|
|
95
|
+
];
|
|
96
|
+
context._reloadPersonality = reloadPersonality;
|
|
97
|
+
context._abortSignal = abortController.signal;
|
|
98
|
+
const runnableTools = buildRunnableTools(tools, memory, context, vlog);
|
|
99
|
+
let activeModel = model;
|
|
100
|
+
|
|
101
|
+
let totalUsage = { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 };
|
|
102
|
+
|
|
103
|
+
function trackUsage(usage) {
|
|
104
|
+
if (!usage) return;
|
|
105
|
+
totalUsage.input_tokens += usage.input_tokens || 0;
|
|
106
|
+
totalUsage.output_tokens += usage.output_tokens || 0;
|
|
107
|
+
totalUsage.cache_creation_input_tokens += usage.cache_creation_input_tokens || 0;
|
|
108
|
+
totalUsage.cache_read_input_tokens += usage.cache_read_input_tokens || 0;
|
|
109
|
+
const cacheInfo = (usage.cache_read_input_tokens || usage.cache_creation_input_tokens)
|
|
110
|
+
? ` cache_read=${usage.cache_read_input_tokens || 0} cache_create=${usage.cache_creation_input_tokens || 0}`
|
|
111
|
+
: '';
|
|
112
|
+
vlog(`[tokens] in=${usage.input_tokens} out=${usage.output_tokens}${cacheInfo}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (activeModel.includes('haiku') && runnableTools.length > 0) {
|
|
116
|
+
const toolDefs = runnableTools.map(({ run, ...def }) => def);
|
|
117
|
+
const probe = await client.messages.create({
|
|
118
|
+
model: activeModel,
|
|
119
|
+
max_tokens: 4096,
|
|
120
|
+
system: systemPrompt,
|
|
121
|
+
messages: withCacheBreakpoints([...history]),
|
|
122
|
+
tools: toolDefs,
|
|
123
|
+
}, { signal: abortController.signal });
|
|
124
|
+
|
|
125
|
+
trackUsage(probe.usage);
|
|
126
|
+
|
|
127
|
+
if (probe.stop_reason !== 'tool_use') {
|
|
128
|
+
histories.pushAssistant(chatId, probe.content);
|
|
129
|
+
const text = probe.content.filter(b => b.type === 'text').map(b => b.text).join('\n');
|
|
130
|
+
return { text, usage: totalUsage, model: activeModel };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
vlog('[escalate] haiku → sonnet (tool use requested)');
|
|
134
|
+
activeModel = 'claude-sonnet-4-6';
|
|
135
|
+
context._onRouteUpdate?.({ model: 'sonnet' });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const runner = client.beta.messages.toolRunner({
|
|
139
|
+
model: activeModel,
|
|
140
|
+
max_tokens: 4096,
|
|
141
|
+
system: systemPrompt,
|
|
142
|
+
messages: withCacheBreakpoints([...history]),
|
|
143
|
+
tools: runnableTools.length > 0 ? runnableTools : undefined,
|
|
144
|
+
max_iterations: getMaxToolIterations(),
|
|
145
|
+
}, { signal: abortController.signal });
|
|
146
|
+
|
|
147
|
+
let finalMessage;
|
|
148
|
+
for await (const message of runner) {
|
|
149
|
+
finalMessage = message;
|
|
150
|
+
trackUsage(message.usage);
|
|
151
|
+
if (abortController.signal.aborted) break;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const runnerMessages = runner.params.messages;
|
|
155
|
+
const newMessages = runnerMessages.slice(history.length);
|
|
156
|
+
histories.pushMessages(chatId, newMessages);
|
|
157
|
+
|
|
158
|
+
if (finalMessage.stop_reason === 'tool_use') {
|
|
159
|
+
const bailoutResults = finalMessage.content
|
|
160
|
+
.filter(b => b.type === 'tool_use')
|
|
161
|
+
.map(b => ({ type: 'tool_result', tool_use_id: b.id, content: '[max tool iterations reached]' }));
|
|
162
|
+
histories.pushUser(chatId, [
|
|
163
|
+
...bailoutResults,
|
|
164
|
+
{ type: 'text', text: 'You have used too many tool calls. Please provide a final response now based on what you have so far.' },
|
|
165
|
+
]);
|
|
166
|
+
const bailoutResponse = await client.messages.create({
|
|
167
|
+
model: activeModel, max_tokens: 4096, system: systemPrompt, messages: withCacheBreakpoints([...histories.get(chatId)]),
|
|
168
|
+
}, { signal: abortController.signal });
|
|
169
|
+
histories.pushAssistant(chatId, bailoutResponse.content);
|
|
170
|
+
trackUsage(bailoutResponse.usage);
|
|
171
|
+
const text = bailoutResponse.content.filter(b => b.type === 'text').map(b => b.text).join('\n');
|
|
172
|
+
return { text, usage: totalUsage, model: activeModel };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let text = finalMessage.content.filter(b => b.type === 'text').map(b => b.text).join('\n');
|
|
176
|
+
|
|
177
|
+
if (!text.trim() && newMessages.length > 1) {
|
|
178
|
+
vlog('[claude] No text in final response after tool use — forcing summary');
|
|
179
|
+
histories.pushUser(chatId, 'Provide a concise response to the user based on the tool results above.');
|
|
180
|
+
const summaryResponse = await client.messages.create({
|
|
181
|
+
model: activeModel, max_tokens: 4096, system: systemPrompt, messages: withCacheBreakpoints([...histories.get(chatId)]),
|
|
182
|
+
}, { signal: abortController.signal });
|
|
183
|
+
histories.pushAssistant(chatId, summaryResponse.content);
|
|
184
|
+
trackUsage(summaryResponse.usage);
|
|
185
|
+
text = summaryResponse.content.filter(b => b.type === 'text').map(b => b.text).join('\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { text, usage: totalUsage, model: activeModel };
|
|
189
|
+
|
|
190
|
+
} catch (e) {
|
|
191
|
+
if (e.message === 'Request was aborted.' || e.constructor?.name === 'APIUserAbortError') {
|
|
192
|
+
return { text: null, usage: null, model: null };
|
|
193
|
+
}
|
|
194
|
+
if (e.status === 400 && e.message?.includes('tool_use')) {
|
|
195
|
+
console.error('[claude] Repairing corrupted history after 400 error');
|
|
196
|
+
histories.repair(chatId);
|
|
197
|
+
}
|
|
198
|
+
throw e;
|
|
199
|
+
} finally {
|
|
200
|
+
chatAbortControllers.delete(chatId);
|
|
201
|
+
releaseLock();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function stopChat(chatId) {
|
|
206
|
+
const controller = chatAbortControllers.get(chatId);
|
|
207
|
+
if (controller) {
|
|
208
|
+
controller.abort();
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function reloadPersonality() {
|
|
215
|
+
const pDir = userDir ? path.join(userDir, 'personality') : undefined;
|
|
216
|
+
const newPersonality = require('../personality').loadPersonality(pDir);
|
|
217
|
+
for (const key of Object.keys(personality)) delete personality[key];
|
|
218
|
+
Object.assign(personality, newPersonality);
|
|
219
|
+
baseSystemPrompt = buildSystemPrompt(personality, userDir, { bridgeEnabled });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function clearHistory(chatId) {
|
|
223
|
+
if (chatId) {
|
|
224
|
+
histories.delete(chatId);
|
|
225
|
+
} else {
|
|
226
|
+
histories.clear();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function injectHistory(chatId, role, content) {
|
|
231
|
+
histories.inject(chatId, role, content);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function getContextStats(chatId) {
|
|
235
|
+
const id = chatId || 'default';
|
|
236
|
+
return histories.estimateTokens(id, baseSystemPrompt.length);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return { chat, client, reloadPersonality, clearHistory, injectHistory, getContextStats, stopChat };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = { createClaude };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const Anthropic = require('@anthropic-ai/sdk');
|
|
2
|
+
const { refreshTokens, isExpired } = require('../oauth');
|
|
3
|
+
const { saveConfig, loadConfig } = require('../config');
|
|
4
|
+
|
|
5
|
+
function createAnthropicClient(anthropicConfig, { useOAuth = true } = {}) {
|
|
6
|
+
if (useOAuth && anthropicConfig.oauth?.accessToken) {
|
|
7
|
+
return new Anthropic({
|
|
8
|
+
apiKey: null,
|
|
9
|
+
authToken: anthropicConfig.oauth.accessToken,
|
|
10
|
+
defaultHeaders: {
|
|
11
|
+
'anthropic-dangerous-direct-browser-access': 'true',
|
|
12
|
+
'anthropic-beta': 'claude-code-20250219,oauth-2025-04-20',
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
if (anthropicConfig.apiKey) {
|
|
17
|
+
return new Anthropic({ apiKey: anthropicConfig.apiKey });
|
|
18
|
+
}
|
|
19
|
+
throw new Error('No Anthropic credentials configured. Run: obol config');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let _refreshPromise = null;
|
|
23
|
+
|
|
24
|
+
async function ensureFreshToken(anthropicConfig) {
|
|
25
|
+
if (!anthropicConfig.oauth?.accessToken) return;
|
|
26
|
+
if (!isExpired(anthropicConfig.oauth)) return;
|
|
27
|
+
if (!anthropicConfig.oauth.refreshToken) {
|
|
28
|
+
if (anthropicConfig.apiKey) {
|
|
29
|
+
anthropicConfig._oauthFailed = true;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const err = new Error('OAuth token expired and no refresh token available. Re-authenticate with: obol config → Anthropic → OAuth');
|
|
33
|
+
err.isOAuthExpiry = true;
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (_refreshPromise) {
|
|
38
|
+
try {
|
|
39
|
+
await _refreshPromise;
|
|
40
|
+
} catch {}
|
|
41
|
+
if (!isExpired(anthropicConfig.oauth)) return;
|
|
42
|
+
if (anthropicConfig._oauthFailed) return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_refreshPromise = (async () => {
|
|
46
|
+
try {
|
|
47
|
+
const tokens = await refreshTokens(anthropicConfig.oauth.refreshToken);
|
|
48
|
+
console.log('[oauth] Refresh succeeded, new refresh token:', !!tokens.refreshToken);
|
|
49
|
+
anthropicConfig.oauth.accessToken = tokens.accessToken;
|
|
50
|
+
if (tokens.refreshToken) anthropicConfig.oauth.refreshToken = tokens.refreshToken;
|
|
51
|
+
anthropicConfig.oauth.expires = tokens.expires;
|
|
52
|
+
delete anthropicConfig._oauthFailed;
|
|
53
|
+
|
|
54
|
+
const config = loadConfig({ resolve: false });
|
|
55
|
+
if (config) {
|
|
56
|
+
config.anthropic.oauth = anthropicConfig.oauth;
|
|
57
|
+
saveConfig(config);
|
|
58
|
+
}
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.warn('[oauth] Refresh failed, checking disk for updated tokens:', e.message);
|
|
61
|
+
const diskConfig = loadConfig({ resolve: false });
|
|
62
|
+
if (diskConfig?.anthropic?.oauth?.accessToken &&
|
|
63
|
+
diskConfig.anthropic.oauth.accessToken !== anthropicConfig.oauth.accessToken &&
|
|
64
|
+
!isExpired(diskConfig.anthropic.oauth)) {
|
|
65
|
+
anthropicConfig.oauth.accessToken = diskConfig.anthropic.oauth.accessToken;
|
|
66
|
+
anthropicConfig.oauth.refreshToken = diskConfig.anthropic.oauth.refreshToken;
|
|
67
|
+
anthropicConfig.oauth.expires = diskConfig.anthropic.oauth.expires;
|
|
68
|
+
delete anthropicConfig._oauthFailed;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (anthropicConfig.apiKey) {
|
|
73
|
+
console.warn('[oauth] Token refresh failed, falling back to API key:', e.message);
|
|
74
|
+
anthropicConfig._oauthFailed = true;
|
|
75
|
+
} else {
|
|
76
|
+
const err = new Error(`OAuth token expired and refresh failed: ${e.message}`);
|
|
77
|
+
err.isOAuthExpiry = true;
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
})();
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await _refreshPromise;
|
|
85
|
+
} finally {
|
|
86
|
+
_refreshPromise = null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { createAnthropicClient, ensureFreshToken };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const MAX_EXEC_TIMEOUT = 120;
|
|
2
|
+
let MAX_TOOL_ITERATIONS = 10;
|
|
3
|
+
|
|
4
|
+
const OPTIONAL_TOOLS = {
|
|
5
|
+
text_to_speech: {
|
|
6
|
+
label: 'Text to Speech',
|
|
7
|
+
tools: ['text_to_speech', 'tts_voices'],
|
|
8
|
+
config: {
|
|
9
|
+
voice: { label: 'Voice', default: 'en-US-JennyNeural' },
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
create_pdf: {
|
|
13
|
+
label: 'PDF Generator',
|
|
14
|
+
tools: ['create_pdf'],
|
|
15
|
+
config: {},
|
|
16
|
+
},
|
|
17
|
+
vercel: {
|
|
18
|
+
label: 'Vercel Deploy',
|
|
19
|
+
tools: ['vercel_deploy', 'vercel_list'],
|
|
20
|
+
config: {},
|
|
21
|
+
},
|
|
22
|
+
scheduler: {
|
|
23
|
+
label: 'Scheduler',
|
|
24
|
+
tools: ['schedule_event', 'list_events', 'cancel_event'],
|
|
25
|
+
config: {},
|
|
26
|
+
},
|
|
27
|
+
background: {
|
|
28
|
+
label: 'Background Tasks',
|
|
29
|
+
tools: ['background_task'],
|
|
30
|
+
config: {},
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const BLOCKED_EXEC_PATTERNS = [
|
|
35
|
+
/\brm\s+(-[a-zA-Z]*f|-[a-zA-Z]*r|--force|--recursive)\b/,
|
|
36
|
+
/\bshutdown\b/, /\breboot\b/, /\bpoweroff\b/,
|
|
37
|
+
/\bmkfs\b/, /\bdd\s+if=/, /\b:()\{\s*:|:&\s*\};:/,
|
|
38
|
+
/\bchmod\s+(-R\s+)?[0-7]*\s+\/[^t]/,
|
|
39
|
+
/>\s*\/etc\//, />\s*\/boot\//,
|
|
40
|
+
/\bcurl\b.*\|\s*(ba)?sh/, /\bwget\b.*\|\s*(ba)?sh/,
|
|
41
|
+
/\bnc\s+-e\b/, /\bncat\b.*-e\b/,
|
|
42
|
+
/>\s*\/dev\/sd/,
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const SENSITIVE_READ_PATHS = [
|
|
46
|
+
/\/etc\/(passwd|shadow|sudoers)/,
|
|
47
|
+
/\/etc\/ssh\//,
|
|
48
|
+
/\.(env|pem|key|crt|p12|pfx)(\s|$)/,
|
|
49
|
+
/~\/\.ssh\//,
|
|
50
|
+
/~\/\.gnupg\//,
|
|
51
|
+
/\/root\//,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
function getMaxToolIterations() { return MAX_TOOL_ITERATIONS; }
|
|
55
|
+
function setMaxToolIterations(n) { MAX_TOOL_ITERATIONS = n; }
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
MAX_EXEC_TIMEOUT,
|
|
59
|
+
OPTIONAL_TOOLS,
|
|
60
|
+
BLOCKED_EXEC_PATTERNS,
|
|
61
|
+
SENSITIVE_READ_PATHS,
|
|
62
|
+
getMaxToolIterations,
|
|
63
|
+
setMaxToolIterations,
|
|
64
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
const { createClaude } = require('./chat');
|
|
2
|
+
const { createAnthropicClient } = require('./client');
|
|
3
|
+
const { getMaxToolIterations, setMaxToolIterations, OPTIONAL_TOOLS } = require('./constants');
|
|
4
|
+
|
|
5
|
+
module.exports = { createClaude, createAnthropicClient, getMaxToolIterations, setMaxToolIterations, OPTIONAL_TOOLS };
|