Merge branch 'main' into fix/ipc-register-group-claude-md

This commit is contained in:
gavrielc
2026-03-25 17:36:01 +02:00
committed by GitHub
15 changed files with 402 additions and 20 deletions

View File

@@ -287,4 +287,5 @@ Tell user to test: send a message in their registered chat. Show: `tail -f logs/
## 9. Diagnostics ## 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.

View File

@@ -237,4 +237,5 @@ Tell the user:
## Diagnostics ## 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.

View File

@@ -16,6 +16,7 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { execFile } from 'child_process';
import { query, HookCallback, PreCompactHookInput } from '@anthropic-ai/claude-agent-sdk'; import { query, HookCallback, PreCompactHookInput } from '@anthropic-ai/claude-agent-sdk';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
@@ -27,6 +28,7 @@ interface ContainerInput {
isMain: boolean; isMain: boolean;
isScheduledTask?: boolean; isScheduledTask?: boolean;
assistantName?: string; assistantName?: string;
script?: string;
} }
interface ContainerOutput { interface ContainerOutput {
@@ -464,6 +466,55 @@ async function runQuery(
return { newSessionId, lastAssistantUuid, closedDuringQuery }; 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> { async function main(): Promise<void> {
let containerInput: ContainerInput; let containerInput: ContainerInput;
@@ -505,6 +556,26 @@ async function main(): Promise<void> {
prompt += '\n' + pending.join('\n'); 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 // Query loop: run query → wait for IPC message → run new query → repeat
let resumeAt: string | undefined; let resumeAt: string | undefined;
try { try {

View File

@@ -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!)'), 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)'), 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.'), 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) => { async (args) => {
// Validate schedule_value before writing IPC // Validate schedule_value before writing IPC
@@ -136,6 +137,7 @@ SCHEDULE VALUE FORMAT (all times are LOCAL timezone):
type: 'schedule_task', type: 'schedule_task',
taskId, taskId,
prompt: args.prompt, prompt: args.prompt,
script: args.script || undefined,
schedule_type: args.schedule_type, schedule_type: args.schedule_type,
schedule_value: args.schedule_value, schedule_value: args.schedule_value,
context_mode: args.context_mode || 'group', context_mode: args.context_mode || 'group',
@@ -255,6 +257,7 @@ server.tool(
prompt: z.string().optional().describe('New prompt for the task'), prompt: z.string().optional().describe('New prompt for the task'),
schedule_type: z.enum(['cron', 'interval', 'once']).optional().describe('New schedule type'), 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)'), 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) => { async (args) => {
// Validate schedule_value if provided // Validate schedule_value if provided
@@ -288,6 +291,7 @@ server.tool(
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}; };
if (args.prompt !== undefined) data.prompt = args.prompt; 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_type !== undefined) data.schedule_type = args.schedule_type;
if (args.schedule_value !== undefined) data.schedule_value = args.schedule_value; if (args.schedule_value !== undefined) data.schedule_value = args.schedule_value;

View File

@@ -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")` - `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. 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
View File

@@ -1,12 +1,12 @@
{ {
"name": "nanoclaw", "name": "nanoclaw",
"version": "1.2.27", "version": "1.2.29",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "nanoclaw", "name": "nanoclaw",
"version": "1.2.27", "version": "1.2.29",
"dependencies": { "dependencies": {
"@onecli-sh/sdk": "^0.2.0", "@onecli-sh/sdk": "^0.2.0",
"better-sqlite3": "11.10.0", "better-sqlite3": "11.10.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "nanoclaw", "name": "nanoclaw",
"version": "1.2.27", "version": "1.2.29",
"description": "Personal Claude assistant. Lightweight, secure, customizable.", "description": "Personal Claude assistant. Lightweight, secure, customizable.",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",

View File

@@ -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"> <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>40.1k tokens, 20% of context window</title> <title>41.0k tokens, 20% of context window</title>
<linearGradient id="s" x2="0" y2="100%"> <linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/> <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" 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"> <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 aria-hidden="true" x="26" y="15" fill="#010101" fill-opacity=".3">tokens</text>
<text x="26" y="14">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 aria-hidden="true" x="74" y="15" fill="#010101" fill-opacity=".3">41.0k</text>
<text x="74" y="14">40.1k</text> <text x="74" y="14">41.0k</text>
</g> </g>
</g> </g>
</a> </a>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,4 +1,7 @@
import { describe, it, expect, beforeEach } from 'vitest'; import fs from 'fs';
import os from 'os';
import path from 'path';
import { afterEach, describe, it, expect, beforeEach } from 'vitest';
import Database from 'better-sqlite3'; import Database from 'better-sqlite3';
@@ -6,7 +9,7 @@ import Database from 'better-sqlite3';
* Tests for the register step. * Tests for the register step.
* *
* Verifies: parameterized SQL (no injection), file templating, * Verifies: parameterized SQL (no injection), file templating,
* apostrophe in names, .env updates. * apostrophe in names, .env updates, CLAUDE.md template copy.
*/ */
function createTestDb(): Database.Database { function createTestDb(): Database.Database {
@@ -255,3 +258,207 @@ describe('file templating', () => {
expect(envContent).toContain('ASSISTANT_NAME="Nova"'); expect(envContent).toContain('ASSISTANT_NAME="Nova"');
}); });
}); });
describe('CLAUDE.md template copy', () => {
let tmpDir: string;
let groupsDir: string;
// Replicates register.ts template copy + name update logic
function simulateRegister(
folder: string,
isMain: boolean,
assistantName = 'Andy',
): void {
const folderDir = path.join(groupsDir, folder);
fs.mkdirSync(path.join(folderDir, 'logs'), { recursive: true });
// Template copy — never overwrite existing (register.ts lines 119-135)
const dest = path.join(folderDir, 'CLAUDE.md');
if (!fs.existsSync(dest)) {
const templatePath = isMain
? path.join(groupsDir, 'main', 'CLAUDE.md')
: path.join(groupsDir, 'global', 'CLAUDE.md');
if (fs.existsSync(templatePath)) {
fs.copyFileSync(templatePath, dest);
}
}
// Name update across all groups (register.ts lines 140-165)
if (assistantName !== 'Andy') {
const mdFiles = fs
.readdirSync(groupsDir)
.map((d) => path.join(groupsDir, d, 'CLAUDE.md'))
.filter((f) => fs.existsSync(f));
for (const mdFile of mdFiles) {
let content = fs.readFileSync(mdFile, 'utf-8');
content = content.replace(/^# Andy$/m, `# ${assistantName}`);
content = content.replace(
/You are Andy/g,
`You are ${assistantName}`,
);
fs.writeFileSync(mdFile, content);
}
}
}
function readGroupMd(folder: string): string {
return fs.readFileSync(
path.join(groupsDir, folder, 'CLAUDE.md'),
'utf-8',
);
}
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nanoclaw-register-test-'));
groupsDir = path.join(tmpDir, 'groups');
fs.mkdirSync(path.join(groupsDir, 'main'), { recursive: true });
fs.mkdirSync(path.join(groupsDir, 'global'), { recursive: true });
fs.writeFileSync(
path.join(groupsDir, 'main', 'CLAUDE.md'),
'# Andy\n\nYou are Andy, a personal assistant.\n\n## Admin Context\n\nThis is the **main channel**.',
);
fs.writeFileSync(
path.join(groupsDir, 'global', 'CLAUDE.md'),
'# Andy\n\nYou are Andy, a personal assistant.',
);
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it('copies global template for non-main group', () => {
simulateRegister('telegram_dev-team', false);
const content = readGroupMd('telegram_dev-team');
expect(content).toContain('You are Andy');
expect(content).not.toContain('Admin Context');
});
it('copies main template for main group', () => {
simulateRegister('whatsapp_main', true);
expect(readGroupMd('whatsapp_main')).toContain('Admin Context');
});
it('each channel can have its own main with admin context', () => {
simulateRegister('whatsapp_main', true);
simulateRegister('telegram_main', true);
simulateRegister('slack_main', true);
simulateRegister('discord_main', true);
for (const folder of [
'whatsapp_main',
'telegram_main',
'slack_main',
'discord_main',
]) {
const content = readGroupMd(folder);
expect(content).toContain('Admin Context');
expect(content).toContain('You are Andy');
}
});
it('non-main groups across channels get global template', () => {
simulateRegister('whatsapp_main', true);
simulateRegister('telegram_friends', false);
simulateRegister('slack_engineering', false);
simulateRegister('discord_general', false);
expect(readGroupMd('whatsapp_main')).toContain('Admin Context');
for (const folder of [
'telegram_friends',
'slack_engineering',
'discord_general',
]) {
const content = readGroupMd(folder);
expect(content).toContain('You are Andy');
expect(content).not.toContain('Admin Context');
}
});
it('custom name propagates to all channels and groups', () => {
// Register multiple channels, last one sets custom name
simulateRegister('whatsapp_main', true);
simulateRegister('telegram_main', true);
simulateRegister('slack_devs', false);
// Final registration triggers name update across all
simulateRegister('discord_main', true, 'Luna');
for (const folder of [
'main',
'global',
'whatsapp_main',
'telegram_main',
'slack_devs',
'discord_main',
]) {
const content = readGroupMd(folder);
expect(content).toContain('# Luna');
expect(content).toContain('You are Luna');
expect(content).not.toContain('Andy');
}
});
it('never overwrites existing CLAUDE.md on re-registration', () => {
simulateRegister('slack_main', true);
// User customizes the file extensively (persona, workspace, rules)
const mdPath = path.join(groupsDir, 'slack_main', 'CLAUDE.md');
fs.writeFileSync(
mdPath,
'# Gambi\n\nCustom persona with workspace rules and family context.',
);
// Re-registering same folder (e.g. re-running /add-slack)
simulateRegister('slack_main', true);
const content = readGroupMd('slack_main');
expect(content).toContain('Custom persona');
expect(content).not.toContain('Admin Context');
});
it('never overwrites when non-main becomes main (isMain changes)', () => {
// User registers a family group as non-main
simulateRegister('whatsapp_casa', false);
// User extensively customizes it (PARA system, task management, etc.)
const mdPath = path.join(groupsDir, 'whatsapp_casa', 'CLAUDE.md');
fs.writeFileSync(
mdPath,
'# Casa\n\nFamily group with PARA system, task management, shopping lists.',
);
// Later, user promotes to main (no trigger required) — CLAUDE.md must be preserved
simulateRegister('whatsapp_casa', true);
const content = readGroupMd('whatsapp_casa');
expect(content).toContain('PARA system');
expect(content).not.toContain('Admin Context');
});
it('preserves custom CLAUDE.md across channels when changing main', () => {
// Real-world scenario: WhatsApp main + customized Discord research channel
simulateRegister('whatsapp_main', true);
simulateRegister('discord_main', false);
const discordPath = path.join(groupsDir, 'discord_main', 'CLAUDE.md');
fs.writeFileSync(
discordPath,
'# Gambi HQ — Research Assistant\n\nResearch workflows for Laura and Ethan.',
);
// Discord becomes main too — custom content must survive
simulateRegister('discord_main', true);
expect(readGroupMd('discord_main')).toContain('Research Assistant');
// WhatsApp main also untouched
expect(readGroupMd('whatsapp_main')).toContain('Admin Context');
});
it('handles missing templates gracefully', () => {
fs.unlinkSync(path.join(groupsDir, 'global', 'CLAUDE.md'));
fs.unlinkSync(path.join(groupsDir, 'main', 'CLAUDE.md'));
simulateRegister('discord_general', false);
expect(
fs.existsSync(path.join(groupsDir, 'discord_general', 'CLAUDE.md')),
).toBe(false);
});
});

