claudity 1.1.0 → 1.2.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/install.sh +6 -1
- package/package.json +1 -1
- package/public/css/style.css +74 -7
- package/public/index.html +20 -2
- package/public/js/app.js +50 -31
- package/src/db.js +20 -0
- package/src/routes/api.js +21 -3
- package/src/services/auth.js +26 -3
- package/src/services/chat.js +49 -63
- package/src/services/claude.js +110 -35
- package/src/services/discord.js +30 -7
- package/src/services/imessage.js +9 -1
- package/src/services/signal.js +10 -1
- package/src/services/slack.js +33 -5
- package/src/services/telegram.js +31 -5
- package/src/services/tools.js +1 -1
- package/src/services/whatsapp.js +15 -5
- package/.github/workflows/publish.yml +0 -18
package/install.sh
CHANGED
|
@@ -326,8 +326,13 @@ step "setting up auto-start"
|
|
|
326
326
|
PLIST_NAME="ai.claudity.server"
|
|
327
327
|
PLIST_PATH="$HOME/Library/LaunchAgents/${PLIST_NAME}.plist"
|
|
328
328
|
NODE_PATH="$(which node)"
|
|
329
|
+
CLAUDE_PATH="$(which claude 2>/dev/null || echo "")"
|
|
329
330
|
mkdir -p "$INSTALL_DIR/data"
|
|
330
331
|
|
|
332
|
+
PLIST_PATH_DIRS="/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin"
|
|
333
|
+
[ -n "$NODE_PATH" ] && PLIST_PATH_DIRS="$(dirname "$NODE_PATH"):${PLIST_PATH_DIRS}"
|
|
334
|
+
[ -n "$CLAUDE_PATH" ] && PLIST_PATH_DIRS="$(dirname "$CLAUDE_PATH"):${PLIST_PATH_DIRS}"
|
|
335
|
+
|
|
331
336
|
launchctl bootout "gui/$(id -u)/${PLIST_NAME}" 2>/dev/null || true
|
|
332
337
|
|
|
333
338
|
cat > "$PLIST_PATH" <<PLIST
|
|
@@ -355,7 +360,7 @@ cat > "$PLIST_PATH" <<PLIST
|
|
|
355
360
|
<key>EnvironmentVariables</key>
|
|
356
361
|
<dict>
|
|
357
362
|
<key>PATH</key>
|
|
358
|
-
<string
|
|
363
|
+
<string>${PLIST_PATH_DIRS}</string>
|
|
359
364
|
</dict>
|
|
360
365
|
</dict>
|
|
361
366
|
</plist>
|
package/package.json
CHANGED
package/public/css/style.css
CHANGED
|
@@ -765,13 +765,6 @@ div[aria-label="messages"] > div[data-activity] [data-status] {
|
|
|
765
765
|
font-size: 11px;
|
|
766
766
|
}
|
|
767
767
|
|
|
768
|
-
div[aria-label="messages"] > div[data-activity] [data-summary] {
|
|
769
|
-
color: var(--accent);
|
|
770
|
-
font-size: 11px;
|
|
771
|
-
margin-top: 4px;
|
|
772
|
-
opacity: 0.7;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
768
|
div[aria-label="messages"] > div[data-activity] [data-stop] {
|
|
776
769
|
background: transparent;
|
|
777
770
|
border: 1px solid var(--bg-hover);
|
|
@@ -790,6 +783,12 @@ div[aria-label="messages"] > div[data-activity] [data-stop]:hover {
|
|
|
790
783
|
color: var(--danger);
|
|
791
784
|
}
|
|
792
785
|
|
|
786
|
+
div[aria-label="messages"] > div[data-activity] [data-watchdog-warn] {
|
|
787
|
+
color: #f59e0b;
|
|
788
|
+
font-size: 11px;
|
|
789
|
+
margin: 0.3rem 0 0;
|
|
790
|
+
}
|
|
791
|
+
|
|
793
792
|
div[aria-label="messages"] > div[data-role="system"] {
|
|
794
793
|
color: var(--muted);
|
|
795
794
|
font-size: 11px;
|
|
@@ -797,6 +796,69 @@ div[aria-label="messages"] > div[data-role="system"] {
|
|
|
797
796
|
font-style: italic;
|
|
798
797
|
}
|
|
799
798
|
|
|
799
|
+
[data-usage-bar] {
|
|
800
|
+
display: flex;
|
|
801
|
+
gap: 1.5rem;
|
|
802
|
+
padding: 0.5rem 1.5rem;
|
|
803
|
+
font-size: 10px;
|
|
804
|
+
color: var(--muted);
|
|
805
|
+
font-variant-numeric: tabular-nums;
|
|
806
|
+
flex-shrink: 0;
|
|
807
|
+
border-top: 1px solid var(--bg-raised);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
[data-usage-bar][hidden] {
|
|
811
|
+
display: none;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
[data-usage-bar] > div {
|
|
815
|
+
flex: 1;
|
|
816
|
+
min-width: 0;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
[data-usage-bar] [data-quota-header] {
|
|
820
|
+
display: flex;
|
|
821
|
+
justify-content: space-between;
|
|
822
|
+
margin-bottom: 3px;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
[data-usage-bar] [data-quota-header] span:first-child {
|
|
826
|
+
color: var(--white);
|
|
827
|
+
font-weight: 600;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
[data-usage-bar] [data-quota-pct] {
|
|
831
|
+
color: var(--white);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
[data-usage-bar] [data-quota-track] {
|
|
835
|
+
height: 6px;
|
|
836
|
+
background: var(--bg-raised);
|
|
837
|
+
border-radius: 3px;
|
|
838
|
+
overflow: hidden;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
[data-usage-bar] [data-quota-fill] {
|
|
842
|
+
height: 100%;
|
|
843
|
+
border-radius: 3px;
|
|
844
|
+
background: var(--accent);
|
|
845
|
+
transition: width 0.4s ease, background 0.3s;
|
|
846
|
+
width: 0%;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
[data-usage-bar] [data-quota-fill][data-warn] {
|
|
850
|
+
background: var(--amber);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
[data-usage-bar] [data-quota-fill][data-danger] {
|
|
854
|
+
background: var(--danger);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
[data-usage-bar] [data-quota-reset] {
|
|
858
|
+
margin-top: 2px;
|
|
859
|
+
font-size: 9px;
|
|
860
|
+
}
|
|
861
|
+
|
|
800
862
|
form[aria-label="input"] {
|
|
801
863
|
display: flex;
|
|
802
864
|
gap: 0.5rem;
|
|
@@ -1493,6 +1555,11 @@ dialog[aria-label="connection"] footer button[data-disconnect]:hover {
|
|
|
1493
1555
|
max-width: 95%;
|
|
1494
1556
|
}
|
|
1495
1557
|
|
|
1558
|
+
[data-usage-bar] {
|
|
1559
|
+
padding: 0.5rem 0.75rem;
|
|
1560
|
+
gap: 0.75rem;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1496
1563
|
form[aria-label="input"] {
|
|
1497
1564
|
padding: 0.5rem 0.75rem 0.75rem;
|
|
1498
1565
|
}
|
package/public/index.html
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<meta name="theme-color" content="#000000">
|
|
9
9
|
<meta name="color-scheme" content="dark">
|
|
10
10
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
11
|
-
<link rel="stylesheet" href="css/style.css?cb=
|
|
11
|
+
<link rel="stylesheet" href="css/style.css?cb=20">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
14
14
|
<button type="button" aria-label="menu" aria-expanded="false" aria-controls="agent-nav" title="menu" data-action="toggle-nav">
|
|
@@ -114,6 +114,24 @@
|
|
|
114
114
|
|
|
115
115
|
<div aria-label="messages" role="log" aria-live="polite"></div>
|
|
116
116
|
|
|
117
|
+
<div data-usage-bar hidden>
|
|
118
|
+
<div data-quota-session>
|
|
119
|
+
<div data-quota-header><span>session</span><span data-quota-pct>0%</span></div>
|
|
120
|
+
<div data-quota-track><div data-quota-fill></div></div>
|
|
121
|
+
<div data-quota-reset></div>
|
|
122
|
+
</div>
|
|
123
|
+
<div data-quota-weekly>
|
|
124
|
+
<div data-quota-header><span>weekly</span><span data-quota-pct>0%</span></div>
|
|
125
|
+
<div data-quota-track><div data-quota-fill></div></div>
|
|
126
|
+
<div data-quota-reset></div>
|
|
127
|
+
</div>
|
|
128
|
+
<div data-quota-overage>
|
|
129
|
+
<div data-quota-header><span>extra usage</span><span data-quota-pct>0%</span></div>
|
|
130
|
+
<div data-quota-track><div data-quota-fill></div></div>
|
|
131
|
+
<div data-quota-reset></div>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
117
135
|
<form aria-label="input">
|
|
118
136
|
<textarea placeholder="say something..." rows="1" aria-label="message"></textarea>
|
|
119
137
|
<button type="submit" aria-label="send message" title="send message">
|
|
@@ -257,6 +275,6 @@
|
|
|
257
275
|
</footer>
|
|
258
276
|
</dialog>
|
|
259
277
|
|
|
260
|
-
<script src="js/app.js?cb=
|
|
278
|
+
<script src="js/app.js?cb=20"></script>
|
|
261
279
|
</body>
|
|
262
280
|
</html>
|
package/public/js/app.js
CHANGED
|
@@ -2,6 +2,7 @@ let agents = [];
|
|
|
2
2
|
let activeAgent = null;
|
|
3
3
|
let eventSource = null;
|
|
4
4
|
let connectionsPoller = null;
|
|
5
|
+
let cachedQuota = null;
|
|
5
6
|
|
|
6
7
|
async function api(method, path, body) {
|
|
7
8
|
const opts = { method, headers: { 'content-type': 'application/json' } };
|
|
@@ -34,6 +35,7 @@ const chatInput = $('form[aria-label="input"] textarea');
|
|
|
34
35
|
const chatSubmit = $('form[aria-label="input"] button[type="submit"]');
|
|
35
36
|
const connectionsBtn = $('button[data-action="connections"]');
|
|
36
37
|
const platformsDiv = $('[data-platforms]');
|
|
38
|
+
const usageBar = $('[data-usage-bar]');
|
|
37
39
|
|
|
38
40
|
const iconRobot = '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M352 0c0-17.7-14.3-32-32-32S288-17.7 288 0l0 64-96 0c-53 0-96 43-96 96l0 224c0 53 43 96 96 96l256 0c53 0 96-43 96-96l0-224c0-53-43-96-96-96l-96 0 0-64zM160 368c0-13.3 10.7-24 24-24l32 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-32 0c-13.3 0-24-10.7-24-24zm120 0c0-13.3 10.7-24 24-24l32 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-32 0c-13.3 0-24-10.7-24-24zm120 0c0-13.3 10.7-24 24-24l32 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-32 0c-13.3 0-24-10.7-24-24zM224 176a48 48 0 1 1 0 96 48 48 0 1 1 0-96zm144 48a48 48 0 1 1 96 0 48 48 0 1 1 -96 0zM64 224c0-17.7-14.3-32-32-32S0 206.3 0 224l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96zm544-32c-17.7 0-32 14.3-32 32l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32z"/></svg>';
|
|
39
41
|
|
|
@@ -144,6 +146,44 @@ function esc(str) {
|
|
|
144
146
|
return el.innerHTML;
|
|
145
147
|
}
|
|
146
148
|
|
|
149
|
+
function formatReset(ts) {
|
|
150
|
+
if (!ts) return '';
|
|
151
|
+
const d = new Date(ts * 1000);
|
|
152
|
+
const now = new Date();
|
|
153
|
+
const sameDay = d.toDateString() === now.toDateString();
|
|
154
|
+
const time = d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
|
|
155
|
+
if (sameDay) return `resets ${time}`;
|
|
156
|
+
const date = d.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
|
157
|
+
return `resets ${date} ${time}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function updateQuotaBar(container, util, reset) {
|
|
161
|
+
const pct = Math.floor(util * 100);
|
|
162
|
+
const fill = container.querySelector('[data-quota-fill]');
|
|
163
|
+
container.querySelector('[data-quota-pct]').textContent = pct + '%';
|
|
164
|
+
fill.style.width = pct + '%';
|
|
165
|
+
delete fill.dataset.warn;
|
|
166
|
+
delete fill.dataset.danger;
|
|
167
|
+
if (pct >= 90) fill.dataset.danger = '';
|
|
168
|
+
else if (pct >= 70) fill.dataset.warn = '';
|
|
169
|
+
container.querySelector('[data-quota-reset]').textContent = formatReset(reset);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function applyQuota(data) {
|
|
173
|
+
if (!data || !data.available) return;
|
|
174
|
+
if (data.session) updateQuotaBar($('[data-quota-session]', usageBar), data.session.utilization, data.session.reset);
|
|
175
|
+
if (data.weekly) updateQuotaBar($('[data-quota-weekly]', usageBar), data.weekly.utilization, data.weekly.reset);
|
|
176
|
+
if (data.overage) updateQuotaBar($('[data-quota-overage]', usageBar), data.overage.utilization, data.overage.reset);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function fetchQuota() {
|
|
180
|
+
try {
|
|
181
|
+
const data = await api('GET', '/api/quota');
|
|
182
|
+
cachedQuota = data;
|
|
183
|
+
applyQuota(data);
|
|
184
|
+
} catch {}
|
|
185
|
+
}
|
|
186
|
+
|
|
147
187
|
function renderMarkdown(text) {
|
|
148
188
|
let html = esc(text);
|
|
149
189
|
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
|
|
@@ -248,6 +288,9 @@ async function selectAgent(id) {
|
|
|
248
288
|
chatInput.value = '';
|
|
249
289
|
chatSubmit.disabled = false;
|
|
250
290
|
|
|
291
|
+
usageBar.hidden = false;
|
|
292
|
+
if (cachedQuota) applyQuota(cachedQuota);
|
|
293
|
+
fetchQuota();
|
|
251
294
|
|
|
252
295
|
try {
|
|
253
296
|
const messages = await api('GET', `/api/agents/${id}/messages`);
|
|
@@ -400,7 +443,7 @@ function stopActivityTimer() {
|
|
|
400
443
|
}
|
|
401
444
|
|
|
402
445
|
function updateToolStatus(toolName) {
|
|
403
|
-
if (toolName !== currentToolName)
|
|
446
|
+
if (toolName !== currentToolName) toolStartTime = Date.now();
|
|
404
447
|
currentToolName = toolName;
|
|
405
448
|
const activity = showActivity();
|
|
406
449
|
if (!activity) return;
|
|
@@ -421,20 +464,7 @@ function updateToolStatus(toolName) {
|
|
|
421
464
|
scrollToBottom();
|
|
422
465
|
}
|
|
423
466
|
|
|
424
|
-
|
|
425
|
-
const activity = showActivity();
|
|
426
|
-
if (!activity) return;
|
|
427
|
-
let el = activity.querySelector('[data-summary]');
|
|
428
|
-
if (!el) {
|
|
429
|
-
el = document.createElement('p');
|
|
430
|
-
el.dataset.summary = '';
|
|
431
|
-
activity.appendChild(el);
|
|
432
|
-
}
|
|
433
|
-
el.textContent = summary;
|
|
434
|
-
const dots = activity.querySelector('[data-dots]');
|
|
435
|
-
if (dots) dots.remove();
|
|
436
|
-
scrollToBottom();
|
|
437
|
-
}
|
|
467
|
+
|
|
438
468
|
|
|
439
469
|
function connectSSE(agentId) {
|
|
440
470
|
if (eventSource) eventSource.close();
|
|
@@ -461,15 +491,6 @@ function connectSSE(agentId) {
|
|
|
461
491
|
} catch {}
|
|
462
492
|
});
|
|
463
493
|
|
|
464
|
-
eventSource.addEventListener('ack_message', (e) => {
|
|
465
|
-
try {
|
|
466
|
-
clearActivity();
|
|
467
|
-
const data = JSON.parse(e.data);
|
|
468
|
-
appendMessage('assistant', data.content);
|
|
469
|
-
scrollToBottom();
|
|
470
|
-
} catch {}
|
|
471
|
-
});
|
|
472
|
-
|
|
473
494
|
eventSource.addEventListener('intermediate', (e) => {
|
|
474
495
|
try {
|
|
475
496
|
const data = JSON.parse(e.data);
|
|
@@ -486,13 +507,6 @@ function connectSSE(agentId) {
|
|
|
486
507
|
} catch {}
|
|
487
508
|
});
|
|
488
509
|
|
|
489
|
-
eventSource.addEventListener('status_update', (e) => {
|
|
490
|
-
try {
|
|
491
|
-
const data = JSON.parse(e.data);
|
|
492
|
-
updateStatusSummary(data.summary);
|
|
493
|
-
} catch {}
|
|
494
|
-
});
|
|
495
|
-
|
|
496
510
|
eventSource.addEventListener('assistant_message', (e) => {
|
|
497
511
|
try {
|
|
498
512
|
clearActivity();
|
|
@@ -503,6 +517,10 @@ function connectSSE(agentId) {
|
|
|
503
517
|
} catch {}
|
|
504
518
|
});
|
|
505
519
|
|
|
520
|
+
eventSource.addEventListener('usage', () => {
|
|
521
|
+
fetchQuota();
|
|
522
|
+
});
|
|
523
|
+
|
|
506
524
|
eventSource.addEventListener('user_message', (e) => {
|
|
507
525
|
try {
|
|
508
526
|
const data = JSON.parse(e.data);
|
|
@@ -1335,6 +1353,7 @@ async function init() {
|
|
|
1335
1353
|
agents = await api('GET', '/api/agents');
|
|
1336
1354
|
renderAgents();
|
|
1337
1355
|
showSection('empty');
|
|
1356
|
+
fetchQuota();
|
|
1338
1357
|
} catch {
|
|
1339
1358
|
showSection('setup');
|
|
1340
1359
|
}
|
package/src/db.js
CHANGED
|
@@ -107,6 +107,23 @@ db.exec(`
|
|
|
107
107
|
)
|
|
108
108
|
`);
|
|
109
109
|
|
|
110
|
+
db.exec(`
|
|
111
|
+
create table if not exists token_usage (
|
|
112
|
+
id text primary key,
|
|
113
|
+
agent_id text not null,
|
|
114
|
+
input_tokens integer not null default 0,
|
|
115
|
+
output_tokens integer not null default 0,
|
|
116
|
+
cache_read_tokens integer not null default 0,
|
|
117
|
+
cache_write_tokens integer not null default 0,
|
|
118
|
+
created_at datetime default current_timestamp,
|
|
119
|
+
foreign key (agent_id) references agents(id) on delete cascade
|
|
120
|
+
)
|
|
121
|
+
`);
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
db.exec('create index if not exists idx_token_usage_agent on token_usage(agent_id)');
|
|
125
|
+
} catch {}
|
|
126
|
+
|
|
110
127
|
const stmts = {
|
|
111
128
|
getConfig: db.prepare('select value from config where key = ?'),
|
|
112
129
|
setConfig: db.prepare('insert into config (key, value, updated_at) values (?, ?, current_timestamp) on conflict(key) do update set value = excluded.value, updated_at = current_timestamp'),
|
|
@@ -157,6 +174,9 @@ const stmts = {
|
|
|
157
174
|
getSession: db.prepare('select * from sessions where agent_id = ?'),
|
|
158
175
|
upsertSession: db.prepare('insert into sessions (agent_id, session_id, prompt_hash, updated_at) values (?, ?, ?, current_timestamp) on conflict(agent_id) do update set session_id = excluded.session_id, prompt_hash = excluded.prompt_hash, updated_at = current_timestamp'),
|
|
159
176
|
deleteSession: db.prepare('delete from sessions where agent_id = ?'),
|
|
177
|
+
|
|
178
|
+
createUsage: db.prepare('insert into token_usage (id, agent_id, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens) values (?, ?, ?, ?, ?, ?)'),
|
|
179
|
+
agentUsageTotals: db.prepare('select coalesce(sum(input_tokens), 0) as input_tokens, coalesce(sum(output_tokens), 0) as output_tokens, coalesce(sum(cache_read_tokens), 0) as cache_read_tokens, coalesce(sum(cache_write_tokens), 0) as cache_write_tokens from token_usage where agent_id = ?'),
|
|
160
180
|
};
|
|
161
181
|
|
|
162
182
|
module.exports = { db, stmts };
|
package/src/routes/api.js
CHANGED
|
@@ -3,6 +3,7 @@ const { v4: uuid } = require('uuid');
|
|
|
3
3
|
const { stmts } = require('../db');
|
|
4
4
|
const auth = require('../services/auth');
|
|
5
5
|
const chat = require('../services/chat');
|
|
6
|
+
const claude = require('../services/claude');
|
|
6
7
|
const workspace = require('../services/workspace');
|
|
7
8
|
const heartbeat = require('../services/heartbeat');
|
|
8
9
|
|
|
@@ -121,6 +122,23 @@ router.delete('/agents/:id/messages', (req, res) => {
|
|
|
121
122
|
res.json({ cleared: true });
|
|
122
123
|
});
|
|
123
124
|
|
|
125
|
+
router.get('/agents/:id/usage', (req, res) => {
|
|
126
|
+
const agent = stmts.getAgent.get(req.params.id);
|
|
127
|
+
if (!agent) return res.status(404).json({ error: 'agent not found' });
|
|
128
|
+
const totals = stmts.agentUsageTotals.get(req.params.id);
|
|
129
|
+
res.json(totals);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
router.get('/quota', async (req, res) => {
|
|
133
|
+
try {
|
|
134
|
+
const quota = await claude.probeQuota();
|
|
135
|
+
if (!quota) return res.json({ available: false });
|
|
136
|
+
res.json({ available: true, ...quota });
|
|
137
|
+
} catch {
|
|
138
|
+
res.json({ available: false });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
124
142
|
router.post('/agents/:id/stop', (req, res) => {
|
|
125
143
|
const stopped = chat.stopAgent(req.params.id);
|
|
126
144
|
res.json({ stopped });
|
|
@@ -156,13 +174,13 @@ router.get('/agents/:id/stream', (req, res) => {
|
|
|
156
174
|
const elapsed = activity ? Math.floor((Date.now() - activity.startTime) / 1000) : 0;
|
|
157
175
|
res.write(`event: typing\ndata: ${JSON.stringify({ active: true, elapsed })}\n\n`);
|
|
158
176
|
if (activity) {
|
|
177
|
+
if (activity.intermediateText) {
|
|
178
|
+
res.write(`event: intermediate\ndata: ${JSON.stringify({ content: activity.intermediateText, elapsed })}\n\n`);
|
|
179
|
+
}
|
|
159
180
|
if (activity.tool) {
|
|
160
181
|
const toolElapsed = activity.toolStartTime ? Math.floor((Date.now() - activity.toolStartTime) / 1000) : 0;
|
|
161
182
|
res.write(`event: tool_call\ndata: ${JSON.stringify({ name: activity.tool, elapsed, toolElapsed })}\n\n`);
|
|
162
183
|
}
|
|
163
|
-
if (activity.summary) {
|
|
164
|
-
res.write(`event: status_update\ndata: ${JSON.stringify({ summary: activity.summary, elapsed, tool: activity.tool || 'thinking' })}\n\n`);
|
|
165
|
-
}
|
|
166
184
|
}
|
|
167
185
|
}
|
|
168
186
|
|
package/src/services/auth.js
CHANGED
|
@@ -4,6 +4,21 @@ const { stmts } = require('../db');
|
|
|
4
4
|
let cachedCredentials = null;
|
|
5
5
|
|
|
6
6
|
function readKeychain() {
|
|
7
|
+
const accounts = [process.env.USER, 'Claude Code'];
|
|
8
|
+
for (const acct of accounts) {
|
|
9
|
+
try {
|
|
10
|
+
const raw = execSync(
|
|
11
|
+
`security find-generic-password -s "Claude Code-credentials" -a "${acct}" -w`,
|
|
12
|
+
{ encoding: 'utf8', timeout: 5000 }
|
|
13
|
+
).trim();
|
|
14
|
+
const parsed = JSON.parse(raw);
|
|
15
|
+
const creds = parsed.claudeAiOauth || parsed;
|
|
16
|
+
if (creds.accessToken && creds.accessToken.startsWith('sk-ant-')) {
|
|
17
|
+
cachedCredentials = creds;
|
|
18
|
+
return creds;
|
|
19
|
+
}
|
|
20
|
+
} catch {}
|
|
21
|
+
}
|
|
7
22
|
try {
|
|
8
23
|
const raw = execSync(
|
|
9
24
|
'security find-generic-password -s "Claude Code-credentials" -w',
|
|
@@ -64,10 +79,18 @@ function getAuthStatus() {
|
|
|
64
79
|
}
|
|
65
80
|
|
|
66
81
|
function getHeaders() {
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
82
|
+
const apiKey = getApiKey();
|
|
83
|
+
if (apiKey) {
|
|
84
|
+
return {
|
|
85
|
+
'x-api-key': apiKey,
|
|
86
|
+
'content-type': 'application/json',
|
|
87
|
+
'anthropic-version': '2023-06-01'
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const creds = cachedCredentials || readKeychain();
|
|
91
|
+
if (!creds || !creds.accessToken) return null;
|
|
69
92
|
return {
|
|
70
|
-
'
|
|
93
|
+
'authorization': `Bearer ${creds.accessToken}`,
|
|
71
94
|
'content-type': 'application/json',
|
|
72
95
|
'anthropic-version': '2023-06-01'
|
|
73
96
|
};
|
package/src/services/chat.js
CHANGED
|
@@ -100,7 +100,7 @@ your memories are automatically extracted from conversations and written to dail
|
|
|
100
100
|
|
|
101
101
|
your workspace is at data/agents/${workspace.sanitizeName(agent.name)}/. you can read and write your own files using read_workspace and write_workspace. your soul, identity, memory, and heartbeat files are yours to evolve.
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
always tell the user what you're about to do before doing it. share your plan, intention, or approach in natural language first, then use tools to execute. don't ask permission - just say what you're doing and do it. when interacting with external platforms, read their documentation first to understand the api.
|
|
104
104
|
|
|
105
105
|
if the user asks you to do something repeatedly or on a schedule, use the schedule_task tool to set it up. you will receive scheduled reminders as messages and should act on them autonomously.
|
|
106
106
|
|
|
@@ -137,7 +137,6 @@ async function handleMessage(agentId, userContent, options = {}) {
|
|
|
137
137
|
if (!agent) throw new Error('agent not found');
|
|
138
138
|
|
|
139
139
|
const isHeartbeat = !!options.heartbeat;
|
|
140
|
-
const isScheduled = typeof userContent === 'string' && userContent.startsWith('[scheduled reminder]');
|
|
141
140
|
|
|
142
141
|
if (!isHeartbeat) {
|
|
143
142
|
const userMsgId = uuid();
|
|
@@ -147,29 +146,12 @@ async function handleMessage(agentId, userContent, options = {}) {
|
|
|
147
146
|
|
|
148
147
|
if (!isHeartbeat) {
|
|
149
148
|
processingAgents.add(agentId);
|
|
150
|
-
agentActivity.set(agentId, { startTime: Date.now(), tool: null, toolStartTime: null,
|
|
149
|
+
agentActivity.set(agentId, { startTime: Date.now(), tool: null, toolStartTime: null, intermediateText: null });
|
|
151
150
|
emit(agentId, 'typing', { active: true });
|
|
152
151
|
}
|
|
153
152
|
|
|
154
|
-
let responseComplete = false;
|
|
155
153
|
const startTime = Date.now();
|
|
156
154
|
let lastToolName = null;
|
|
157
|
-
let statusInterval = null;
|
|
158
|
-
|
|
159
|
-
if (!isHeartbeat) {
|
|
160
|
-
statusInterval = setInterval(async () => {
|
|
161
|
-
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
162
|
-
const tool = lastToolName || 'thinking';
|
|
163
|
-
try {
|
|
164
|
-
const summary = await claude.generateStatus(tool, elapsed, agent.name);
|
|
165
|
-
if (summary) {
|
|
166
|
-
const state = agentActivity.get(agentId);
|
|
167
|
-
if (state) state.summary = summary;
|
|
168
|
-
emit(agentId, 'status_update', { elapsed, summary, tool });
|
|
169
|
-
}
|
|
170
|
-
} catch {}
|
|
171
|
-
}, 60000);
|
|
172
|
-
}
|
|
173
155
|
|
|
174
156
|
const systemPrompt = buildSystemPrompt(agent);
|
|
175
157
|
const toolDefs = tools.getAllToolDefinitions();
|
|
@@ -184,26 +166,37 @@ async function handleMessage(agentId, userContent, options = {}) {
|
|
|
184
166
|
|
|
185
167
|
let allToolCalls = [];
|
|
186
168
|
let intermediateTexts = [];
|
|
187
|
-
|
|
188
|
-
const wantsAck = !isHeartbeat && !isScheduled && !isBootstrap;
|
|
189
|
-
const ackPromise = wantsAck
|
|
190
|
-
? claude.generateQuickAck(userContent, agent.name).catch(() => null)
|
|
191
|
-
: Promise.resolve(null);
|
|
169
|
+
const totalUsage = { input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cache_write_tokens: 0 };
|
|
192
170
|
|
|
193
171
|
const onEvent = !isHeartbeat ? (event) => {
|
|
194
|
-
if (event.type
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
172
|
+
if (event.type !== 'assistant' || !event.message?.content) return;
|
|
173
|
+
|
|
174
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
175
|
+
|
|
176
|
+
for (const block of event.message.content) {
|
|
177
|
+
if (block.type === 'tool_use') {
|
|
178
|
+
lastToolName = block.name;
|
|
179
|
+
const state = agentActivity.get(agentId);
|
|
180
|
+
if (state) { state.tool = block.name; state.toolStartTime = Date.now(); }
|
|
181
|
+
emit(agentId, 'tool_call', { name: block.name, input: block.input, elapsed });
|
|
182
|
+
} else if (block.type === 'text' && block.text?.trim()) {
|
|
183
|
+
const text = block.text.trim();
|
|
184
|
+
const state = agentActivity.get(agentId);
|
|
185
|
+
if (state) state.intermediateText = text;
|
|
186
|
+
emit(agentId, 'intermediate', { content: text, elapsed });
|
|
187
|
+
if (options.onAck) {
|
|
188
|
+
try { options.onAck(text); } catch {}
|
|
202
189
|
}
|
|
203
190
|
}
|
|
204
191
|
}
|
|
205
192
|
} : null;
|
|
206
193
|
|
|
194
|
+
const ackThinking = (text) => {
|
|
195
|
+
if (options.onAck && text) {
|
|
196
|
+
try { options.onAck(text); } catch {}
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
207
200
|
const mainPromise = claude.sendMessage({
|
|
208
201
|
system: systemPrompt,
|
|
209
202
|
messages,
|
|
@@ -217,30 +210,12 @@ async function handleMessage(agentId, userContent, options = {}) {
|
|
|
217
210
|
});
|
|
218
211
|
|
|
219
212
|
try {
|
|
220
|
-
let response;
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
]);
|
|
227
|
-
|
|
228
|
-
if (raceResult.type === 'main') {
|
|
229
|
-
response = raceResult.result;
|
|
230
|
-
} else {
|
|
231
|
-
const quickAck = await ackPromise;
|
|
232
|
-
if (quickAck) {
|
|
233
|
-
const ackMsgId = uuid();
|
|
234
|
-
stmts.createMessage.run(ackMsgId, agentId, 'assistant', quickAck, null);
|
|
235
|
-
emit(agentId, 'typing', { active: false });
|
|
236
|
-
emit(agentId, 'ack_message', { content: quickAck });
|
|
237
|
-
if (options.onAck) options.onAck(quickAck);
|
|
238
|
-
emit(agentId, 'typing', { active: true });
|
|
239
|
-
}
|
|
240
|
-
response = await mainPromise;
|
|
241
|
-
}
|
|
242
|
-
} else {
|
|
243
|
-
response = await mainPromise;
|
|
213
|
+
let response = await mainPromise;
|
|
214
|
+
if (response.usage) {
|
|
215
|
+
totalUsage.input_tokens += response.usage.input_tokens;
|
|
216
|
+
totalUsage.output_tokens += response.usage.output_tokens;
|
|
217
|
+
totalUsage.cache_read_tokens += response.usage.cache_read_tokens;
|
|
218
|
+
totalUsage.cache_write_tokens += response.usage.cache_write_tokens;
|
|
244
219
|
}
|
|
245
220
|
|
|
246
221
|
while (claude.hasToolUse(response)) {
|
|
@@ -250,7 +225,10 @@ async function handleMessage(agentId, userContent, options = {}) {
|
|
|
250
225
|
|
|
251
226
|
if (thinkingText) {
|
|
252
227
|
intermediateTexts.push(thinkingText);
|
|
228
|
+
const state = agentActivity.get(agentId);
|
|
229
|
+
if (state) state.intermediateText = thinkingText;
|
|
253
230
|
emit(agentId, 'intermediate', { content: thinkingText });
|
|
231
|
+
ackThinking(thinkingText);
|
|
254
232
|
}
|
|
255
233
|
|
|
256
234
|
messages.push({ role: 'assistant', content: response.content });
|
|
@@ -298,6 +276,12 @@ async function handleMessage(agentId, userContent, options = {}) {
|
|
|
298
276
|
noBuiltinTools: isBootstrap,
|
|
299
277
|
onEvent
|
|
300
278
|
});
|
|
279
|
+
if (response.usage) {
|
|
280
|
+
totalUsage.input_tokens += response.usage.input_tokens;
|
|
281
|
+
totalUsage.output_tokens += response.usage.output_tokens;
|
|
282
|
+
totalUsage.cache_read_tokens += response.usage.cache_read_tokens;
|
|
283
|
+
totalUsage.cache_write_tokens += response.usage.cache_write_tokens;
|
|
284
|
+
}
|
|
301
285
|
}
|
|
302
286
|
|
|
303
287
|
let rawText = claude.extractText(response);
|
|
@@ -334,11 +318,9 @@ async function handleMessage(agentId, userContent, options = {}) {
|
|
|
334
318
|
const stripped = responseText.replace(/\s+/g, ' ').trim();
|
|
335
319
|
const isOk = stripped.includes('HEARTBEAT_OK') && stripped.length <= 300;
|
|
336
320
|
if (isOk) {
|
|
337
|
-
responseComplete = true;
|
|
338
321
|
return { id: null, content: responseText, suppressed: true };
|
|
339
322
|
}
|
|
340
323
|
stmts.createHeartbeatMessage.run(assistantMsgId, agentId, 'assistant', responseText, toolCallsJson);
|
|
341
|
-
responseComplete = true;
|
|
342
324
|
emit(agentId, 'heartbeat_alert', {
|
|
343
325
|
id: assistantMsgId,
|
|
344
326
|
content: responseText,
|
|
@@ -349,6 +331,13 @@ async function handleMessage(agentId, userContent, options = {}) {
|
|
|
349
331
|
|
|
350
332
|
stmts.createMessage.run(assistantMsgId, agentId, 'assistant', responseText, toolCallsJson);
|
|
351
333
|
|
|
334
|
+
if (totalUsage.input_tokens > 0 || totalUsage.output_tokens > 0) {
|
|
335
|
+
try {
|
|
336
|
+
stmts.createUsage.run(uuid(), agentId, totalUsage.input_tokens, totalUsage.output_tokens, totalUsage.cache_read_tokens, totalUsage.cache_write_tokens);
|
|
337
|
+
} catch {}
|
|
338
|
+
emit(agentId, 'usage', totalUsage);
|
|
339
|
+
}
|
|
340
|
+
|
|
352
341
|
if (isBootstrap && stmts.getAgent.get(agentId)?.bootstrapped !== 0) {
|
|
353
342
|
emit(agentId, 'bootstrap_complete', {});
|
|
354
343
|
}
|
|
@@ -359,8 +348,6 @@ async function handleMessage(agentId, userContent, options = {}) {
|
|
|
359
348
|
});
|
|
360
349
|
}
|
|
361
350
|
|
|
362
|
-
if (statusInterval) clearInterval(statusInterval);
|
|
363
|
-
responseComplete = true;
|
|
364
351
|
processingAgents.delete(agentId);
|
|
365
352
|
agentActivity.delete(agentId);
|
|
366
353
|
emit(agentId, 'typing', { active: false });
|
|
@@ -373,8 +360,6 @@ async function handleMessage(agentId, userContent, options = {}) {
|
|
|
373
360
|
return { id: assistantMsgId, content: responseText, tool_calls: allToolCalls.length ? allToolCalls : null };
|
|
374
361
|
|
|
375
362
|
} catch (err) {
|
|
376
|
-
if (statusInterval) clearInterval(statusInterval);
|
|
377
|
-
responseComplete = true;
|
|
378
363
|
processingAgents.delete(agentId);
|
|
379
364
|
agentActivity.delete(agentId);
|
|
380
365
|
if (!isHeartbeat) emit(agentId, 'typing', { active: false });
|
|
@@ -392,6 +377,7 @@ function enqueueMessage(agentId, content, options = {}) {
|
|
|
392
377
|
messageQueues.delete(agentId);
|
|
393
378
|
}
|
|
394
379
|
});
|
|
380
|
+
tracked.catch(() => {});
|
|
395
381
|
messageQueues.set(agentId, tracked);
|
|
396
382
|
return chained;
|
|
397
383
|
}
|
package/src/services/claude.js
CHANGED
|
@@ -10,6 +10,28 @@ const API_URL = 'https://api.anthropic.com/v1/messages';
|
|
|
10
10
|
const MODEL = 'claude-opus-4-6';
|
|
11
11
|
const activeProcesses = new Map();
|
|
12
12
|
|
|
13
|
+
let supportsEffort = null;
|
|
14
|
+
function checkEffortSupport() {
|
|
15
|
+
if (supportsEffort !== null) return supportsEffort;
|
|
16
|
+
try {
|
|
17
|
+
const { execSync } = require('child_process');
|
|
18
|
+
const help = execSync('claude --help 2>&1', { encoding: 'utf8' });
|
|
19
|
+
supportsEffort = help.includes('--effort');
|
|
20
|
+
} catch {
|
|
21
|
+
supportsEffort = false;
|
|
22
|
+
}
|
|
23
|
+
return supportsEffort;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function applyEffort(args, extraEnv, effort) {
|
|
27
|
+
if (checkEffortSupport()) {
|
|
28
|
+
args.push('--effort', effort);
|
|
29
|
+
} else {
|
|
30
|
+
const tokens = { low: '0', medium: '16000', high: '31999' };
|
|
31
|
+
extraEnv.MAX_THINKING_TOKENS = tokens[effort] || '31999';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
13
35
|
function hashPrompt(str) {
|
|
14
36
|
let h = 0;
|
|
15
37
|
for (let i = 0; i < str.length; i++) {
|
|
@@ -143,9 +165,12 @@ function runCli(args, input, extraEnv = {}, agentId = null, onEvent = null) {
|
|
|
143
165
|
}
|
|
144
166
|
}
|
|
145
167
|
});
|
|
146
|
-
proc.stderr.on('data', d =>
|
|
168
|
+
proc.stderr.on('data', d => {
|
|
169
|
+
stderr += d;
|
|
170
|
+
});
|
|
147
171
|
|
|
148
172
|
proc.on('close', code => {
|
|
173
|
+
try { fs.rmSync(cwd, { recursive: true, force: true }); } catch {}
|
|
149
174
|
if (done) return;
|
|
150
175
|
done = true;
|
|
151
176
|
if (agentId) activeProcesses.delete(agentId);
|
|
@@ -169,6 +194,7 @@ function runCli(args, input, extraEnv = {}, agentId = null, onEvent = null) {
|
|
|
169
194
|
});
|
|
170
195
|
|
|
171
196
|
proc.on('error', err => {
|
|
197
|
+
clearInterval(idleCheck);
|
|
172
198
|
if (done) return;
|
|
173
199
|
done = true;
|
|
174
200
|
reject(err);
|
|
@@ -191,9 +217,11 @@ async function sendViaCli({ system, messages, tools, maxTokens, agentId, model =
|
|
|
191
217
|
let output;
|
|
192
218
|
|
|
193
219
|
if (canResume) {
|
|
194
|
-
const args = ['-p', '--output-format', 'stream-json', '--verbose', '--
|
|
220
|
+
const args = ['-p', '--output-format', 'stream-json', '--verbose', '--dangerously-skip-permissions', '--setting-sources', '', '--resume', session.session_id];
|
|
221
|
+
const extraEnv = {};
|
|
222
|
+
applyEffort(args, extraEnv, effort);
|
|
195
223
|
try {
|
|
196
|
-
output = await runCli(args, promptText,
|
|
224
|
+
output = await runCli(args, promptText, extraEnv, agentId, onEvent);
|
|
197
225
|
return processCliOutput(output, tools);
|
|
198
226
|
} catch (err) {
|
|
199
227
|
if (err.message === 'aborted') throw err;
|
|
@@ -216,14 +244,16 @@ async function sendCliFresh({ sysPrompt, messages, promptText, tools, agentId, c
|
|
|
216
244
|
if (context) fullPrompt += `previous conversation:\n${context}\n\n`;
|
|
217
245
|
fullPrompt += promptText;
|
|
218
246
|
|
|
219
|
-
const args = ['-p', '--output-format', 'stream-json', '--verbose', '--model', model, '--
|
|
247
|
+
const args = ['-p', '--output-format', 'stream-json', '--verbose', '--model', model, '--dangerously-skip-permissions', '--setting-sources', '', '--session-id', sessionId];
|
|
248
|
+
const extraEnv = {};
|
|
249
|
+
applyEffort(args, extraEnv, effort);
|
|
220
250
|
if (noBuiltinTools) args.push('--tools', '');
|
|
221
251
|
if (sysPrompt) {
|
|
222
252
|
args.push('--system-prompt', sysPrompt);
|
|
223
253
|
}
|
|
224
254
|
|
|
225
255
|
try {
|
|
226
|
-
const output = await runCli(args, fullPrompt,
|
|
256
|
+
const output = await runCli(args, fullPrompt, extraEnv, agentId, onEvent);
|
|
227
257
|
|
|
228
258
|
if (agentId) {
|
|
229
259
|
stmts.upsertSession.run(agentId, sessionId, currentHash);
|
|
@@ -233,9 +263,11 @@ async function sendCliFresh({ sysPrompt, messages, promptText, tools, agentId, c
|
|
|
233
263
|
} catch (err) {
|
|
234
264
|
if (err.message === 'aborted') throw err;
|
|
235
265
|
if (isContextOverflow(err) && context) {
|
|
236
|
-
const retryArgs = ['-p', '--output-format', 'stream-json', '--verbose', '--model', model, '--
|
|
266
|
+
const retryArgs = ['-p', '--output-format', 'stream-json', '--verbose', '--model', model, '--dangerously-skip-permissions', '--setting-sources', '', '--session-id', randomUUID()];
|
|
267
|
+
const retryEnv = {};
|
|
268
|
+
applyEffort(retryArgs, retryEnv, effort);
|
|
237
269
|
if (sysPrompt) retryArgs.push('--system-prompt', sysPrompt);
|
|
238
|
-
const output = await runCli(retryArgs, promptText,
|
|
270
|
+
const output = await runCli(retryArgs, promptText, retryEnv, agentId);
|
|
239
271
|
return processCliOutput(output, tools);
|
|
240
272
|
}
|
|
241
273
|
throw err;
|
|
@@ -246,6 +278,13 @@ function processCliOutput(output, tools) {
|
|
|
246
278
|
const parsed = parseCliOutput(output);
|
|
247
279
|
if (!parsed) throw new Error('claude cli returned empty response');
|
|
248
280
|
|
|
281
|
+
const usage = parsed.usage ? {
|
|
282
|
+
input_tokens: parsed.usage.input_tokens || 0,
|
|
283
|
+
output_tokens: parsed.usage.output_tokens || 0,
|
|
284
|
+
cache_read_tokens: parsed.usage.cache_read_input_tokens || 0,
|
|
285
|
+
cache_write_tokens: parsed.usage.cache_creation_input_tokens || 0
|
|
286
|
+
} : null;
|
|
287
|
+
|
|
249
288
|
if (parsed.is_error && parsed.num_turns === 0) {
|
|
250
289
|
throw new Error('cli session error');
|
|
251
290
|
}
|
|
@@ -254,23 +293,32 @@ function processCliOutput(output, tools) {
|
|
|
254
293
|
const text = typeof parsed.result === 'string' && parsed.result.length > 0
|
|
255
294
|
? parsed.result
|
|
256
295
|
: '';
|
|
257
|
-
|
|
296
|
+
const resp = buildResponse(text, tools);
|
|
297
|
+
resp.usage = usage;
|
|
298
|
+
return resp;
|
|
258
299
|
}
|
|
259
300
|
|
|
260
301
|
if (parsed.result !== undefined && parsed.result !== null && parsed.result !== '') {
|
|
261
302
|
const text = typeof parsed.result === 'string' ? parsed.result : JSON.stringify(parsed.result);
|
|
262
|
-
|
|
303
|
+
const resp = buildResponse(text, tools);
|
|
304
|
+
resp.usage = usage;
|
|
305
|
+
return resp;
|
|
263
306
|
}
|
|
264
307
|
|
|
265
308
|
if (parsed.content) {
|
|
309
|
+
parsed.usage = usage;
|
|
266
310
|
return parsed;
|
|
267
311
|
}
|
|
268
312
|
|
|
269
313
|
if (parsed.type === 'result') {
|
|
270
|
-
|
|
314
|
+
const resp = buildResponse('', tools);
|
|
315
|
+
resp.usage = usage;
|
|
316
|
+
return resp;
|
|
271
317
|
}
|
|
272
318
|
|
|
273
|
-
|
|
319
|
+
const resp = buildResponse(output, tools);
|
|
320
|
+
resp.usage = usage;
|
|
321
|
+
return resp;
|
|
274
322
|
}
|
|
275
323
|
|
|
276
324
|
function parseCliOutput(output) {
|
|
@@ -358,19 +406,6 @@ function hasToolUse(response) {
|
|
|
358
406
|
return response.stop_reason === 'tool_use';
|
|
359
407
|
}
|
|
360
408
|
|
|
361
|
-
async function generateQuickAck(userContent, agentName) {
|
|
362
|
-
const args = ['-p', '--output-format', 'json', '--model', 'haiku', '--dangerously-skip-permissions', '--setting-sources', ''];
|
|
363
|
-
const prompt = `you are ${agentName}. the user just said: "${userContent}"\n\nacknowledge their message in one casual sentence - show you understood what they want and you're about to start. don't answer or attempt the task, just the acknowledgment. no quotes.`;
|
|
364
|
-
try {
|
|
365
|
-
const output = await runCli(args, prompt);
|
|
366
|
-
const parsed = parseCliOutput(output);
|
|
367
|
-
if (parsed && typeof parsed.result === 'string' && parsed.result.length > 0) {
|
|
368
|
-
return parsed.result.trim();
|
|
369
|
-
}
|
|
370
|
-
} catch {}
|
|
371
|
-
return null;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
409
|
function abort(agentId) {
|
|
375
410
|
const proc = activeProcesses.get(agentId);
|
|
376
411
|
if (proc) {
|
|
@@ -381,18 +416,58 @@ function abort(agentId) {
|
|
|
381
416
|
return false;
|
|
382
417
|
}
|
|
383
418
|
|
|
384
|
-
async function
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
419
|
+
async function probeQuota() {
|
|
420
|
+
const status = auth.getAuthStatus();
|
|
421
|
+
if (!status.authenticated) return null;
|
|
422
|
+
|
|
423
|
+
const headers = auth.getHeaders();
|
|
424
|
+
if (!headers) return null;
|
|
425
|
+
if (headers.authorization) headers['anthropic-beta'] = 'oauth-2025-04-20';
|
|
426
|
+
|
|
388
427
|
try {
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
428
|
+
const res = await fetch(API_URL, {
|
|
429
|
+
method: 'POST',
|
|
430
|
+
headers,
|
|
431
|
+
body: JSON.stringify({
|
|
432
|
+
model: MODEL,
|
|
433
|
+
max_tokens: 1,
|
|
434
|
+
messages: [{ role: 'user', content: 'quota' }]
|
|
435
|
+
})
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
res.text().catch(() => {});
|
|
439
|
+
const quota = {};
|
|
440
|
+
const h = res.headers;
|
|
441
|
+
|
|
442
|
+
const session = h.get('anthropic-ratelimit-unified-5h-utilization');
|
|
443
|
+
if (session !== null) {
|
|
444
|
+
quota.session = {
|
|
445
|
+
utilization: parseFloat(session),
|
|
446
|
+
reset: parseInt(h.get('anthropic-ratelimit-unified-5h-reset') || '0', 10)
|
|
447
|
+
};
|
|
393
448
|
}
|
|
394
|
-
|
|
395
|
-
|
|
449
|
+
|
|
450
|
+
const weekly = h.get('anthropic-ratelimit-unified-7d-utilization');
|
|
451
|
+
if (weekly !== null) {
|
|
452
|
+
quota.weekly = {
|
|
453
|
+
utilization: parseFloat(weekly),
|
|
454
|
+
reset: parseInt(h.get('anthropic-ratelimit-unified-7d-reset') || '0', 10)
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const overageUtil = h.get('anthropic-ratelimit-unified-overage-utilization');
|
|
459
|
+
if (overageUtil !== null) {
|
|
460
|
+
quota.overage = {
|
|
461
|
+
utilization: parseFloat(overageUtil),
|
|
462
|
+
reset: parseInt(h.get('anthropic-ratelimit-unified-overage-reset') || '0', 10)
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (!quota.session && !quota.weekly) return null;
|
|
467
|
+
return quota;
|
|
468
|
+
} catch {
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
396
471
|
}
|
|
397
472
|
|
|
398
|
-
module.exports = { sendMessage, extractText, extractToolUse, hasToolUse,
|
|
473
|
+
module.exports = { sendMessage, extractText, extractToolUse, hasToolUse, abort, probeQuota };
|
package/src/services/discord.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { Client, GatewayIntentBits, Partials } = require('discord.js');
|
|
2
2
|
|
|
3
3
|
const MAX_RESPONSE_LENGTH = 1900;
|
|
4
|
+
const EDIT_THROTTLE = 5000;
|
|
4
5
|
|
|
5
6
|
let client = null;
|
|
6
7
|
let typingTimer = null;
|
|
@@ -17,6 +18,10 @@ function parseMessage(text, botId) {
|
|
|
17
18
|
return { agent: match[1].toLowerCase(), command: match[2].trim() };
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
function clamp(text) {
|
|
22
|
+
return text.length > MAX_RESPONSE_LENGTH ? text.slice(0, MAX_RESPONSE_LENGTH) + '...' : text;
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
function startTyping(channel) {
|
|
21
26
|
channel.sendTyping().catch(() => {});
|
|
22
27
|
typingTimer = setInterval(() => {
|
|
@@ -86,21 +91,39 @@ function start(config, callbacks) {
|
|
|
86
91
|
log(`${parsed.agent}: ${parsed.command}`);
|
|
87
92
|
startTyping(message.channel);
|
|
88
93
|
|
|
94
|
+
let statusReply = null;
|
|
95
|
+
let statusPromise = null;
|
|
96
|
+
let lastEditTime = 0;
|
|
97
|
+
|
|
89
98
|
try {
|
|
90
99
|
const result = await chatModule.enqueueMessage(agent.id, parsed.command, {
|
|
91
100
|
onAck: (text) => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
101
|
+
text = clamp(text);
|
|
102
|
+
if (!statusPromise) {
|
|
103
|
+
statusPromise = message.reply(text)
|
|
104
|
+
.then(sent => { statusReply = sent; })
|
|
105
|
+
.catch(() => {});
|
|
106
|
+
} else if (Date.now() - lastEditTime >= EDIT_THROTTLE) {
|
|
107
|
+
lastEditTime = Date.now();
|
|
108
|
+
statusPromise = statusPromise.then(() => {
|
|
109
|
+
if (statusReply) return statusReply.edit(text).catch(() => {});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
95
112
|
}
|
|
96
113
|
});
|
|
97
114
|
stopTyping();
|
|
98
115
|
if (result && result.content) {
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
|
|
116
|
+
const text = clamp(result.content);
|
|
117
|
+
if (statusPromise) {
|
|
118
|
+
await statusPromise;
|
|
119
|
+
if (statusReply) {
|
|
120
|
+
await statusReply.edit(text).catch(() => {});
|
|
121
|
+
} else {
|
|
122
|
+
await message.reply(text);
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
await message.reply(text);
|
|
102
126
|
}
|
|
103
|
-
await message.reply(text);
|
|
104
127
|
}
|
|
105
128
|
} catch (err) {
|
|
106
129
|
stopTyping();
|
package/src/services/imessage.js
CHANGED
|
@@ -155,11 +155,19 @@ function poll() {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
async function handleCommand(agent, command, chatGuid) {
|
|
158
|
+
let lastAcked = null;
|
|
159
|
+
let lastAckTime = Date.now();
|
|
158
160
|
try {
|
|
159
161
|
const result = await chatModule.enqueueMessage(agent.id, command, {
|
|
160
|
-
onAck: (text) =>
|
|
162
|
+
onAck: (text) => {
|
|
163
|
+
if (Date.now() - lastAckTime < 15000) return;
|
|
164
|
+
lastAckTime = Date.now();
|
|
165
|
+
lastAcked = text;
|
|
166
|
+
sendMessage(chatGuid, text);
|
|
167
|
+
}
|
|
161
168
|
});
|
|
162
169
|
if (result && result.content) {
|
|
170
|
+
if (lastAcked && (result.content === lastAcked || result.content.startsWith(lastAcked) || lastAcked.startsWith(result.content))) return;
|
|
163
171
|
sendMessage(chatGuid, result.content);
|
|
164
172
|
}
|
|
165
173
|
} catch (err) {
|
package/src/services/signal.js
CHANGED
|
@@ -83,10 +83,19 @@ function startDaemon(phone, onStatus, chatModule, stmts) {
|
|
|
83
83
|
|
|
84
84
|
log(`${parsed.agent}: ${parsed.command}`);
|
|
85
85
|
|
|
86
|
+
let lastAcked = null;
|
|
87
|
+
let lastAckTime = Date.now();
|
|
88
|
+
|
|
86
89
|
chatModule.enqueueMessage(agent.id, parsed.command, {
|
|
87
|
-
onAck: (ackText) =>
|
|
90
|
+
onAck: (ackText) => {
|
|
91
|
+
if (Date.now() - lastAckTime < 15000) return;
|
|
92
|
+
lastAckTime = Date.now();
|
|
93
|
+
lastAcked = ackText;
|
|
94
|
+
send(sender, ackText);
|
|
95
|
+
}
|
|
88
96
|
}).then((result) => {
|
|
89
97
|
if (result && result.content) {
|
|
98
|
+
if (lastAcked && (result.content === lastAcked || result.content.startsWith(lastAcked) || lastAcked.startsWith(result.content))) return;
|
|
90
99
|
let reply = result.content;
|
|
91
100
|
if (reply.length > MAX_RESPONSE_LENGTH) {
|
|
92
101
|
reply = reply.slice(0, MAX_RESPONSE_LENGTH) + '...';
|
package/src/services/slack.js
CHANGED
|
@@ -2,6 +2,7 @@ const { App, LogLevel } = require('@slack/bolt');
|
|
|
2
2
|
const { WebClient } = require('@slack/web-api');
|
|
3
3
|
|
|
4
4
|
const MAX_RESPONSE_LENGTH = 3000;
|
|
5
|
+
const EDIT_THROTTLE = 5000;
|
|
5
6
|
|
|
6
7
|
let app = null;
|
|
7
8
|
|
|
@@ -17,6 +18,10 @@ function parseMessage(text) {
|
|
|
17
18
|
return { agent: match[1].toLowerCase(), command: match[2].trim() };
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
function clamp(text) {
|
|
22
|
+
return text.length > MAX_RESPONSE_LENGTH ? text.slice(0, MAX_RESPONSE_LENGTH) + '...' : text;
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
function start(config, callbacks) {
|
|
21
26
|
const { onStatus } = callbacks || {};
|
|
22
27
|
const { bot_token, app_token } = config || {};
|
|
@@ -63,16 +68,39 @@ function start(config, callbacks) {
|
|
|
63
68
|
|
|
64
69
|
log(`${parsed.agent}: ${parsed.command}`);
|
|
65
70
|
|
|
71
|
+
let statusTs = null;
|
|
72
|
+
let statusChannel = null;
|
|
73
|
+
let statusPromise = null;
|
|
74
|
+
let lastEditTime = 0;
|
|
75
|
+
|
|
66
76
|
try {
|
|
67
77
|
const result = await chatModule.enqueueMessage(agent.id, parsed.command, {
|
|
68
|
-
onAck: (text) =>
|
|
78
|
+
onAck: (text) => {
|
|
79
|
+
text = clamp(text);
|
|
80
|
+
if (!statusPromise) {
|
|
81
|
+
statusPromise = say(text)
|
|
82
|
+
.then(res => { statusTs = res.ts; statusChannel = res.channel; })
|
|
83
|
+
.catch(() => {});
|
|
84
|
+
} else if (Date.now() - lastEditTime >= EDIT_THROTTLE) {
|
|
85
|
+
lastEditTime = Date.now();
|
|
86
|
+
statusPromise = statusPromise.then(() => {
|
|
87
|
+
if (statusTs) return client.chat.update({ channel: statusChannel, ts: statusTs, text }).catch(() => {});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
69
91
|
});
|
|
70
92
|
if (result && result.content) {
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
|
|
93
|
+
const text = clamp(result.content);
|
|
94
|
+
if (statusPromise) {
|
|
95
|
+
await statusPromise;
|
|
96
|
+
if (statusTs) {
|
|
97
|
+
await client.chat.update({ channel: statusChannel, ts: statusTs, text }).catch(() => {});
|
|
98
|
+
} else {
|
|
99
|
+
await say(text);
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
await say(text);
|
|
74
103
|
}
|
|
75
|
-
await say(text);
|
|
76
104
|
}
|
|
77
105
|
} catch (err) {
|
|
78
106
|
await say(`error: ${err.message}`);
|
package/src/services/telegram.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const TelegramBot = require('node-telegram-bot-api');
|
|
2
2
|
|
|
3
3
|
const MAX_RESPONSE_LENGTH = 4000;
|
|
4
|
+
const EDIT_THROTTLE = 5000;
|
|
4
5
|
|
|
5
6
|
let bot = null;
|
|
6
7
|
|
|
@@ -15,6 +16,10 @@ function parseMessage(text) {
|
|
|
15
16
|
return { agent: match[1].toLowerCase(), command: match[2].trim() };
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
function clamp(text) {
|
|
20
|
+
return text.length > MAX_RESPONSE_LENGTH ? text.slice(0, MAX_RESPONSE_LENGTH) + '...' : text;
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
function start(config, callbacks) {
|
|
19
24
|
const { onStatus } = callbacks || {};
|
|
20
25
|
const { token } = config || {};
|
|
@@ -63,19 +68,40 @@ function start(config, callbacks) {
|
|
|
63
68
|
log(`${parsed.agent}: ${parsed.command}`);
|
|
64
69
|
bot.sendChatAction(msg.chat.id, 'typing').catch(() => {});
|
|
65
70
|
|
|
71
|
+
let statusMsgId = null;
|
|
72
|
+
let statusPromise = null;
|
|
73
|
+
let lastEditTime = 0;
|
|
74
|
+
|
|
66
75
|
try {
|
|
67
76
|
const result = await chatModule.enqueueMessage(agent.id, parsed.command, {
|
|
68
77
|
onAck: (text) => {
|
|
69
|
-
|
|
78
|
+
text = clamp(text);
|
|
79
|
+
if (!statusPromise) {
|
|
80
|
+
statusPromise = bot.sendMessage(msg.chat.id, text, { reply_to_message_id: msg.message_id })
|
|
81
|
+
.then(sent => { statusMsgId = sent.message_id; })
|
|
82
|
+
.catch(() => {});
|
|
83
|
+
} else if (Date.now() - lastEditTime >= EDIT_THROTTLE) {
|
|
84
|
+
lastEditTime = Date.now();
|
|
85
|
+
statusPromise = statusPromise.then(() => {
|
|
86
|
+
if (statusMsgId) return bot.editMessageText(text, { chat_id: msg.chat.id, message_id: statusMsgId }).catch(() => {});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
70
89
|
bot.sendChatAction(msg.chat.id, 'typing').catch(() => {});
|
|
71
90
|
}
|
|
72
91
|
});
|
|
92
|
+
|
|
73
93
|
if (result && result.content) {
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
|
|
94
|
+
const text = clamp(result.content);
|
|
95
|
+
if (statusPromise) {
|
|
96
|
+
await statusPromise;
|
|
97
|
+
if (statusMsgId) {
|
|
98
|
+
await bot.editMessageText(text, { chat_id: msg.chat.id, message_id: statusMsgId }).catch(() => {});
|
|
99
|
+
} else {
|
|
100
|
+
await bot.sendMessage(msg.chat.id, text, { reply_to_message_id: msg.message_id });
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
await bot.sendMessage(msg.chat.id, text, { reply_to_message_id: msg.message_id });
|
|
77
104
|
}
|
|
78
|
-
await bot.sendMessage(msg.chat.id, text, { reply_to_message_id: msg.message_id });
|
|
79
105
|
}
|
|
80
106
|
} catch (err) {
|
|
81
107
|
bot.sendMessage(msg.chat.id, `error: ${err.message}`, { reply_to_message_id: msg.message_id }).catch(() => {});
|
package/src/services/tools.js
CHANGED
|
@@ -353,7 +353,7 @@ async function spawnSubagent(input, context) {
|
|
|
353
353
|
|
|
354
354
|
return new Promise((resolve) => {
|
|
355
355
|
let done = false;
|
|
356
|
-
const args = ['-p', '--output-format', 'json', '--model', model, '--dangerously-skip-permissions'];
|
|
356
|
+
const args = ['-p', '--output-format', 'json', '--model', model, '--dangerously-skip-permissions', '--setting-sources', ''];
|
|
357
357
|
const proc = spawn('claude', args, {
|
|
358
358
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
359
359
|
});
|
package/src/services/whatsapp.js
CHANGED
|
@@ -76,16 +76,26 @@ function start(config, callbacks) {
|
|
|
76
76
|
log(`${parsed.agent}: ${parsed.command}`);
|
|
77
77
|
busy = true;
|
|
78
78
|
|
|
79
|
+
let lastAcked = null;
|
|
80
|
+
let lastAckTime = Date.now();
|
|
81
|
+
|
|
79
82
|
try {
|
|
80
83
|
const result = await chatModule.enqueueMessage(agent.id, parsed.command, {
|
|
81
|
-
onAck: (text) =>
|
|
84
|
+
onAck: (text) => {
|
|
85
|
+
if (Date.now() - lastAckTime < 15000) return;
|
|
86
|
+
lastAckTime = Date.now();
|
|
87
|
+
lastAcked = text;
|
|
88
|
+
message.reply(text).catch(() => {});
|
|
89
|
+
}
|
|
82
90
|
});
|
|
83
91
|
if (result && result.content) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
92
|
+
if (!(lastAcked && (result.content === lastAcked || result.content.startsWith(lastAcked) || lastAcked.startsWith(result.content)))) {
|
|
93
|
+
let text = result.content;
|
|
94
|
+
if (text.length > MAX_RESPONSE_LENGTH) {
|
|
95
|
+
text = text.slice(0, MAX_RESPONSE_LENGTH) + '...';
|
|
96
|
+
}
|
|
97
|
+
await message.reply(text);
|
|
87
98
|
}
|
|
88
|
-
await message.reply(text);
|
|
89
99
|
}
|
|
90
100
|
} catch (err) {
|
|
91
101
|
message.reply(`error: ${err.message}`).catch(() => {});
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
name: publish
|
|
2
|
-
on:
|
|
3
|
-
release:
|
|
4
|
-
types: [published]
|
|
5
|
-
jobs:
|
|
6
|
-
publish:
|
|
7
|
-
runs-on: ubuntu-latest
|
|
8
|
-
permissions:
|
|
9
|
-
contents: read
|
|
10
|
-
id-token: write
|
|
11
|
-
steps:
|
|
12
|
-
- uses: actions/checkout@v4
|
|
13
|
-
- uses: actions/setup-node@v4
|
|
14
|
-
with:
|
|
15
|
-
node-version: 20
|
|
16
|
-
registry-url: https://registry.npmjs.org
|
|
17
|
-
- run: npm install
|
|
18
|
-
- run: npm publish --provenance --access public
|