Merge branch 'main' into admin_mode_1
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 |
@@ -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',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
15
src/index.ts
15
src/index.ts
@@ -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();
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ beforeEach(() => {
|
|||||||
syncGroups: async () => {},
|
syncGroups: async () => {},
|
||||||
getAvailableGroups: () => [],
|
getAvailableGroups: () => [],
|
||||||
writeGroupsSnapshot: () => {},
|
writeGroupsSnapshot: () => {},
|
||||||
|
onTasksChanged: () => {},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user