Merge branch 'main' into admin_mode_1

This commit is contained in:
gavrielc
2026-03-18 12:10:19 +02:00
committed by GitHub
10 changed files with 56 additions and 11 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "nanoclaw", "name": "nanoclaw",
"version": "1.2.14", "version": "1.2.16",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "nanoclaw", "name": "nanoclaw",
"version": "1.2.14", "version": "1.2.16",
"dependencies": { "dependencies": {
"better-sqlite3": "^11.8.1", "better-sqlite3": "^11.8.1",
"cron-parser": "^5.5.0", "cron-parser": "^5.5.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "nanoclaw", "name": "nanoclaw",
"version": "1.2.14", "version": "1.2.16",
"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.4k 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="40.6k tokens, 20% of context window">
<title>40.4k tokens, 20% of context window</title> <title>40.6k 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.4k</text> <text aria-hidden="true" x="74" y="15" fill="#010101" fill-opacity=".3">40.6k</text>
<text x="74" y="14">40.4k</text> <text x="74" y="14">40.6k</text>
</g> </g>
</g> </g>
</a> </a>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -62,6 +62,7 @@ ExecStart=${nodePath} ${projectRoot}/dist/index.js
WorkingDirectory=${projectRoot} WorkingDirectory=${projectRoot}
Restart=always Restart=always
RestartSec=5 RestartSec=5
KillMode=process
Environment=HOME=${homeDir} Environment=HOME=${homeDir}
Environment=PATH=/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin Environment=PATH=/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin
StandardOutput=append:${projectRoot}/logs/nanoclaw.log StandardOutput=append:${projectRoot}/logs/nanoclaw.log
@@ -142,6 +143,16 @@ describe('systemd unit generation', () => {
expect(unit).toContain('RestartSec=5'); 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', () => { it('sets correct ExecStart', () => {
const unit = generateSystemdUnit( const unit = generateSystemdUnit(
'/usr/bin/node', '/usr/bin/node',

View File

@@ -243,6 +243,7 @@ ExecStart=${nodePath} ${projectRoot}/dist/index.js
WorkingDirectory=${projectRoot} WorkingDirectory=${projectRoot}
Restart=always Restart=always
RestartSec=5 RestartSec=5
KillMode=process
Environment=HOME=${homeDir} Environment=HOME=${homeDir}
Environment=PATH=/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin Environment=PATH=/usr/local/bin:/usr/bin:/bin:${homeDir}/.local/bin
StandardOutput=append:${projectRoot}/logs/nanoclaw.log StandardOutput=append:${projectRoot}/logs/nanoclaw.log

View File

@@ -632,6 +632,21 @@ async function main(): Promise<void> {
getAvailableGroups, getAvailableGroups,
writeGroupsSnapshot: (gf, im, ag, rj) => writeGroupsSnapshot: (gf, im, ag, rj) =>
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); queue.setProcessMessagesFn(processGroupMessages);
recoverPendingMessages(); recoverPendingMessages();

View File

@@ -62,6 +62,7 @@ beforeEach(() => {
syncGroups: async () => {}, syncGroups: async () => {},
getAvailableGroups: () => [], getAvailableGroups: () => [],
writeGroupsSnapshot: () => {}, writeGroupsSnapshot: () => {},
onTasksChanged: () => {},
}; };
}); });

View File

@@ -22,6 +22,7 @@ export interface IpcDeps {
availableGroups: AvailableGroup[], availableGroups: AvailableGroup[],
registeredJids: Set<string>, registeredJids: Set<string>,
) => void; ) => void;
onTasksChanged: () => void;
} }
let ipcWatcherRunning = false; let ipcWatcherRunning = false;
@@ -270,6 +271,7 @@ export async function processTaskIpc(
{ taskId, sourceGroup, targetFolder, contextMode }, { taskId, sourceGroup, targetFolder, contextMode },
'Task created via IPC', 'Task created via IPC',
); );
deps.onTasksChanged();
} }
break; break;
@@ -282,6 +284,7 @@ export async function processTaskIpc(
{ taskId: data.taskId, sourceGroup }, { taskId: data.taskId, sourceGroup },
'Task paused via IPC', 'Task paused via IPC',
); );
deps.onTasksChanged();
} else { } else {
logger.warn( logger.warn(
{ taskId: data.taskId, sourceGroup }, { taskId: data.taskId, sourceGroup },
@@ -300,6 +303,7 @@ export async function processTaskIpc(
{ taskId: data.taskId, sourceGroup }, { taskId: data.taskId, sourceGroup },
'Task resumed via IPC', 'Task resumed via IPC',
); );
deps.onTasksChanged();
} else { } else {
logger.warn( logger.warn(
{ taskId: data.taskId, sourceGroup }, { taskId: data.taskId, sourceGroup },
@@ -318,6 +322,7 @@ export async function processTaskIpc(
{ taskId: data.taskId, sourceGroup }, { taskId: data.taskId, sourceGroup },
'Task cancelled via IPC', 'Task cancelled via IPC',
); );
deps.onTasksChanged();
} else { } else {
logger.warn( logger.warn(
{ taskId: data.taskId, sourceGroup }, { taskId: data.taskId, sourceGroup },
@@ -388,6 +393,7 @@ export async function processTaskIpc(
{ taskId: data.taskId, sourceGroup, updates }, { taskId: data.taskId, sourceGroup, updates },
'Task updated via IPC', 'Task updated via IPC',
); );
deps.onTasksChanged();
} }
break; break;

View File

@@ -24,7 +24,12 @@ import {
// --- Helpers --- // --- Helpers ---
function createMockProcess(pid = 12345) { 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', () => { describe('remote-control', () => {
@@ -108,8 +113,8 @@ describe('remote-control', () => {
const spawnCall = spawnMock.mock.calls[0]; const spawnCall = spawnMock.mock.calls[0];
const options = spawnCall[2]; const options = spawnCall[2];
// stdio should use file descriptors (numbers), not 'pipe' // stdio[0] is 'pipe' so we can write 'y' to accept the prompt
expect(options.stdio[0]).toBe('ignore'); expect(options.stdio[0]).toBe('pipe');
expect(typeof options.stdio[1]).toBe('number'); expect(typeof options.stdio[1]).toBe('number');
expect(typeof options.stdio[2]).toBe('number'); expect(typeof options.stdio[2]).toBe('number');
}); });

View File

@@ -111,7 +111,7 @@ export async function startRemoteControl(
try { try {
proc = spawn('claude', ['remote-control', '--name', 'NanoClaw Remote'], { proc = spawn('claude', ['remote-control', '--name', 'NanoClaw Remote'], {
cwd, cwd,
stdio: ['ignore', stdoutFd, stderrFd], stdio: ['pipe', stdoutFd, stderrFd],
detached: true, detached: true,
}); });
} catch (err: any) { } catch (err: any) {
@@ -120,6 +120,12 @@ export async function startRemoteControl(
return { ok: false, error: `Failed to start: ${err.message}` }; 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 // Close FDs in the parent — the child inherited copies
fs.closeSync(stdoutFd); fs.closeSync(stdoutFd);
fs.closeSync(stderrFd); fs.closeSync(stderrFd);