Merge branch 'main' into upstream/fix-register-claude-md
This commit is contained in:
@@ -287,4 +287,5 @@ Tell user to test: send a message in their registered chat. Show: `tail -f logs/
|
||||
|
||||
## 9. Diagnostics
|
||||
|
||||
Send diagnostics data by following `.claude/skills/setup/diagnostics.md`.
|
||||
1. Use the Read tool to read `.claude/skills/setup/diagnostics.md`.
|
||||
2. Follow every step in that file before completing setup.
|
||||
|
||||
@@ -237,4 +237,5 @@ Tell the user:
|
||||
|
||||
## Diagnostics
|
||||
|
||||
Send diagnostics data by following `.claude/skills/update-nanoclaw/diagnostics.md`.
|
||||
1. Use the Read tool to read `.claude/skills/update-nanoclaw/diagnostics.md`.
|
||||
2. Follow every step in that file before finishing.
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { execFile } from 'child_process';
|
||||
import { query, HookCallback, PreCompactHookInput } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
@@ -27,6 +28,7 @@ interface ContainerInput {
|
||||
isMain: boolean;
|
||||
isScheduledTask?: boolean;
|
||||
assistantName?: string;
|
||||
script?: string;
|
||||
}
|
||||
|
||||
interface ContainerOutput {
|
||||
@@ -464,6 +466,55 @@ async function runQuery(
|
||||
return { newSessionId, lastAssistantUuid, closedDuringQuery };
|
||||
}
|
||||
|
||||
interface ScriptResult {
|
||||
wakeAgent: boolean;
|
||||
data?: unknown;
|
||||
}
|
||||
|
||||
const SCRIPT_TIMEOUT_MS = 30_000;
|
||||
|
||||
async function runScript(script: string): Promise<ScriptResult | null> {
|
||||
const scriptPath = '/tmp/task-script.sh';
|
||||
fs.writeFileSync(scriptPath, script, { mode: 0o755 });
|
||||
|
||||
return new Promise((resolve) => {
|
||||
execFile('bash', [scriptPath], {
|
||||
timeout: SCRIPT_TIMEOUT_MS,
|
||||
maxBuffer: 1024 * 1024,
|
||||
env: process.env,
|
||||
}, (error, stdout, stderr) => {
|
||||
if (stderr) {
|
||||
log(`Script stderr: ${stderr.slice(0, 500)}`);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
log(`Script error: ${error.message}`);
|
||||
return resolve(null);
|
||||
}
|
||||
|
||||
// Parse last non-empty line of stdout as JSON
|
||||
const lines = stdout.trim().split('\n');
|
||||
const lastLine = lines[lines.length - 1];
|
||||
if (!lastLine) {
|
||||
log('Script produced no output');
|
||||
return resolve(null);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = JSON.parse(lastLine);
|
||||
if (typeof result.wakeAgent !== 'boolean') {
|
||||
log(`Script output missing wakeAgent boolean: ${lastLine.slice(0, 200)}`);
|
||||
return resolve(null);
|
||||
}
|
||||
resolve(result as ScriptResult);
|
||||
} catch {
|
||||
log(`Script output is not valid JSON: ${lastLine.slice(0, 200)}`);
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
let containerInput: ContainerInput;
|
||||
|
||||
@@ -505,6 +556,26 @@ async function main(): Promise<void> {
|
||||
prompt += '\n' + pending.join('\n');
|
||||
}
|
||||
|
||||
// Script phase: run script before waking agent
|
||||
if (containerInput.script && containerInput.isScheduledTask) {
|
||||
log('Running task script...');
|
||||
const scriptResult = await runScript(containerInput.script);
|
||||
|
||||
if (!scriptResult || !scriptResult.wakeAgent) {
|
||||
const reason = scriptResult ? 'wakeAgent=false' : 'script error/no output';
|
||||
log(`Script decided not to wake agent: ${reason}`);
|
||||
writeOutput({
|
||||
status: 'success',
|
||||
result: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Script says wake agent — enrich prompt with script data
|
||||
log(`Script wakeAgent=true, enriching prompt with data`);
|
||||
prompt = `[SCHEDULED TASK]\n\nScript output:\n${JSON.stringify(scriptResult.data, null, 2)}\n\nInstructions:\n${containerInput.prompt}`;
|
||||
}
|
||||
|
||||
// Query loop: run query → wait for IPC message → run new query → repeat
|
||||
let resumeAt: string | undefined;
|
||||
try {
|
||||
|
||||
@@ -91,6 +91,7 @@ SCHEDULE VALUE FORMAT (all times are LOCAL timezone):
|
||||
schedule_value: z.string().describe('cron: "*/5 * * * *" | interval: milliseconds like "300000" | once: local timestamp like "2026-02-01T15:30:00" (no Z suffix!)'),
|
||||
context_mode: z.enum(['group', 'isolated']).default('group').describe('group=runs with chat history and memory, isolated=fresh session (include context in prompt)'),
|
||||
target_group_jid: z.string().optional().describe('(Main group only) JID of the group to schedule the task for. Defaults to the current group.'),
|
||||
script: z.string().optional().describe('Optional bash script to run before waking the agent. Script must output JSON on the last line of stdout: { "wakeAgent": boolean, "data"?: any }. If wakeAgent is false, the agent is not called. Test your script with bash -c "..." before scheduling.'),
|
||||
},
|
||||
async (args) => {
|
||||
// Validate schedule_value before writing IPC
|
||||
@@ -136,6 +137,7 @@ SCHEDULE VALUE FORMAT (all times are LOCAL timezone):
|
||||
type: 'schedule_task',
|
||||
taskId,
|
||||
prompt: args.prompt,
|
||||
script: args.script || undefined,
|
||||
schedule_type: args.schedule_type,
|
||||
schedule_value: args.schedule_value,
|
||||
context_mode: args.context_mode || 'group',
|
||||
@@ -255,6 +257,7 @@ server.tool(
|
||||
prompt: z.string().optional().describe('New prompt for the task'),
|
||||
schedule_type: z.enum(['cron', 'interval', 'once']).optional().describe('New schedule type'),
|
||||
schedule_value: z.string().optional().describe('New schedule value (see schedule_task for format)'),
|
||||
script: z.string().optional().describe('New script for the task. Set to empty string to remove the script.'),
|
||||
},
|
||||
async (args) => {
|
||||
// Validate schedule_value if provided
|
||||
@@ -288,6 +291,7 @@ server.tool(
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
if (args.prompt !== undefined) data.prompt = args.prompt;
|
||||
if (args.script !== undefined) data.script = args.script;
|
||||
if (args.schedule_type !== undefined) data.schedule_type = args.schedule_type;
|
||||
if (args.schedule_value !== undefined) data.schedule_value = args.schedule_value;
|
||||
|
||||
|
||||
@@ -262,3 +262,42 @@ When scheduling tasks for other groups, use the `target_group_jid` parameter wit
|
||||
- `schedule_task(prompt: "...", schedule_type: "cron", schedule_value: "0 9 * * 1", target_group_jid: "120363336345536173@g.us")`
|
||||
|
||||
The task will run in that group's context with access to their files and memory.
|
||||
|
||||
---
|
||||
|
||||
## Task Scripts
|
||||
|
||||
When scheduling tasks that check a condition before acting (new PRs, website changes, API status), use the `script` parameter. The script runs first — if there's nothing to do, you don't wake up.
|
||||
|
||||
### How it works
|
||||
|
||||
1. You provide a bash `script` alongside the `prompt` when scheduling
|
||||
2. When the task fires, the script runs first (30-second timeout)
|
||||
3. Script prints JSON to stdout: `{ "wakeAgent": true/false, "data": {...} }`
|
||||
4. If `wakeAgent: false` — nothing happens, task waits for next run
|
||||
5. If `wakeAgent: true` — you wake up and receive the script's data + prompt
|
||||
|
||||
### Always test your script first
|
||||
|
||||
Before scheduling, run the script in your sandbox to verify it works:
|
||||
|
||||
```bash
|
||||
bash -c 'node --input-type=module -e "
|
||||
const r = await fetch(\"https://api.github.com/repos/owner/repo/pulls?state=open\");
|
||||
const prs = await r.json();
|
||||
console.log(JSON.stringify({ wakeAgent: prs.length > 0, data: prs.slice(0, 5) }));
|
||||
"'
|
||||
```
|
||||
|
||||
### When NOT to use scripts
|
||||
|
||||
If a task requires your judgment every time (daily briefings, reminders, reports), skip the script — just use a regular prompt.
|
||||
|
||||
### Frequent task guidance
|
||||
|
||||
If a user wants tasks running more than ~2x daily and a script can't reduce agent wake-ups:
|
||||
|
||||
- Explain that each wake-up uses API credits and risks rate limits
|
||||
- Suggest restructuring with a script that checks the condition first
|
||||
- If the user needs an LLM to evaluate data, suggest using an API key with direct Anthropic API calls inside the script
|
||||
- Help the user find the minimum viable frequency
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "nanoclaw",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.29",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "nanoclaw",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.29",
|
||||
"dependencies": {
|
||||
"@onecli-sh/sdk": "^0.2.0",
|
||||
"better-sqlite3": "11.10.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "nanoclaw",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.29",
|
||||
"description": "Personal Claude assistant. Lightweight, secure, customizable.",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="97" height="20" role="img" aria-label="40.1k tokens, 20% of context window">
|
||||
<title>40.1k tokens, 20% of context window</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="97" height="20" role="img" aria-label="41.0k tokens, 20% of context window">
|
||||
<title>41.0k tokens, 20% of context window</title>
|
||||
<linearGradient id="s" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
@@ -15,8 +15,8 @@
|
||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
|
||||
<text aria-hidden="true" x="26" y="15" fill="#010101" fill-opacity=".3">tokens</text>
|
||||
<text x="26" y="14">tokens</text>
|
||||
<text aria-hidden="true" x="74" y="15" fill="#010101" fill-opacity=".3">40.1k</text>
|
||||
<text x="74" y="14">40.1k</text>
|
||||
<text aria-hidden="true" x="74" y="15" fill="#010101" fill-opacity=".3">41.0k</text>
|
||||
<text x="74" y="14">41.0k</text>
|
||||
</g>
|
||||
</g>
|
||||
</a>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -42,6 +42,7 @@ export interface ContainerInput {
|
||||
isMain: boolean;
|
||||
isScheduledTask?: boolean;
|
||||
assistantName?: string;
|
||||
script?: string;
|
||||
}
|
||||
|
||||
export interface ContainerOutput {
|
||||
@@ -191,9 +192,18 @@ function buildVolumeMounts(
|
||||
group.folder,
|
||||
'agent-runner-src',
|
||||
);
|
||||
if (!fs.existsSync(groupAgentRunnerDir) && fs.existsSync(agentRunnerSrc)) {
|
||||
if (fs.existsSync(agentRunnerSrc)) {
|
||||
const srcIndex = path.join(agentRunnerSrc, 'index.ts');
|
||||
const cachedIndex = path.join(groupAgentRunnerDir, 'index.ts');
|
||||
const needsCopy =
|
||||
!fs.existsSync(groupAgentRunnerDir) ||
|
||||
!fs.existsSync(cachedIndex) ||
|
||||
(fs.existsSync(srcIndex) &&
|
||||
fs.statSync(srcIndex).mtimeMs > fs.statSync(cachedIndex).mtimeMs);
|
||||
if (needsCopy) {
|
||||
fs.cpSync(agentRunnerSrc, groupAgentRunnerDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
mounts.push({
|
||||
hostPath: groupAgentRunnerDir,
|
||||
containerPath: '/app/src',
|
||||
@@ -667,6 +677,7 @@ export function writeTasksSnapshot(
|
||||
id: string;
|
||||
groupFolder: string;
|
||||
prompt: string;
|
||||
script?: string | null;
|
||||
schedule_type: string;
|
||||
schedule_value: string;
|
||||
status: string;
|
||||
|
||||
23
src/db.ts
23
src/db.ts
@@ -93,6 +93,13 @@ function createSchema(database: Database.Database): void {
|
||||
/* column already exists */
|
||||
}
|
||||
|
||||
// Add script column if it doesn't exist (migration for existing DBs)
|
||||
try {
|
||||
database.exec(`ALTER TABLE scheduled_tasks ADD COLUMN script TEXT`);
|
||||
} catch {
|
||||
/* column already exists */
|
||||
}
|
||||
|
||||
// Add is_bot_message column if it doesn't exist (migration for existing DBs)
|
||||
try {
|
||||
database.exec(
|
||||
@@ -368,14 +375,15 @@ export function createTask(
|
||||
): void {
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO scheduled_tasks (id, group_folder, chat_jid, prompt, schedule_type, schedule_value, context_mode, next_run, status, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO scheduled_tasks (id, group_folder, chat_jid, prompt, script, schedule_type, schedule_value, context_mode, next_run, status, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
).run(
|
||||
task.id,
|
||||
task.group_folder,
|
||||
task.chat_jid,
|
||||
task.prompt,
|
||||
task.script || null,
|
||||
task.schedule_type,
|
||||
task.schedule_value,
|
||||
task.context_mode || 'isolated',
|
||||
@@ -410,7 +418,12 @@ export function updateTask(
|
||||
updates: Partial<
|
||||
Pick<
|
||||
ScheduledTask,
|
||||
'prompt' | 'schedule_type' | 'schedule_value' | 'next_run' | 'status'
|
||||
| 'prompt'
|
||||
| 'script'
|
||||
| 'schedule_type'
|
||||
| 'schedule_value'
|
||||
| 'next_run'
|
||||
| 'status'
|
||||
>
|
||||
>,
|
||||
): void {
|
||||
@@ -421,6 +434,10 @@ export function updateTask(
|
||||
fields.push('prompt = ?');
|
||||
values.push(updates.prompt);
|
||||
}
|
||||
if (updates.script !== undefined) {
|
||||
fields.push('script = ?');
|
||||
values.push(updates.script || null);
|
||||
}
|
||||
if (updates.schedule_type !== undefined) {
|
||||
fields.push('schedule_type = ?');
|
||||
values.push(updates.schedule_type);
|
||||
|
||||
@@ -162,6 +162,7 @@ export async function processTaskIpc(
|
||||
schedule_type?: string;
|
||||
schedule_value?: string;
|
||||
context_mode?: string;
|
||||
script?: string;
|
||||
groupFolder?: string;
|
||||
chatJid?: string;
|
||||
targetJid?: string;
|
||||
@@ -260,6 +261,7 @@ export async function processTaskIpc(
|
||||
group_folder: targetFolder,
|
||||
chat_jid: targetJid,
|
||||
prompt: data.prompt,
|
||||
script: data.script || null,
|
||||
schedule_type: scheduleType,
|
||||
schedule_value: data.schedule_value,
|
||||
context_mode: contextMode,
|
||||
@@ -352,6 +354,7 @@ export async function processTaskIpc(
|
||||
|
||||
const updates: Parameters<typeof updateTask>[1] = {};
|
||||
if (data.prompt !== undefined) updates.prompt = data.prompt;
|
||||
if (data.script !== undefined) updates.script = data.script || null;
|
||||
if (data.schedule_type !== undefined)
|
||||
updates.schedule_type = data.schedule_type as
|
||||
| 'cron'
|
||||
|
||||
@@ -139,6 +139,7 @@ async function runTask(
|
||||
id: t.id,
|
||||
groupFolder: t.group_folder,
|
||||
prompt: t.prompt,
|
||||
script: t.script,
|
||||
schedule_type: t.schedule_type,
|
||||
schedule_value: t.schedule_value,
|
||||
status: t.status,
|
||||
@@ -179,6 +180,7 @@ async function runTask(
|
||||
isMain,
|
||||
isScheduledTask: true,
|
||||
assistantName: ASSISTANT_NAME,
|
||||
script: task.script || undefined,
|
||||
},
|
||||
(proc, containerName) =>
|
||||
deps.onProcess(task.chat_jid, proc, containerName, task.group_folder),
|
||||
|
||||
@@ -58,6 +58,7 @@ export interface ScheduledTask {
|
||||
group_folder: string;
|
||||
chat_jid: string;
|
||||
prompt: string;
|
||||
script?: string | null;
|
||||
schedule_type: 'cron' | 'interval' | 'once';
|
||||
schedule_value: string;
|
||||
context_mode: 'group' | 'isolated';
|
||||
|
||||
Reference in New Issue
Block a user