diff --git a/package-lock.json b/package-lock.json index 04b06a2..ad5f762 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nanoclaw", - "version": "1.2.14", + "version": "1.2.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nanoclaw", - "version": "1.2.14", + "version": "1.2.16", "dependencies": { "better-sqlite3": "^11.8.1", "cron-parser": "^5.5.0", diff --git a/package.json b/package.json index 76d9bfa..44312f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nanoclaw", - "version": "1.2.14", + "version": "1.2.16", "description": "Personal Claude assistant. Lightweight, secure, customizable.", "type": "module", "main": "dist/index.js", diff --git a/repo-tokens/badge.svg b/repo-tokens/badge.svg index 4fd94b8..ce35723 100644 --- a/repo-tokens/badge.svg +++ b/repo-tokens/badge.svg @@ -1,5 +1,5 @@ - - 40.4k tokens, 20% of context window + + 40.6k tokens, 20% of context window @@ -15,8 +15,8 @@ tokens - - 40.4k + + 40.6k diff --git a/setup/service.test.ts b/setup/service.test.ts index eb15db8..9168fe1 100644 --- a/setup/service.test.ts +++ b/setup/service.test.ts @@ -62,6 +62,7 @@ ExecStart=${nodePath} ${projectRoot}/dist/index.js WorkingDirectory=${projectRoot} Restart=always RestartSec=5 +KillMode=process Environment=HOME=${homeDir} Environment=PATH=/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin StandardOutput=append:${projectRoot}/logs/nanoclaw.log @@ -142,6 +143,16 @@ describe('systemd unit generation', () => { expect(unit).toContain('RestartSec=5'); }); + it('uses KillMode=process to preserve detached children', () => { + const unit = generateSystemdUnit( + '/usr/bin/node', + '/home/user/nanoclaw', + '/home/user', + false, + ); + expect(unit).toContain('KillMode=process'); + }); + it('sets correct ExecStart', () => { const unit = generateSystemdUnit( '/usr/bin/node', diff --git a/setup/service.ts b/setup/service.ts index 643c8c9..71b3c63 100644 --- a/setup/service.ts +++ b/setup/service.ts @@ -243,6 +243,7 @@ ExecStart=${nodePath} ${projectRoot}/dist/index.js WorkingDirectory=${projectRoot} Restart=always RestartSec=5 +KillMode=process Environment=HOME=${homeDir} Environment=PATH=/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin StandardOutput=append:${projectRoot}/logs/nanoclaw.log diff --git a/src/index.ts b/src/index.ts index 504400d..98682fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -632,6 +632,21 @@ async function main(): Promise { getAvailableGroups, writeGroupsSnapshot: (gf, im, ag, rj) => writeGroupsSnapshot(gf, im, ag, rj), + onTasksChanged: () => { + const tasks = getAllTasks(); + const taskRows = tasks.map((t) => ({ + id: t.id, + groupFolder: t.group_folder, + prompt: t.prompt, + schedule_type: t.schedule_type, + schedule_value: t.schedule_value, + status: t.status, + next_run: t.next_run, + })); + for (const group of Object.values(registeredGroups)) { + writeTasksSnapshot(group.folder, group.isMain === true, taskRows); + } + }, }); queue.setProcessMessagesFn(processGroupMessages); recoverPendingMessages(); diff --git a/src/ipc-auth.test.ts b/src/ipc-auth.test.ts index 1aa681e..0adf899 100644 --- a/src/ipc-auth.test.ts +++ b/src/ipc-auth.test.ts @@ -62,6 +62,7 @@ beforeEach(() => { syncGroups: async () => {}, getAvailableGroups: () => [], writeGroupsSnapshot: () => {}, + onTasksChanged: () => {}, }; }); diff --git a/src/ipc.ts b/src/ipc.ts index 7a972c0..48efeb5 100644 --- a/src/ipc.ts +++ b/src/ipc.ts @@ -22,6 +22,7 @@ export interface IpcDeps { availableGroups: AvailableGroup[], registeredJids: Set, ) => void; + onTasksChanged: () => void; } let ipcWatcherRunning = false; @@ -270,6 +271,7 @@ export async function processTaskIpc( { taskId, sourceGroup, targetFolder, contextMode }, 'Task created via IPC', ); + deps.onTasksChanged(); } break; @@ -282,6 +284,7 @@ export async function processTaskIpc( { taskId: data.taskId, sourceGroup }, 'Task paused via IPC', ); + deps.onTasksChanged(); } else { logger.warn( { taskId: data.taskId, sourceGroup }, @@ -300,6 +303,7 @@ export async function processTaskIpc( { taskId: data.taskId, sourceGroup }, 'Task resumed via IPC', ); + deps.onTasksChanged(); } else { logger.warn( { taskId: data.taskId, sourceGroup }, @@ -318,6 +322,7 @@ export async function processTaskIpc( { taskId: data.taskId, sourceGroup }, 'Task cancelled via IPC', ); + deps.onTasksChanged(); } else { logger.warn( { taskId: data.taskId, sourceGroup }, @@ -388,6 +393,7 @@ export async function processTaskIpc( { taskId: data.taskId, sourceGroup, updates }, 'Task updated via IPC', ); + deps.onTasksChanged(); } break; diff --git a/src/remote-control.test.ts b/src/remote-control.test.ts index 1fa434b..24e1b11 100644 --- a/src/remote-control.test.ts +++ b/src/remote-control.test.ts @@ -24,7 +24,12 @@ import { // --- Helpers --- function createMockProcess(pid = 12345) { - return { pid, unref: vi.fn(), kill: vi.fn() }; + return { + pid, + unref: vi.fn(), + kill: vi.fn(), + stdin: { write: vi.fn(), end: vi.fn() }, + }; } describe('remote-control', () => { @@ -108,8 +113,8 @@ describe('remote-control', () => { const spawnCall = spawnMock.mock.calls[0]; const options = spawnCall[2]; - // stdio should use file descriptors (numbers), not 'pipe' - expect(options.stdio[0]).toBe('ignore'); + // stdio[0] is 'pipe' so we can write 'y' to accept the prompt + expect(options.stdio[0]).toBe('pipe'); expect(typeof options.stdio[1]).toBe('number'); expect(typeof options.stdio[2]).toBe('number'); }); diff --git a/src/remote-control.ts b/src/remote-control.ts index 015aa7f..2f0bdc4 100644 --- a/src/remote-control.ts +++ b/src/remote-control.ts @@ -111,7 +111,7 @@ export async function startRemoteControl( try { proc = spawn('claude', ['remote-control', '--name', 'NanoClaw Remote'], { cwd, - stdio: ['ignore', stdoutFd, stderrFd], + stdio: ['pipe', stdoutFd, stderrFd], detached: true, }); } catch (err: any) { @@ -120,6 +120,12 @@ export async function startRemoteControl( return { ok: false, error: `Failed to start: ${err.message}` }; } + // Auto-accept the "Enable Remote Control?" prompt + if (proc.stdin) { + proc.stdin.write('y\n'); + proc.stdin.end(); + } + // Close FDs in the parent — the child inherited copies fs.closeSync(stdoutFd); fs.closeSync(stderrFd);