View File

@@ -116,6 +116,30 @@ export async function run(args: string[]): Promise<void> {
recursive: true, recursive: true,
}); });
// Create CLAUDE.md in the new group folder from template if it doesn't exist.
// The agent runs with CWD=/workspace/group and loads CLAUDE.md from there.
// Never overwrite an existing CLAUDE.md — users customize these extensively
// (persona, workspace structure, communication rules, family context, etc.)
// and a stock template replacement would destroy that work.
const groupClaudeMdPath = path.join(
projectRoot,
'groups',
parsed.folder,
'CLAUDE.md',
);
if (!fs.existsSync(groupClaudeMdPath)) {
const templatePath = parsed.isMain
? path.join(projectRoot, 'groups', 'main', 'CLAUDE.md')
: path.join(projectRoot, 'groups', 'global', 'CLAUDE.md');
if (fs.existsSync(templatePath)) {
fs.copyFileSync(templatePath, groupClaudeMdPath);
logger.info(
{ file: groupClaudeMdPath, template: templatePath },
'Created CLAUDE.md from template',
);
}
}
// Update assistant name in CLAUDE.md files if different from default // Update assistant name in CLAUDE.md files if different from default
let nameUpdated = false; let nameUpdated = false;
if (parsed.assistantName !== 'Andy') { if (parsed.assistantName !== 'Andy') {
@@ -124,10 +148,11 @@ export async function run(args: string[]): Promise<void> {
'Updating assistant name', 'Updating assistant name',
); );
const mdFiles = [ const groupsDir = path.join(projectRoot, 'groups');
path.join(projectRoot, 'groups', 'global', 'CLAUDE.md'), const mdFiles = fs
path.join(projectRoot, 'groups', parsed.folder, 'CLAUDE.md'), .readdirSync(groupsDir)
]; .map((d) => path.join(groupsDir, d, 'CLAUDE.md'))
.filter((f) => fs.existsSync(f));
for (const mdFile of mdFiles) { for (const mdFile of mdFiles) {
if (fs.existsSync(mdFile)) { if (fs.existsSync(mdFile)) {

View File

@@ -42,6 +42,7 @@ export interface ContainerInput {
isMain: boolean; isMain: boolean;
isScheduledTask?: boolean; isScheduledTask?: boolean;
assistantName?: string; assistantName?: string;
script?: string;
} }
export interface ContainerOutput { export interface ContainerOutput {
@@ -191,8 +192,17 @@ function buildVolumeMounts(
group.folder, group.folder,
'agent-runner-src', 'agent-runner-src',
); );
if (!fs.existsSync(groupAgentRunnerDir) && fs.existsSync(agentRunnerSrc)) { if (fs.existsSync(agentRunnerSrc)) {
fs.cpSync(agentRunnerSrc, groupAgentRunnerDir, { recursive: true }); 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({ mounts.push({
hostPath: groupAgentRunnerDir, hostPath: groupAgentRunnerDir,
@@ -667,6 +677,7 @@ export function writeTasksSnapshot(
id: string; id: string;
groupFolder: string; groupFolder: string;
prompt: string; prompt: string;
script?: string | null;
schedule_type: string; schedule_type: string;
schedule_value: string; schedule_value: string;
status: string; status: string;

View File

@@ -93,6 +93,13 @@ function createSchema(database: Database.Database): void {
/* column already exists */ /* 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) // Add is_bot_message column if it doesn't exist (migration for existing DBs)
try { try {
database.exec( database.exec(
@@ -368,14 +375,15 @@ export function createTask(
): void { ): void {
db.prepare( db.prepare(
` `
INSERT INTO scheduled_tasks (id, group_folder, chat_jid, prompt, schedule_type, schedule_value, context_mode, next_run, status, created_at) INSERT INTO scheduled_tasks (id, group_folder, chat_jid, prompt, script, schedule_type, schedule_value, context_mode, next_run, status, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, `,
).run( ).run(
task.id, task.id,
task.group_folder, task.group_folder,
task.chat_jid, task.chat_jid,
task.prompt, task.prompt,
task.script || null,
task.schedule_type, task.schedule_type,
task.schedule_value, task.schedule_value,
task.context_mode || 'isolated', task.context_mode || 'isolated',
@@ -410,7 +418,12 @@ export function updateTask(
updates: Partial< updates: Partial<
Pick< Pick<
ScheduledTask, ScheduledTask,
'prompt' | 'schedule_type' | 'schedule_value' | 'next_run' | 'status' | 'prompt'
| 'script'
| 'schedule_type'
| 'schedule_value'
| 'next_run'
| 'status'
> >
>, >,
): void { ): void {
@@ -421,6 +434,10 @@ export function updateTask(
fields.push('prompt = ?'); fields.push('prompt = ?');
values.push(updates.prompt); values.push(updates.prompt);
} }
if (updates.script !== undefined) {
fields.push('script = ?');
values.push(updates.script || null);
}
if (updates.schedule_type !== undefined) { if (updates.schedule_type !== undefined) {
fields.push('schedule_type = ?'); fields.push('schedule_type = ?');
values.push(updates.schedule_type); values.push(updates.schedule_type);

View File

@@ -162,6 +162,7 @@ export async function processTaskIpc(
schedule_type?: string; schedule_type?: string;
schedule_value?: string; schedule_value?: string;
context_mode?: string; context_mode?: string;
script?: string;
groupFolder?: string; groupFolder?: string;
chatJid?: string; chatJid?: string;
targetJid?: string; targetJid?: string;
@@ -260,6 +261,7 @@ export async function processTaskIpc(
group_folder: targetFolder, group_folder: targetFolder,
chat_jid: targetJid, chat_jid: targetJid,
prompt: data.prompt, prompt: data.prompt,
script: data.script || null,
schedule_type: scheduleType, schedule_type: scheduleType,
schedule_value: data.schedule_value, schedule_value: data.schedule_value,
context_mode: contextMode, context_mode: contextMode,
@@ -352,6 +354,7 @@ export async function processTaskIpc(
const updates: Parameters<typeof updateTask>[1] = {}; const updates: Parameters<typeof updateTask>[1] = {};
if (data.prompt !== undefined) updates.prompt = data.prompt; if (data.prompt !== undefined) updates.prompt = data.prompt;
if (data.script !== undefined) updates.script = data.script || null;
if (data.schedule_type !== undefined) if (data.schedule_type !== undefined)
updates.schedule_type = data.schedule_type as updates.schedule_type = data.schedule_type as
| 'cron' | 'cron'

View File

@@ -139,6 +139,7 @@ async function runTask(
id: t.id, id: t.id,
groupFolder: t.group_folder, groupFolder: t.group_folder,
prompt: t.prompt, prompt: t.prompt,
script: t.script,
schedule_type: t.schedule_type, schedule_type: t.schedule_type,
schedule_value: t.schedule_value, schedule_value: t.schedule_value,
status: t.status, status: t.status,
@@ -179,6 +180,7 @@ async function runTask(
isMain, isMain,
isScheduledTask: true, isScheduledTask: true,
assistantName: ASSISTANT_NAME, assistantName: ASSISTANT_NAME,
script: task.script || undefined,
}, },
(proc, containerName) => (proc, containerName) =>
deps.onProcess(task.chat_jid, proc, containerName, task.group_folder), deps.onProcess(task.chat_jid, proc, containerName, task.group_folder),

View File

@@ -58,6 +58,7 @@ export interface ScheduledTask {
group_folder: string; group_folder: string;
chat_jid: string; chat_jid: string;
prompt: string; prompt: string;
script?: string | null;
schedule_type: 'cron' | 'interval' | 'once'; schedule_type: 'cron' | 'interval' | 'once';
schedule_value: string; schedule_value: string;
context_mode: 'group' | 'isolated'; context_mode: 'group' | 'isolated';