claudity 1.0.0 → 1.1.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/.github/workflows/publish.yml +18 -0
- package/install.sh +50 -12
- package/package.json +1 -1
- package/public/css/style.css +56 -9
- package/public/index.html +7 -7
- package/public/js/app.js +118 -15
- package/setup.sh +1 -1
- package/src/db.js +2 -2
- package/src/index.js +17 -1
- package/src/routes/api.js +23 -7
- package/src/services/chat.js +74 -22
- package/src/services/claude.js +86 -41
- package/src/services/connections.js +1 -1
- package/src/services/imessage.js +3 -2
- package/src/services/memory.js +2 -2
- package/src/services/signal.js +2 -3
- package/src/services/tools.js +7 -18
- package/src/services/whatsapp.js +2 -2
- package/src/services/workspace.js +7 -7
package/src/routes/api.js
CHANGED
|
@@ -15,7 +15,7 @@ router.get('/auth/status', (req, res) => {
|
|
|
15
15
|
router.post('/auth/api-key', (req, res) => {
|
|
16
16
|
const { key } = req.body;
|
|
17
17
|
if (!key) return res.status(400).json({ error: 'key required' });
|
|
18
|
-
if (!key.startsWith('sk-ant-api')) return res.status(400).json({ error: 'invalid api key
|
|
18
|
+
if (!key.startsWith('sk-ant-api')) return res.status(400).json({ error: 'invalid api key - must start with sk-ant-api. setup tokens and oauth tokens are not supported here. run claude setup-token instead.' });
|
|
19
19
|
auth.setApiKey(key);
|
|
20
20
|
res.json({ saved: true });
|
|
21
21
|
});
|
|
@@ -23,12 +23,12 @@ router.post('/auth/api-key', (req, res) => {
|
|
|
23
23
|
router.post('/auth/setup-token', (req, res) => {
|
|
24
24
|
const { token } = req.body;
|
|
25
25
|
if (!token) return res.status(400).json({ error: 'token required' });
|
|
26
|
-
if (!token.startsWith('sk-ant-oat')) return res.status(400).json({ error: 'invalid setup token
|
|
26
|
+
if (!token.startsWith('sk-ant-oat')) return res.status(400).json({ error: 'invalid setup token - must start with sk-ant-oat. if you have an api key, use the api key option instead.' });
|
|
27
27
|
try {
|
|
28
28
|
auth.writeSetupToken(token);
|
|
29
29
|
const status = auth.getAuthStatus();
|
|
30
30
|
if (status.authenticated) return res.json({ saved: true });
|
|
31
|
-
return res.status(400).json({ error: 'token saved to keychain but authentication failed
|
|
31
|
+
return res.status(400).json({ error: 'token saved to keychain but authentication failed - token may be invalid or expired' });
|
|
32
32
|
} catch (err) {
|
|
33
33
|
return res.status(500).json({ error: 'failed to write to keychain: ' + err.message });
|
|
34
34
|
}
|
|
@@ -44,14 +44,14 @@ router.get('/agents', (req, res) => {
|
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
router.post('/agents', (req, res) => {
|
|
47
|
-
const { name, is_default, model,
|
|
47
|
+
const { name, is_default, model, effort } = req.body;
|
|
48
48
|
if (!name) return res.status(400).json({ error: 'name required' });
|
|
49
49
|
const id = uuid();
|
|
50
50
|
try {
|
|
51
51
|
stmts.createAgent.run(id, name);
|
|
52
52
|
stmts.setBootstrapped.run(0, id);
|
|
53
53
|
if (model) stmts.setModel.run(model, id);
|
|
54
|
-
if (
|
|
54
|
+
if (effort) stmts.setEffort.run(effort, id);
|
|
55
55
|
if (is_default) {
|
|
56
56
|
stmts.clearDefaultAgent.run();
|
|
57
57
|
stmts.setDefaultAgent.run(id);
|
|
@@ -89,7 +89,7 @@ router.patch('/agents/:id', (req, res) => {
|
|
|
89
89
|
heartbeat.updateInterval(req.params.id, req.body.heartbeat_interval);
|
|
90
90
|
}
|
|
91
91
|
if (req.body.model) stmts.setModel.run(req.body.model, req.params.id);
|
|
92
|
-
if (req.body.
|
|
92
|
+
if (req.body.effort) stmts.setEffort.run(req.body.effort, req.params.id);
|
|
93
93
|
if ('show_heartbeat' in req.body) stmts.setShowHeartbeat.run(req.body.show_heartbeat ? 1 : 0, req.params.id);
|
|
94
94
|
res.json(stmts.getAgent.get(req.params.id));
|
|
95
95
|
});
|
|
@@ -121,6 +121,11 @@ router.delete('/agents/:id/messages', (req, res) => {
|
|
|
121
121
|
res.json({ cleared: true });
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
+
router.post('/agents/:id/stop', (req, res) => {
|
|
125
|
+
const stopped = chat.stopAgent(req.params.id);
|
|
126
|
+
res.json({ stopped });
|
|
127
|
+
});
|
|
128
|
+
|
|
124
129
|
router.post('/agents/:id/chat', (req, res) => {
|
|
125
130
|
const agent = stmts.getAgent.get(req.params.id);
|
|
126
131
|
if (!agent) return res.status(404).json({ error: 'agent not found' });
|
|
@@ -147,7 +152,18 @@ router.get('/agents/:id/stream', (req, res) => {
|
|
|
147
152
|
res.write(`event: connected\ndata: ${JSON.stringify({ agent_id: req.params.id })}\n\n`);
|
|
148
153
|
|
|
149
154
|
if (chat.isProcessing(req.params.id)) {
|
|
150
|
-
|
|
155
|
+
const activity = chat.getActivity(req.params.id);
|
|
156
|
+
const elapsed = activity ? Math.floor((Date.now() - activity.startTime) / 1000) : 0;
|
|
157
|
+
res.write(`event: typing\ndata: ${JSON.stringify({ active: true, elapsed })}\n\n`);
|
|
158
|
+
if (activity) {
|
|
159
|
+
if (activity.tool) {
|
|
160
|
+
const toolElapsed = activity.toolStartTime ? Math.floor((Date.now() - activity.toolStartTime) / 1000) : 0;
|
|
161
|
+
res.write(`event: tool_call\ndata: ${JSON.stringify({ name: activity.tool, elapsed, toolElapsed })}\n\n`);
|
|
162
|
+
}
|
|
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
|
+
}
|
|
151
167
|
}
|
|
152
168
|
|
|
153
169
|
chat.addStream(req.params.id, res);
|
package/src/services/chat.js
CHANGED
|
@@ -8,6 +8,7 @@ const workspace = require('./workspace');
|
|
|
8
8
|
const agentStreams = new Map();
|
|
9
9
|
const messageQueues = new Map();
|
|
10
10
|
const processingAgents = new Set();
|
|
11
|
+
const agentActivity = new Map();
|
|
11
12
|
|
|
12
13
|
function addStream(agentId, res) {
|
|
13
14
|
if (!agentStreams.has(agentId)) agentStreams.set(agentId, []);
|
|
@@ -84,28 +85,28 @@ your workspace is at data/agents/${workspace.sanitizeName(agent.name)}/. use wri
|
|
|
84
85
|
|
|
85
86
|
if (dailyLogs) prompt += `recent context:\n${dailyLogs}\n\n`;
|
|
86
87
|
|
|
87
|
-
prompt += `adapt your tone and style naturally to match whoever you are talking to. be personable. you are not a task executor
|
|
88
|
+
prompt += `adapt your tone and style naturally to match whoever you are talking to. be personable. you are not a task executor - you are a conversational agent who can also get things done when asked.
|
|
88
89
|
|
|
89
|
-
you have full machine access
|
|
90
|
+
you have full machine access - bash, file read/write/edit, glob, grep - all available as built-in tools in your environment. use them freely to accomplish tasks: run commands, read/write files, explore the filesystem, execute scripts, etc.
|
|
90
91
|
|
|
91
92
|
available claudity tools:
|
|
92
93
|
${toolList}
|
|
93
94
|
|
|
94
95
|
use spawn_subagent to offload complex or time-consuming work (writing code, running multi-step commands, analysis) to an ephemeral subprocess. the subagent has full machine access but no claudity tools or memory.
|
|
95
96
|
|
|
96
|
-
use delegate to collaborate with other agents
|
|
97
|
+
use delegate to collaborate with other agents - send a message to another agent by name and get their response. useful when a task falls in another agent's domain.
|
|
97
98
|
|
|
98
99
|
your memories are automatically extracted from conversations and written to daily logs. use the remember tool for critical standing instructions or preferences you must never lose.
|
|
99
100
|
|
|
100
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.
|
|
101
102
|
|
|
102
|
-
when using tools, just use them naturally as part of the conversation
|
|
103
|
+
when using tools, just use them naturally as part of the conversation - no need to announce plans or ask permission. when interacting with external platforms, read their documentation first to understand the api.
|
|
103
104
|
|
|
104
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.
|
|
105
106
|
|
|
106
|
-
when you receive a [scheduled reminder], just do the thing
|
|
107
|
+
when you receive a [scheduled reminder], just do the thing - no need to announce that it was a reminder. act naturally.
|
|
107
108
|
|
|
108
|
-
CRITICAL: users cannot see tool results. they only see your final text response. when you use tools, you MUST include every key detail from the results
|
|
109
|
+
CRITICAL: users cannot see tool results. they only see your final text response. when you use tools, you MUST include every key detail from the results - urls, links, claim links, confirmation codes, registration links, usernames, error messages, anything actionable. if you don't include it in your response, the user will never see it. never summarize away actionable information.
|
|
109
110
|
|
|
110
111
|
you know nothing about the user until they tell you. do not infer, guess, or use any username, file path, hostname, or environment variable to identify them. if you see a username in a path or system info, ignore it completely. never address the user by name until they introduce themselves.`;
|
|
111
112
|
|
|
@@ -146,17 +147,36 @@ async function handleMessage(agentId, userContent, options = {}) {
|
|
|
146
147
|
|
|
147
148
|
if (!isHeartbeat) {
|
|
148
149
|
processingAgents.add(agentId);
|
|
150
|
+
agentActivity.set(agentId, { startTime: Date.now(), tool: null, toolStartTime: null, summary: null });
|
|
149
151
|
emit(agentId, 'typing', { active: true });
|
|
150
152
|
}
|
|
151
153
|
|
|
152
154
|
let responseComplete = false;
|
|
155
|
+
const startTime = Date.now();
|
|
156
|
+
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
|
+
}
|
|
153
173
|
|
|
154
174
|
const systemPrompt = buildSystemPrompt(agent);
|
|
155
175
|
const toolDefs = tools.getAllToolDefinitions();
|
|
156
176
|
const isBootstrap = agent.bootstrapped === 0;
|
|
157
177
|
const sessionAgentId = (!isBootstrap && !isHeartbeat) ? agentId : null;
|
|
158
178
|
const model = agent.model || 'opus';
|
|
159
|
-
const
|
|
179
|
+
const effort = (isBootstrap || isHeartbeat) ? 'low' : (agent.effort || 'high');
|
|
160
180
|
|
|
161
181
|
let messages = isHeartbeat
|
|
162
182
|
? [{ role: 'user', content: userContent }]
|
|
@@ -165,11 +185,25 @@ async function handleMessage(agentId, userContent, options = {}) {
|
|
|
165
185
|
let allToolCalls = [];
|
|
166
186
|
let intermediateTexts = [];
|
|
167
187
|
|
|
168
|
-
const wantsAck = !isHeartbeat && !isScheduled &&
|
|
188
|
+
const wantsAck = !isHeartbeat && !isScheduled && !isBootstrap;
|
|
169
189
|
const ackPromise = wantsAck
|
|
170
190
|
? claude.generateQuickAck(userContent, agent.name).catch(() => null)
|
|
171
191
|
: Promise.resolve(null);
|
|
172
192
|
|
|
193
|
+
const onEvent = !isHeartbeat ? (event) => {
|
|
194
|
+
if (event.type === 'assistant' && event.message && event.message.content) {
|
|
195
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
196
|
+
for (const block of event.message.content) {
|
|
197
|
+
if (block.type === 'tool_use') {
|
|
198
|
+
lastToolName = block.name;
|
|
199
|
+
const state = agentActivity.get(agentId);
|
|
200
|
+
if (state) { state.tool = block.name; state.toolStartTime = Date.now(); }
|
|
201
|
+
emit(agentId, 'tool_call', { name: block.name, input: block.input, elapsed });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} : null;
|
|
206
|
+
|
|
173
207
|
const mainPromise = claude.sendMessage({
|
|
174
208
|
system: systemPrompt,
|
|
175
209
|
messages,
|
|
@@ -177,8 +211,9 @@ async function handleMessage(agentId, userContent, options = {}) {
|
|
|
177
211
|
maxTokens: 4096,
|
|
178
212
|
agentId: sessionAgentId,
|
|
179
213
|
model,
|
|
180
|
-
|
|
181
|
-
noBuiltinTools: isBootstrap
|
|
214
|
+
effort,
|
|
215
|
+
noBuiltinTools: isBootstrap,
|
|
216
|
+
onEvent
|
|
182
217
|
});
|
|
183
218
|
|
|
184
219
|
try {
|
|
@@ -223,7 +258,9 @@ async function handleMessage(agentId, userContent, options = {}) {
|
|
|
223
258
|
const toolResults = [];
|
|
224
259
|
|
|
225
260
|
for (const call of calls) {
|
|
226
|
-
|
|
261
|
+
lastToolName = call.name;
|
|
262
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
263
|
+
emit(agentId, 'tool_call', { name: call.name, input: call.input, elapsed });
|
|
227
264
|
|
|
228
265
|
try {
|
|
229
266
|
const result = await tools.executeTool(call.name, call.input, { agentId });
|
|
@@ -257,8 +294,9 @@ async function handleMessage(agentId, userContent, options = {}) {
|
|
|
257
294
|
maxTokens: 4096,
|
|
258
295
|
agentId: sessionAgentId,
|
|
259
296
|
model,
|
|
260
|
-
|
|
261
|
-
noBuiltinTools: isBootstrap
|
|
297
|
+
effort,
|
|
298
|
+
noBuiltinTools: isBootstrap,
|
|
299
|
+
onEvent
|
|
262
300
|
});
|
|
263
301
|
}
|
|
264
302
|
|
|
@@ -311,13 +349,8 @@ async function handleMessage(agentId, userContent, options = {}) {
|
|
|
311
349
|
|
|
312
350
|
stmts.createMessage.run(assistantMsgId, agentId, 'assistant', responseText, toolCallsJson);
|
|
313
351
|
|
|
314
|
-
if (isBootstrap && stmts.getAgent.get(agentId)?.bootstrapped
|
|
315
|
-
|
|
316
|
-
if (msgCount >= 4) {
|
|
317
|
-
stmts.setBootstrapped.run(1, agentId);
|
|
318
|
-
workspace.deleteFile(agent.name, 'BOOTSTRAP.md');
|
|
319
|
-
emit(agentId, 'bootstrap_complete', {});
|
|
320
|
-
}
|
|
352
|
+
if (isBootstrap && stmts.getAgent.get(agentId)?.bootstrapped !== 0) {
|
|
353
|
+
emit(agentId, 'bootstrap_complete', {});
|
|
321
354
|
}
|
|
322
355
|
|
|
323
356
|
if (!isHeartbeat) {
|
|
@@ -326,8 +359,10 @@ async function handleMessage(agentId, userContent, options = {}) {
|
|
|
326
359
|
});
|
|
327
360
|
}
|
|
328
361
|
|
|
362
|
+
if (statusInterval) clearInterval(statusInterval);
|
|
329
363
|
responseComplete = true;
|
|
330
364
|
processingAgents.delete(agentId);
|
|
365
|
+
agentActivity.delete(agentId);
|
|
331
366
|
emit(agentId, 'typing', { active: false });
|
|
332
367
|
emit(agentId, 'assistant_message', {
|
|
333
368
|
id: assistantMsgId,
|
|
@@ -338,10 +373,13 @@ async function handleMessage(agentId, userContent, options = {}) {
|
|
|
338
373
|
return { id: assistantMsgId, content: responseText, tool_calls: allToolCalls.length ? allToolCalls : null };
|
|
339
374
|
|
|
340
375
|
} catch (err) {
|
|
376
|
+
if (statusInterval) clearInterval(statusInterval);
|
|
341
377
|
responseComplete = true;
|
|
342
378
|
processingAgents.delete(agentId);
|
|
379
|
+
agentActivity.delete(agentId);
|
|
343
380
|
if (!isHeartbeat) emit(agentId, 'typing', { active: false });
|
|
344
|
-
if (!isHeartbeat) emit(agentId, 'error', { error: err.message });
|
|
381
|
+
if (!isHeartbeat && err.message !== 'aborted') emit(agentId, 'error', { error: err.message });
|
|
382
|
+
if (err.message === 'aborted') return;
|
|
345
383
|
throw err;
|
|
346
384
|
}
|
|
347
385
|
}
|
|
@@ -362,4 +400,18 @@ function isProcessing(agentId) {
|
|
|
362
400
|
return processingAgents.has(agentId);
|
|
363
401
|
}
|
|
364
402
|
|
|
365
|
-
|
|
403
|
+
function stopAgent(agentId) {
|
|
404
|
+
if (!processingAgents.has(agentId)) return false;
|
|
405
|
+
claude.abort(agentId);
|
|
406
|
+
processingAgents.delete(agentId);
|
|
407
|
+
agentActivity.delete(agentId);
|
|
408
|
+
emit(agentId, 'typing', { active: false });
|
|
409
|
+
emit(agentId, 'stopped', {});
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function getActivity(agentId) {
|
|
414
|
+
return agentActivity.get(agentId) || null;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
module.exports = { handleMessage, enqueueMessage, addStream, removeStream, emit, isProcessing, stopAgent, getActivity };
|
package/src/services/claude.js
CHANGED
|
@@ -8,6 +8,7 @@ const { stmts } = require('../db');
|
|
|
8
8
|
|
|
9
9
|
const API_URL = 'https://api.anthropic.com/v1/messages';
|
|
10
10
|
const MODEL = 'claude-opus-4-6';
|
|
11
|
+
const activeProcesses = new Map();
|
|
11
12
|
|
|
12
13
|
function hashPrompt(str) {
|
|
13
14
|
let h = 0;
|
|
@@ -17,24 +18,20 @@ function hashPrompt(str) {
|
|
|
17
18
|
return h.toString(36);
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
function invalidateSession(agentId) {
|
|
21
|
-
stmts.deleteSession.run(agentId);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
21
|
function isContextOverflow(err) {
|
|
25
22
|
const msg = (err.message || '').toLowerCase();
|
|
26
23
|
return msg.includes('context') || msg.includes('overflow') || msg.includes('too long') || msg.includes('token limit');
|
|
27
24
|
}
|
|
28
25
|
|
|
29
|
-
async function sendMessage({ system, messages, tools, maxTokens = 4096, agentId, model = 'opus',
|
|
26
|
+
async function sendMessage({ system, messages, tools, maxTokens = 4096, agentId, model = 'opus', effort = 'high', noBuiltinTools = false, onEvent = null }) {
|
|
30
27
|
const status = auth.getAuthStatus();
|
|
31
|
-
if (!status.authenticated) throw new Error('not authenticated
|
|
28
|
+
if (!status.authenticated) throw new Error('not authenticated - run claude setup-token');
|
|
32
29
|
|
|
33
30
|
if (status.mode === 'api_key') {
|
|
34
31
|
return sendViaApi({ system, messages, tools, maxTokens });
|
|
35
32
|
}
|
|
36
33
|
|
|
37
|
-
return sendViaCli({ system, messages, tools, maxTokens, agentId, model,
|
|
34
|
+
return sendViaCli({ system, messages, tools, maxTokens, agentId, model, effort, noBuiltinTools, onEvent });
|
|
38
35
|
}
|
|
39
36
|
|
|
40
37
|
async function sendViaApi({ system, messages, tools, maxTokens }) {
|
|
@@ -110,40 +107,62 @@ function buildContext(messages) {
|
|
|
110
107
|
}).filter(Boolean).join('\n\n');
|
|
111
108
|
}
|
|
112
109
|
|
|
113
|
-
function runCli(args, input,
|
|
110
|
+
function runCli(args, input, extraEnv = {}, agentId = null, onEvent = null) {
|
|
114
111
|
return new Promise((resolve, reject) => {
|
|
115
112
|
let done = false;
|
|
116
113
|
const env = { ...process.env, ...extraEnv };
|
|
114
|
+
delete env.CLAUDECODE;
|
|
117
115
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'claudity-'));
|
|
118
116
|
const proc = spawn('claude', args, {
|
|
119
117
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
120
|
-
timeout: timeoutMs,
|
|
121
118
|
cwd,
|
|
122
119
|
env
|
|
123
120
|
});
|
|
124
121
|
|
|
125
|
-
|
|
126
|
-
if (!done) {
|
|
127
|
-
done = true;
|
|
128
|
-
proc.kill();
|
|
129
|
-
reject(new Error('claude cli timed out'));
|
|
130
|
-
}
|
|
131
|
-
}, timeoutMs + 10000);
|
|
122
|
+
if (agentId) activeProcesses.set(agentId, proc);
|
|
132
123
|
|
|
133
124
|
let stdout = '';
|
|
134
125
|
let stderr = '';
|
|
135
|
-
|
|
136
|
-
|
|
126
|
+
let lastResult = null;
|
|
127
|
+
let buffer = '';
|
|
128
|
+
|
|
129
|
+
proc.stdout.on('data', d => {
|
|
130
|
+
const chunk = d.toString();
|
|
131
|
+
stdout += chunk;
|
|
132
|
+
if (onEvent) {
|
|
133
|
+
buffer += chunk;
|
|
134
|
+
const lines = buffer.split('\n');
|
|
135
|
+
buffer = lines.pop();
|
|
136
|
+
for (const line of lines) {
|
|
137
|
+
if (!line.trim()) continue;
|
|
138
|
+
try {
|
|
139
|
+
const event = JSON.parse(line);
|
|
140
|
+
if (event.type === 'result') lastResult = line;
|
|
141
|
+
onEvent(event);
|
|
142
|
+
} catch {}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
137
146
|
proc.stderr.on('data', d => stderr += d);
|
|
138
147
|
|
|
139
148
|
proc.on('close', code => {
|
|
140
149
|
if (done) return;
|
|
141
150
|
done = true;
|
|
142
|
-
|
|
143
|
-
if (
|
|
151
|
+
if (agentId) activeProcesses.delete(agentId);
|
|
152
|
+
if (onEvent && lastResult) {
|
|
153
|
+
resolve(lastResult);
|
|
154
|
+
} else if (onEvent && !lastResult) {
|
|
155
|
+
if (code === null) {
|
|
156
|
+
reject(new Error('aborted'));
|
|
157
|
+
} else {
|
|
158
|
+
reject(new Error(`claude cli exited with code ${code}${stderr.trim() ? ': ' + stderr.trim() : ''}`));
|
|
159
|
+
}
|
|
160
|
+
} else if (stdout.trim()) {
|
|
144
161
|
resolve(stdout.trim());
|
|
145
|
-
} else if (code !== 0) {
|
|
146
|
-
reject(new Error(`claude cli exited ${code}
|
|
162
|
+
} else if (code !== 0 && code !== null) {
|
|
163
|
+
reject(new Error(`claude cli exited with code ${code}${stderr.trim() ? ': ' + stderr.trim() : ''}`));
|
|
164
|
+
} else if (code === null) {
|
|
165
|
+
reject(new Error('claude cli process was terminated unexpectedly - try again'));
|
|
147
166
|
} else {
|
|
148
167
|
resolve('');
|
|
149
168
|
}
|
|
@@ -152,7 +171,6 @@ function runCli(args, input, timeoutMs = 300000, extraEnv = {}) {
|
|
|
152
171
|
proc.on('error', err => {
|
|
153
172
|
if (done) return;
|
|
154
173
|
done = true;
|
|
155
|
-
clearTimeout(fallback);
|
|
156
174
|
reject(err);
|
|
157
175
|
});
|
|
158
176
|
|
|
@@ -161,13 +179,11 @@ function runCli(args, input, timeoutMs = 300000, extraEnv = {}) {
|
|
|
161
179
|
});
|
|
162
180
|
}
|
|
163
181
|
|
|
164
|
-
async function sendViaCli({ system, messages, tools, maxTokens, agentId, model = 'opus',
|
|
182
|
+
async function sendViaCli({ system, messages, tools, maxTokens, agentId, model = 'opus', effort = 'high', noBuiltinTools = false, onEvent = null }) {
|
|
165
183
|
const lastUserMsg = messages.filter(m => m.role === 'user').pop();
|
|
166
184
|
const promptText = buildPromptText(lastUserMsg);
|
|
167
185
|
const sysPrompt = buildFullSysPrompt(system, tools);
|
|
168
186
|
const currentHash = hashPrompt(sysPrompt);
|
|
169
|
-
const thinkingTokens = { low: '0', medium: '16000', high: '31999' };
|
|
170
|
-
const extraEnv = { MAX_THINKING_TOKENS: thinkingTokens[thinking] || '31999' };
|
|
171
187
|
|
|
172
188
|
const session = agentId ? stmts.getSession.get(agentId) : null;
|
|
173
189
|
const canResume = session && session.prompt_hash === currentHash;
|
|
@@ -175,24 +191,24 @@ async function sendViaCli({ system, messages, tools, maxTokens, agentId, model =
|
|
|
175
191
|
let output;
|
|
176
192
|
|
|
177
193
|
if (canResume) {
|
|
178
|
-
const args = ['-p', '--output-format', 'json', '--dangerously-skip-permissions', '--setting-sources', '', '--resume', session.session_id];
|
|
194
|
+
const args = ['-p', '--output-format', 'stream-json', '--verbose', '--effort', effort, '--dangerously-skip-permissions', '--setting-sources', '', '--resume', session.session_id];
|
|
179
195
|
try {
|
|
180
|
-
output = await runCli(args, promptText,
|
|
196
|
+
output = await runCli(args, promptText, {}, agentId, onEvent);
|
|
197
|
+
return processCliOutput(output, tools);
|
|
181
198
|
} catch (err) {
|
|
199
|
+
if (err.message === 'aborted') throw err;
|
|
182
200
|
stmts.deleteSession.run(agentId);
|
|
183
201
|
if (isContextOverflow(err)) {
|
|
184
|
-
return sendCliFresh({ sysPrompt, messages: [messages[messages.length - 1]], promptText, tools, agentId, currentHash, model,
|
|
202
|
+
return sendCliFresh({ sysPrompt, messages: [messages[messages.length - 1]], promptText, tools, agentId, currentHash, model, effort, noBuiltinTools, onEvent });
|
|
185
203
|
}
|
|
186
|
-
return sendCliFresh({ sysPrompt, messages, promptText, tools, agentId, currentHash, model,
|
|
204
|
+
return sendCliFresh({ sysPrompt, messages, promptText, tools, agentId, currentHash, model, effort, noBuiltinTools, onEvent });
|
|
187
205
|
}
|
|
188
|
-
} else {
|
|
189
|
-
return sendCliFresh({ sysPrompt, messages, promptText, tools, agentId, currentHash, model, extraEnv, noBuiltinTools });
|
|
190
206
|
}
|
|
191
207
|
|
|
192
|
-
return
|
|
208
|
+
return sendCliFresh({ sysPrompt, messages, promptText, tools, agentId, currentHash, model, effort, noBuiltinTools, onEvent });
|
|
193
209
|
}
|
|
194
210
|
|
|
195
|
-
async function sendCliFresh({ sysPrompt, messages, promptText, tools, agentId, currentHash, model = 'opus',
|
|
211
|
+
async function sendCliFresh({ sysPrompt, messages, promptText, tools, agentId, currentHash, model = 'opus', effort = 'high', noBuiltinTools = false, onEvent = null }) {
|
|
196
212
|
const sessionId = randomUUID();
|
|
197
213
|
const context = buildContext(messages);
|
|
198
214
|
|
|
@@ -200,14 +216,14 @@ async function sendCliFresh({ sysPrompt, messages, promptText, tools, agentId, c
|
|
|
200
216
|
if (context) fullPrompt += `previous conversation:\n${context}\n\n`;
|
|
201
217
|
fullPrompt += promptText;
|
|
202
218
|
|
|
203
|
-
const args = ['-p', '--output-format', 'json', '--model', model, '--dangerously-skip-permissions', '--setting-sources', '', '--session-id', sessionId];
|
|
219
|
+
const args = ['-p', '--output-format', 'stream-json', '--verbose', '--model', model, '--effort', effort, '--dangerously-skip-permissions', '--setting-sources', '', '--session-id', sessionId];
|
|
204
220
|
if (noBuiltinTools) args.push('--tools', '');
|
|
205
221
|
if (sysPrompt) {
|
|
206
222
|
args.push('--system-prompt', sysPrompt);
|
|
207
223
|
}
|
|
208
224
|
|
|
209
225
|
try {
|
|
210
|
-
const output = await runCli(args, fullPrompt,
|
|
226
|
+
const output = await runCli(args, fullPrompt, {}, agentId, onEvent);
|
|
211
227
|
|
|
212
228
|
if (agentId) {
|
|
213
229
|
stmts.upsertSession.run(agentId, sessionId, currentHash);
|
|
@@ -215,10 +231,11 @@ async function sendCliFresh({ sysPrompt, messages, promptText, tools, agentId, c
|
|
|
215
231
|
|
|
216
232
|
return processCliOutput(output, tools);
|
|
217
233
|
} catch (err) {
|
|
234
|
+
if (err.message === 'aborted') throw err;
|
|
218
235
|
if (isContextOverflow(err) && context) {
|
|
219
|
-
const retryArgs = ['-p', '--output-format', 'json', '--model', model, '--dangerously-skip-permissions', '--setting-sources', '', '--session-id', randomUUID()];
|
|
236
|
+
const retryArgs = ['-p', '--output-format', 'stream-json', '--verbose', '--model', model, '--effort', effort, '--dangerously-skip-permissions', '--setting-sources', '', '--session-id', randomUUID()];
|
|
220
237
|
if (sysPrompt) retryArgs.push('--system-prompt', sysPrompt);
|
|
221
|
-
const output = await runCli(retryArgs, promptText,
|
|
238
|
+
const output = await runCli(retryArgs, promptText, {}, agentId);
|
|
222
239
|
return processCliOutput(output, tools);
|
|
223
240
|
}
|
|
224
241
|
throw err;
|
|
@@ -229,6 +246,10 @@ function processCliOutput(output, tools) {
|
|
|
229
246
|
const parsed = parseCliOutput(output);
|
|
230
247
|
if (!parsed) throw new Error('claude cli returned empty response');
|
|
231
248
|
|
|
249
|
+
if (parsed.is_error && parsed.num_turns === 0) {
|
|
250
|
+
throw new Error('cli session error');
|
|
251
|
+
}
|
|
252
|
+
|
|
232
253
|
if (parsed.is_error || parsed.subtype === 'error_max_turns') {
|
|
233
254
|
const text = typeof parsed.result === 'string' && parsed.result.length > 0
|
|
234
255
|
? parsed.result
|
|
@@ -339,9 +360,9 @@ function hasToolUse(response) {
|
|
|
339
360
|
|
|
340
361
|
async function generateQuickAck(userContent, agentName) {
|
|
341
362
|
const args = ['-p', '--output-format', 'json', '--model', 'haiku', '--dangerously-skip-permissions', '--setting-sources', ''];
|
|
342
|
-
const prompt = `you are ${agentName}
|
|
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.`;
|
|
343
364
|
try {
|
|
344
|
-
const output = await runCli(args, prompt
|
|
365
|
+
const output = await runCli(args, prompt);
|
|
345
366
|
const parsed = parseCliOutput(output);
|
|
346
367
|
if (parsed && typeof parsed.result === 'string' && parsed.result.length > 0) {
|
|
347
368
|
return parsed.result.trim();
|
|
@@ -350,4 +371,28 @@ async function generateQuickAck(userContent, agentName) {
|
|
|
350
371
|
return null;
|
|
351
372
|
}
|
|
352
373
|
|
|
353
|
-
|
|
374
|
+
function abort(agentId) {
|
|
375
|
+
const proc = activeProcesses.get(agentId);
|
|
376
|
+
if (proc) {
|
|
377
|
+
proc.kill();
|
|
378
|
+
activeProcesses.delete(agentId);
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function generateStatus(toolName, elapsedSeconds, agentName) {
|
|
385
|
+
const mins = Math.floor(elapsedSeconds / 60);
|
|
386
|
+
const args = ['-p', '--output-format', 'json', '--model', 'haiku', '--dangerously-skip-permissions', '--setting-sources', ''];
|
|
387
|
+
const prompt = `you are ${agentName}. you've been working for ${mins} minute${mins === 1 ? '' : 's'} and you're currently using ${toolName}. give a one-sentence first-person casual status update for your user. no quotes.`;
|
|
388
|
+
try {
|
|
389
|
+
const output = await runCli(args, prompt);
|
|
390
|
+
const parsed = parseCliOutput(output);
|
|
391
|
+
if (parsed && typeof parsed.result === 'string' && parsed.result.length > 0) {
|
|
392
|
+
return parsed.result.trim().replace(/^["']|["']$/g, '');
|
|
393
|
+
}
|
|
394
|
+
} catch {}
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
module.exports = { sendMessage, extractText, extractToolUse, hasToolUse, generateQuickAck, generateStatus, abort };
|
|
@@ -19,7 +19,7 @@ function log(msg) {
|
|
|
19
19
|
function onStatus(platform) {
|
|
20
20
|
return (status, detail) => {
|
|
21
21
|
stmts.updateConnectionStatus.run(status, detail || null, platform);
|
|
22
|
-
log(`${platform}: ${status}${detail ? '
|
|
22
|
+
log(`${platform}: ${status}${detail ? ' - ' + detail : ''}`);
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
|
package/src/services/imessage.js
CHANGED
|
@@ -113,7 +113,7 @@ end tell`;
|
|
|
113
113
|
function deleteResponseEcho(afterRowId, text) {
|
|
114
114
|
try {
|
|
115
115
|
const echo = db.prepare(
|
|
116
|
-
"select m.ROWID from message m where m.is_from_me =
|
|
116
|
+
"select m.ROWID from message m where m.is_from_me = 1 and m.ROWID > ? and m.text = ?"
|
|
117
117
|
).get(afterRowId, text);
|
|
118
118
|
|
|
119
119
|
if (echo) {
|
|
@@ -134,7 +134,8 @@ function poll() {
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
if (!msg.text) continue;
|
|
137
|
-
|
|
137
|
+
const msgText = msg.text.length > MAX_RESPONSE_LENGTH ? msg.text.slice(0, MAX_RESPONSE_LENGTH) + '...' : msg.text;
|
|
138
|
+
if (recentSent.has(msg.text) || recentSent.has(msgText)) continue;
|
|
138
139
|
|
|
139
140
|
const { stmts } = require('../db');
|
|
140
141
|
let parsed = parseMessage(msg.text);
|
package/src/services/memory.js
CHANGED
|
@@ -139,7 +139,7 @@ function startConsolidation() {
|
|
|
139
139
|
if (memories.length < 5) continue;
|
|
140
140
|
|
|
141
141
|
const memoryList = memories.map(m => `- ${m.summary}`).join('\n');
|
|
142
|
-
const systemPrompt = `you are a memory consolidation tool. your ONLY job is to deduplicate and clean up a list of factual memories. output ONLY a bullet list
|
|
142
|
+
const systemPrompt = `you are a memory consolidation tool. your ONLY job is to deduplicate and clean up a list of factual memories. output ONLY a bullet list - one fact per line, each starting with "- ". do NOT add commentary, explanations, confirmations, or any text that is not a memory. do NOT say "done" or "here is your list" or anything like that. just output the cleaned list.`;
|
|
143
143
|
|
|
144
144
|
const result = await callLightweight(memoryList, systemPrompt);
|
|
145
145
|
if (!result || result.trim().toLowerCase() === 'none') continue;
|
|
@@ -153,7 +153,7 @@ function startConsolidation() {
|
|
|
153
153
|
|
|
154
154
|
if (lines.length === 0) continue;
|
|
155
155
|
if (lines.length < memories.length * 0.3) {
|
|
156
|
-
console.log(`[memory] skipping consolidation for ${agent.name}
|
|
156
|
+
console.log(`[memory] skipping consolidation for ${agent.name} - output too small (${lines.length} vs ${memories.length})`);
|
|
157
157
|
continue;
|
|
158
158
|
}
|
|
159
159
|
|
package/src/services/signal.js
CHANGED
|
@@ -66,7 +66,6 @@ function startDaemon(phone, onStatus, chatModule, stmts) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
if (!text || !sender) continue;
|
|
69
|
-
if (!sender) continue;
|
|
70
69
|
|
|
71
70
|
let parsed = parseMessage(text);
|
|
72
71
|
let agent;
|
|
@@ -191,7 +190,7 @@ function startLinking(onStatus, chatModule, stmts, updateConfig) {
|
|
|
191
190
|
|
|
192
191
|
if (!linked) {
|
|
193
192
|
log(`link process exited with code ${code}`);
|
|
194
|
-
if (onStatus) onStatus('error', 'linking failed
|
|
193
|
+
if (onStatus) onStatus('error', 'linking failed - scan the qr code within 60 seconds');
|
|
195
194
|
}
|
|
196
195
|
});
|
|
197
196
|
|
|
@@ -204,7 +203,7 @@ function start(config, callbacks) {
|
|
|
204
203
|
const { onStatus } = callbacks || {};
|
|
205
204
|
|
|
206
205
|
if (!signalCliAvailable()) {
|
|
207
|
-
if (onStatus) onStatus('error', 'signal-cli not found
|
|
206
|
+
if (onStatus) onStatus('error', 'signal-cli not found - install with: brew install signal-cli');
|
|
208
207
|
return;
|
|
209
208
|
}
|
|
210
209
|
|