@rozek/nanoclaw 1.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.json +1 -0
- package/.claude/skills/add-compact/SKILL.md +135 -0
- package/.claude/skills/add-discord/SKILL.md +203 -0
- package/.claude/skills/add-gmail/SKILL.md +220 -0
- package/.claude/skills/add-image-vision/SKILL.md +94 -0
- package/.claude/skills/add-ollama-tool/SKILL.md +153 -0
- package/.claude/skills/add-parallel/SKILL.md +290 -0
- package/.claude/skills/add-pdf-reader/SKILL.md +104 -0
- package/.claude/skills/add-reactions/SKILL.md +117 -0
- package/.claude/skills/add-slack/SKILL.md +207 -0
- package/.claude/skills/add-telegram/SKILL.md +222 -0
- package/.claude/skills/add-telegram-swarm/SKILL.md +384 -0
- package/.claude/skills/add-voice-transcription/SKILL.md +148 -0
- package/.claude/skills/add-whatsapp/SKILL.md +372 -0
- package/.claude/skills/convert-to-apple-container/SKILL.md +175 -0
- package/.claude/skills/customize/SKILL.md +110 -0
- package/.claude/skills/debug/SKILL.md +349 -0
- package/.claude/skills/get-qodo-rules/SKILL.md +122 -0
- package/.claude/skills/get-qodo-rules/references/output-format.md +41 -0
- package/.claude/skills/get-qodo-rules/references/pagination.md +33 -0
- package/.claude/skills/get-qodo-rules/references/repository-scope.md +26 -0
- package/.claude/skills/qodo-pr-resolver/SKILL.md +326 -0
- package/.claude/skills/qodo-pr-resolver/resources/providers.md +329 -0
- package/.claude/skills/setup/SKILL.md +218 -0
- package/.claude/skills/update-nanoclaw/SKILL.md +235 -0
- package/.claude/skills/update-skills/SKILL.md +130 -0
- package/.claude/skills/use-local-whisper/SKILL.md +152 -0
- package/.claude/skills/x-integration/SKILL.md +417 -0
- package/.claude/skills/x-integration/agent.ts +243 -0
- package/.claude/skills/x-integration/host.ts +159 -0
- package/.claude/skills/x-integration/lib/browser.ts +148 -0
- package/.claude/skills/x-integration/lib/config.ts +62 -0
- package/.claude/skills/x-integration/scripts/like.ts +56 -0
- package/.claude/skills/x-integration/scripts/post.ts +66 -0
- package/.claude/skills/x-integration/scripts/quote.ts +80 -0
- package/.claude/skills/x-integration/scripts/reply.ts +74 -0
- package/.claude/skills/x-integration/scripts/retweet.ts +62 -0
- package/.claude/skills/x-integration/scripts/setup.ts +87 -0
- package/.env.example +1 -0
- package/.github/CODEOWNERS +10 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +14 -0
- package/.github/workflows/bump-version.yml +32 -0
- package/.github/workflows/ci.yml +25 -0
- package/.github/workflows/merge-forward-skills.yml +160 -0
- package/.github/workflows/update-tokens.yml +42 -0
- package/.husky/pre-commit +1 -0
- package/.mcp.json +3 -0
- package/.nvmrc +1 -0
- package/.prettierrc +3 -0
- package/CHANGELOG.md +8 -0
- package/CLAUDE.md +64 -0
- package/CONTRIBUTING.md +23 -0
- package/CONTRIBUTORS.md +15 -0
- package/LICENSE +21 -0
- package/NanoClaw_with_Web-Support.md +290 -0
- package/README.md +261 -0
- package/README_zh.md +200 -0
- package/assets/nanoclaw-favicon.png +0 -0
- package/assets/nanoclaw-icon.png +0 -0
- package/assets/nanoclaw-logo-dark.png +0 -0
- package/assets/nanoclaw-logo.png +0 -0
- package/assets/nanoclaw-profile.jpeg +0 -0
- package/assets/nanoclaw-sales.png +0 -0
- package/assets/social-preview.jpg +0 -0
- package/config-examples/mount-allowlist.json +25 -0
- package/container/Dockerfile +70 -0
- package/container/agent-runner/package-lock.json +1524 -0
- package/container/agent-runner/package.json +21 -0
- package/container/agent-runner/src/index.ts +558 -0
- package/container/agent-runner/src/ipc-mcp-stdio.ts +338 -0
- package/container/agent-runner/tsconfig.json +15 -0
- package/container/build.sh +23 -0
- package/container/skills/agent-browser/SKILL.md +159 -0
- package/container/skills/capabilities/SKILL.md +100 -0
- package/container/skills/status/SKILL.md +104 -0
- package/dist/channels/index.d.ts +2 -0
- package/dist/channels/index.d.ts.map +1 -0
- package/dist/channels/index.js +9 -0
- package/dist/channels/index.js.map +1 -0
- package/dist/channels/registry.d.ts +13 -0
- package/dist/channels/registry.d.ts.map +1 -0
- package/dist/channels/registry.js +11 -0
- package/dist/channels/registry.js.map +1 -0
- package/dist/channels/registry.test.d.ts +2 -0
- package/dist/channels/registry.test.d.ts.map +1 -0
- package/dist/channels/registry.test.js +32 -0
- package/dist/channels/registry.test.js.map +1 -0
- package/dist/channels/web.d.ts +2 -0
- package/dist/channels/web.d.ts.map +1 -0
- package/dist/channels/web.js +1738 -0
- package/dist/channels/web.js.map +1 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +182 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +36 -0
- package/dist/config.js.map +1 -0
- package/dist/container-runner.d.ts +44 -0
- package/dist/container-runner.d.ts.map +1 -0
- package/dist/container-runner.js +467 -0
- package/dist/container-runner.js.map +1 -0
- package/dist/container-runner.test.d.ts +2 -0
- package/dist/container-runner.test.d.ts.map +1 -0
- package/dist/container-runner.test.js +150 -0
- package/dist/container-runner.test.js.map +1 -0
- package/dist/container-runtime.d.ts +22 -0
- package/dist/container-runtime.d.ts.map +1 -0
- package/dist/container-runtime.js +96 -0
- package/dist/container-runtime.js.map +1 -0
- package/dist/container-runtime.test.d.ts +2 -0
- package/dist/container-runtime.test.d.ts.map +1 -0
- package/dist/container-runtime.test.js +93 -0
- package/dist/container-runtime.test.js.map +1 -0
- package/dist/credential-proxy.d.ts +21 -0
- package/dist/credential-proxy.d.ts.map +1 -0
- package/dist/credential-proxy.js +95 -0
- package/dist/credential-proxy.js.map +1 -0
- package/dist/credential-proxy.test.d.ts +2 -0
- package/dist/credential-proxy.test.d.ts.map +1 -0
- package/dist/credential-proxy.test.js +134 -0
- package/dist/credential-proxy.test.js.map +1 -0
- package/dist/db.d.ts +115 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +549 -0
- package/dist/db.js.map +1 -0
- package/dist/db.test.d.ts +2 -0
- package/dist/db.test.d.ts.map +1 -0
- package/dist/db.test.js +360 -0
- package/dist/db.test.js.map +1 -0
- package/dist/env.d.ts +8 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +42 -0
- package/dist/env.js.map +1 -0
- package/dist/formatting.test.d.ts +2 -0
- package/dist/formatting.test.d.ts.map +1 -0
- package/dist/formatting.test.js +183 -0
- package/dist/formatting.test.js.map +1 -0
- package/dist/group-folder.d.ts +5 -0
- package/dist/group-folder.d.ts.map +1 -0
- package/dist/group-folder.js +44 -0
- package/dist/group-folder.js.map +1 -0
- package/dist/group-folder.test.d.ts +2 -0
- package/dist/group-folder.test.d.ts.map +1 -0
- package/dist/group-folder.test.js +29 -0
- package/dist/group-folder.test.js.map +1 -0
- package/dist/group-queue.d.ts +34 -0
- package/dist/group-queue.d.ts.map +1 -0
- package/dist/group-queue.js +263 -0
- package/dist/group-queue.js.map +1 -0
- package/dist/group-queue.test.d.ts +2 -0
- package/dist/group-queue.test.d.ts.map +1 -0
- package/dist/group-queue.test.js +341 -0
- package/dist/group-queue.test.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +518 -0
- package/dist/index.js.map +1 -0
- package/dist/ipc-auth.test.d.ts +2 -0
- package/dist/ipc-auth.test.d.ts.map +1 -0
- package/dist/ipc-auth.test.js +434 -0
- package/dist/ipc-auth.test.js.map +1 -0
- package/dist/ipc.d.ts +32 -0
- package/dist/ipc.d.ts.map +1 -0
- package/dist/ipc.js +311 -0
- package/dist/ipc.js.map +1 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +14 -0
- package/dist/logger.js.map +1 -0
- package/dist/mount-security.d.ts +34 -0
- package/dist/mount-security.d.ts.map +1 -0
- package/dist/mount-security.js +325 -0
- package/dist/mount-security.js.map +1 -0
- package/dist/remote-control.d.ts +32 -0
- package/dist/remote-control.d.ts.map +1 -0
- package/dist/remote-control.js +185 -0
- package/dist/remote-control.js.map +1 -0
- package/dist/remote-control.test.d.ts +2 -0
- package/dist/remote-control.test.d.ts.map +1 -0
- package/dist/remote-control.test.js +321 -0
- package/dist/remote-control.test.js.map +1 -0
- package/dist/router.d.ts +8 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +37 -0
- package/dist/router.js.map +1 -0
- package/dist/routing.test.d.ts +2 -0
- package/dist/routing.test.d.ts.map +1 -0
- package/dist/routing.test.js +81 -0
- package/dist/routing.test.js.map +1 -0
- package/dist/sender-allowlist.d.ts +14 -0
- package/dist/sender-allowlist.d.ts.map +1 -0
- package/dist/sender-allowlist.js +79 -0
- package/dist/sender-allowlist.js.map +1 -0
- package/dist/sender-allowlist.test.d.ts +2 -0
- package/dist/sender-allowlist.test.d.ts.map +1 -0
- package/dist/sender-allowlist.test.js +186 -0
- package/dist/sender-allowlist.test.js.map +1 -0
- package/dist/session-commands.d.ts +47 -0
- package/dist/session-commands.d.ts.map +1 -0
- package/dist/session-commands.js +102 -0
- package/dist/session-commands.js.map +1 -0
- package/dist/session-commands.test.d.ts +2 -0
- package/dist/session-commands.test.d.ts.map +1 -0
- package/dist/session-commands.test.js +190 -0
- package/dist/session-commands.test.js.map +1 -0
- package/dist/task-scheduler.d.ts +22 -0
- package/dist/task-scheduler.d.ts.map +1 -0
- package/dist/task-scheduler.js +210 -0
- package/dist/task-scheduler.js.map +1 -0
- package/dist/task-scheduler.test.d.ts +2 -0
- package/dist/task-scheduler.test.d.ts.map +1 -0
- package/dist/task-scheduler.test.js +107 -0
- package/dist/task-scheduler.test.js.map +1 -0
- package/dist/timezone.d.ts +6 -0
- package/dist/timezone.d.ts.map +1 -0
- package/dist/timezone.js +17 -0
- package/dist/timezone.js.map +1 -0
- package/dist/timezone.test.d.ts +2 -0
- package/dist/timezone.test.d.ts.map +1 -0
- package/dist/timezone.test.js +23 -0
- package/dist/timezone.test.js.map +1 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/docs/APPLE-CONTAINER-NETWORKING.md +90 -0
- package/docs/DEBUG_CHECKLIST.md +143 -0
- package/docs/REQUIREMENTS.md +196 -0
- package/docs/SDK_DEEP_DIVE.md +643 -0
- package/docs/SECURITY.md +122 -0
- package/docs/SPEC.md +785 -0
- package/docs/docker-sandboxes.md +359 -0
- package/docs/nanoclaw-architecture-final.md +1063 -0
- package/docs/nanorepo-architecture.md +168 -0
- package/docs/skills-as-branches.md +662 -0
- package/groups/global/CLAUDE.md +58 -0
- package/groups/main/CLAUDE.md +246 -0
- package/launchd/com.nanoclaw.plist +32 -0
- package/package.json +45 -0
- package/repo-tokens/README.md +113 -0
- package/repo-tokens/action.yml +186 -0
- package/repo-tokens/badge.svg +23 -0
- package/repo-tokens/examples/green.svg +14 -0
- package/repo-tokens/examples/red.svg +14 -0
- package/repo-tokens/examples/yellow-green.svg +14 -0
- package/repo-tokens/examples/yellow.svg +14 -0
- package/scripts/run-migrations.ts +105 -0
- package/setup/container.ts +144 -0
- package/setup/environment.test.ts +121 -0
- package/setup/environment.ts +94 -0
- package/setup/groups.ts +229 -0
- package/setup/index.ts +58 -0
- package/setup/mounts.ts +115 -0
- package/setup/platform.test.ts +120 -0
- package/setup/platform.ts +132 -0
- package/setup/register.test.ts +257 -0
- package/setup/register.ts +177 -0
- package/setup/service.test.ts +187 -0
- package/setup/service.ts +362 -0
- package/setup/status.ts +16 -0
- package/setup/verify.ts +192 -0
- package/setup.sh +161 -0
- package/src/channels/index.ts +12 -0
- package/src/channels/registry.test.ts +42 -0
- package/src/channels/registry.ts +32 -0
- package/src/channels/web.ts +1856 -0
- package/src/cli.ts +209 -0
- package/src/config.ts +73 -0
- package/src/container-runner.test.ts +210 -0
- package/src/container-runner.ts +707 -0
- package/src/container-runtime.test.ts +149 -0
- package/src/container-runtime.ts +127 -0
- package/src/credential-proxy.test.ts +192 -0
- package/src/credential-proxy.ts +125 -0
- package/src/db.test.ts +484 -0
- package/src/db.ts +803 -0
- package/src/env.ts +42 -0
- package/src/formatting.test.ts +256 -0
- package/src/group-folder.test.ts +43 -0
- package/src/group-folder.ts +44 -0
- package/src/group-queue.test.ts +484 -0
- package/src/group-queue.ts +365 -0
- package/src/index.ts +731 -0
- package/src/ipc-auth.test.ts +679 -0
- package/src/ipc.ts +461 -0
- package/src/logger.ts +16 -0
- package/src/mount-security.ts +419 -0
- package/src/remote-control.test.ts +397 -0
- package/src/remote-control.ts +224 -0
- package/src/router.ts +52 -0
- package/src/routing.test.ts +170 -0
- package/src/sender-allowlist.test.ts +216 -0
- package/src/sender-allowlist.ts +128 -0
- package/src/session-commands.test.ts +247 -0
- package/src/session-commands.ts +163 -0
- package/src/task-scheduler.test.ts +129 -0
- package/src/task-scheduler.ts +295 -0
- package/src/timezone.test.ts +29 -0
- package/src/timezone.ts +16 -0
- package/src/types.ts +107 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +7 -0
- package/vitest.skills.config.ts +7 -0
package/src/ipc.ts
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
import { CronExpressionParser } from 'cron-parser';
|
|
5
|
+
|
|
6
|
+
import { DATA_DIR, IPC_POLL_INTERVAL, TIMEZONE } from './config.js';
|
|
7
|
+
import { AvailableGroup } from './container-runner.js';
|
|
8
|
+
import { createTask, deleteTask, getTaskById, updateTask } from './db.js';
|
|
9
|
+
import { isValidGroupFolder } from './group-folder.js';
|
|
10
|
+
import { logger } from './logger.js';
|
|
11
|
+
import { RegisteredGroup } from './types.js';
|
|
12
|
+
|
|
13
|
+
export interface IpcDeps {
|
|
14
|
+
sendMessage: (jid: string, text: string) => Promise<void>;
|
|
15
|
+
registeredGroups: () => Record<string, RegisteredGroup>;
|
|
16
|
+
registerGroup: (jid: string, group: RegisteredGroup) => void;
|
|
17
|
+
syncGroups: (force: boolean) => Promise<void>;
|
|
18
|
+
getAvailableGroups: () => AvailableGroup[];
|
|
19
|
+
writeGroupsSnapshot: (
|
|
20
|
+
groupFolder: string,
|
|
21
|
+
isMain: boolean,
|
|
22
|
+
availableGroups: AvailableGroup[],
|
|
23
|
+
registeredJids: Set<string>,
|
|
24
|
+
) => void;
|
|
25
|
+
onTasksChanged: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let ipcWatcherRunning = false;
|
|
29
|
+
|
|
30
|
+
export function startIpcWatcher(deps: IpcDeps): void {
|
|
31
|
+
if (ipcWatcherRunning) {
|
|
32
|
+
logger.debug('IPC watcher already running, skipping duplicate start');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
ipcWatcherRunning = true;
|
|
36
|
+
|
|
37
|
+
const ipcBaseDir = path.join(DATA_DIR, 'ipc');
|
|
38
|
+
fs.mkdirSync(ipcBaseDir, { recursive: true });
|
|
39
|
+
|
|
40
|
+
const processIpcFiles = async () => {
|
|
41
|
+
// Scan all group IPC directories (identity determined by directory)
|
|
42
|
+
let groupFolders: string[];
|
|
43
|
+
try {
|
|
44
|
+
groupFolders = fs.readdirSync(ipcBaseDir).filter((f) => {
|
|
45
|
+
const stat = fs.statSync(path.join(ipcBaseDir, f));
|
|
46
|
+
return stat.isDirectory() && f !== 'errors';
|
|
47
|
+
});
|
|
48
|
+
} catch (err) {
|
|
49
|
+
logger.error({ err }, 'Error reading IPC base directory');
|
|
50
|
+
setTimeout(processIpcFiles, IPC_POLL_INTERVAL);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const registeredGroups = deps.registeredGroups();
|
|
55
|
+
|
|
56
|
+
// Build folder→isMain lookup from registered groups
|
|
57
|
+
const folderIsMain = new Map<string, boolean>();
|
|
58
|
+
for (const group of Object.values(registeredGroups)) {
|
|
59
|
+
if (group.isMain) folderIsMain.set(group.folder, true);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const sourceGroup of groupFolders) {
|
|
63
|
+
const isMain = folderIsMain.get(sourceGroup) === true;
|
|
64
|
+
const messagesDir = path.join(ipcBaseDir, sourceGroup, 'messages');
|
|
65
|
+
const tasksDir = path.join(ipcBaseDir, sourceGroup, 'tasks');
|
|
66
|
+
|
|
67
|
+
// Process messages from this group's IPC directory
|
|
68
|
+
try {
|
|
69
|
+
if (fs.existsSync(messagesDir)) {
|
|
70
|
+
const messageFiles = fs
|
|
71
|
+
.readdirSync(messagesDir)
|
|
72
|
+
.filter((f) => f.endsWith('.json'));
|
|
73
|
+
for (const file of messageFiles) {
|
|
74
|
+
const filePath = path.join(messagesDir, file);
|
|
75
|
+
try {
|
|
76
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
77
|
+
if (data.type === 'message' && data.chatJid && data.text) {
|
|
78
|
+
// Authorization: verify this group can send to this chatJid
|
|
79
|
+
const targetGroup = registeredGroups[data.chatJid];
|
|
80
|
+
if (
|
|
81
|
+
isMain ||
|
|
82
|
+
(targetGroup && targetGroup.folder === sourceGroup)
|
|
83
|
+
) {
|
|
84
|
+
await deps.sendMessage(data.chatJid, data.text);
|
|
85
|
+
logger.info(
|
|
86
|
+
{ chatJid: data.chatJid, sourceGroup },
|
|
87
|
+
'IPC message sent',
|
|
88
|
+
);
|
|
89
|
+
} else {
|
|
90
|
+
logger.warn(
|
|
91
|
+
{ chatJid: data.chatJid, sourceGroup },
|
|
92
|
+
'Unauthorized IPC message attempt blocked',
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
fs.unlinkSync(filePath);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
logger.error(
|
|
99
|
+
{ file, sourceGroup, err },
|
|
100
|
+
'Error processing IPC message',
|
|
101
|
+
);
|
|
102
|
+
const errorDir = path.join(ipcBaseDir, 'errors');
|
|
103
|
+
fs.mkdirSync(errorDir, { recursive: true });
|
|
104
|
+
fs.renameSync(
|
|
105
|
+
filePath,
|
|
106
|
+
path.join(errorDir, `${sourceGroup}-${file}`),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch (err) {
|
|
112
|
+
logger.error(
|
|
113
|
+
{ err, sourceGroup },
|
|
114
|
+
'Error reading IPC messages directory',
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Process tasks from this group's IPC directory
|
|
119
|
+
try {
|
|
120
|
+
if (fs.existsSync(tasksDir)) {
|
|
121
|
+
const taskFiles = fs
|
|
122
|
+
.readdirSync(tasksDir)
|
|
123
|
+
.filter((f) => f.endsWith('.json'));
|
|
124
|
+
for (const file of taskFiles) {
|
|
125
|
+
const filePath = path.join(tasksDir, file);
|
|
126
|
+
try {
|
|
127
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
128
|
+
// Pass source group identity to processTaskIpc for authorization
|
|
129
|
+
await processTaskIpc(data, sourceGroup, isMain, deps);
|
|
130
|
+
fs.unlinkSync(filePath);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
logger.error(
|
|
133
|
+
{ file, sourceGroup, err },
|
|
134
|
+
'Error processing IPC task',
|
|
135
|
+
);
|
|
136
|
+
const errorDir = path.join(ipcBaseDir, 'errors');
|
|
137
|
+
fs.mkdirSync(errorDir, { recursive: true });
|
|
138
|
+
fs.renameSync(
|
|
139
|
+
filePath,
|
|
140
|
+
path.join(errorDir, `${sourceGroup}-${file}`),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
logger.error({ err, sourceGroup }, 'Error reading IPC tasks directory');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
setTimeout(processIpcFiles, IPC_POLL_INTERVAL);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
processIpcFiles();
|
|
154
|
+
logger.info('IPC watcher started (per-group namespaces)');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function processTaskIpc(
|
|
158
|
+
data: {
|
|
159
|
+
type: string;
|
|
160
|
+
taskId?: string;
|
|
161
|
+
prompt?: string;
|
|
162
|
+
schedule_type?: string;
|
|
163
|
+
schedule_value?: string;
|
|
164
|
+
context_mode?: string;
|
|
165
|
+
groupFolder?: string;
|
|
166
|
+
chatJid?: string;
|
|
167
|
+
targetJid?: string;
|
|
168
|
+
// For register_group
|
|
169
|
+
jid?: string;
|
|
170
|
+
name?: string;
|
|
171
|
+
folder?: string;
|
|
172
|
+
trigger?: string;
|
|
173
|
+
requiresTrigger?: boolean;
|
|
174
|
+
containerConfig?: RegisteredGroup['containerConfig'];
|
|
175
|
+
},
|
|
176
|
+
sourceGroup: string, // Verified identity from IPC directory
|
|
177
|
+
isMain: boolean, // Verified from directory path
|
|
178
|
+
deps: IpcDeps,
|
|
179
|
+
): Promise<void> {
|
|
180
|
+
const registeredGroups = deps.registeredGroups();
|
|
181
|
+
|
|
182
|
+
switch (data.type) {
|
|
183
|
+
case 'schedule_task':
|
|
184
|
+
if (
|
|
185
|
+
data.prompt &&
|
|
186
|
+
data.schedule_type &&
|
|
187
|
+
data.schedule_value &&
|
|
188
|
+
data.targetJid
|
|
189
|
+
) {
|
|
190
|
+
// Resolve the target group from JID
|
|
191
|
+
const targetJid = data.targetJid as string;
|
|
192
|
+
const targetGroupEntry = registeredGroups[targetJid];
|
|
193
|
+
|
|
194
|
+
if (!targetGroupEntry) {
|
|
195
|
+
logger.warn(
|
|
196
|
+
{ targetJid },
|
|
197
|
+
'Cannot schedule task: target group not registered',
|
|
198
|
+
);
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const targetFolder = targetGroupEntry.folder;
|
|
203
|
+
|
|
204
|
+
// Authorization: non-main groups can only schedule for themselves
|
|
205
|
+
if (!isMain && targetFolder !== sourceGroup) {
|
|
206
|
+
logger.warn(
|
|
207
|
+
{ sourceGroup, targetFolder },
|
|
208
|
+
'Unauthorized schedule_task attempt blocked',
|
|
209
|
+
);
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const scheduleType = data.schedule_type as 'cron' | 'interval' | 'once';
|
|
214
|
+
|
|
215
|
+
let nextRun: string | null = null;
|
|
216
|
+
if (scheduleType === 'cron') {
|
|
217
|
+
try {
|
|
218
|
+
const interval = CronExpressionParser.parse(data.schedule_value, {
|
|
219
|
+
tz: TIMEZONE,
|
|
220
|
+
});
|
|
221
|
+
nextRun = interval.next().toISOString();
|
|
222
|
+
} catch {
|
|
223
|
+
logger.warn(
|
|
224
|
+
{ scheduleValue: data.schedule_value },
|
|
225
|
+
'Invalid cron expression',
|
|
226
|
+
);
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
} else if (scheduleType === 'interval') {
|
|
230
|
+
const ms = parseInt(data.schedule_value, 10);
|
|
231
|
+
if (isNaN(ms) || ms <= 0) {
|
|
232
|
+
logger.warn(
|
|
233
|
+
{ scheduleValue: data.schedule_value },
|
|
234
|
+
'Invalid interval',
|
|
235
|
+
);
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
nextRun = new Date(Date.now() + ms).toISOString();
|
|
239
|
+
} else if (scheduleType === 'once') {
|
|
240
|
+
const date = new Date(data.schedule_value);
|
|
241
|
+
if (isNaN(date.getTime())) {
|
|
242
|
+
logger.warn(
|
|
243
|
+
{ scheduleValue: data.schedule_value },
|
|
244
|
+
'Invalid timestamp',
|
|
245
|
+
);
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
nextRun = date.toISOString();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const taskId =
|
|
252
|
+
data.taskId ||
|
|
253
|
+
`task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
254
|
+
const contextMode =
|
|
255
|
+
data.context_mode === 'group' || data.context_mode === 'isolated'
|
|
256
|
+
? data.context_mode
|
|
257
|
+
: 'isolated';
|
|
258
|
+
createTask({
|
|
259
|
+
id: taskId,
|
|
260
|
+
group_folder: targetFolder,
|
|
261
|
+
chat_jid: targetJid,
|
|
262
|
+
prompt: data.prompt,
|
|
263
|
+
schedule_type: scheduleType,
|
|
264
|
+
schedule_value: data.schedule_value,
|
|
265
|
+
context_mode: contextMode,
|
|
266
|
+
next_run: nextRun,
|
|
267
|
+
status: 'active',
|
|
268
|
+
created_at: new Date().toISOString(),
|
|
269
|
+
});
|
|
270
|
+
logger.info(
|
|
271
|
+
{ taskId, sourceGroup, targetFolder, contextMode },
|
|
272
|
+
'Task created via IPC',
|
|
273
|
+
);
|
|
274
|
+
deps.onTasksChanged();
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
277
|
+
|
|
278
|
+
case 'pause_task':
|
|
279
|
+
if (data.taskId) {
|
|
280
|
+
const task = getTaskById(data.taskId);
|
|
281
|
+
if (task && (isMain || task.group_folder === sourceGroup)) {
|
|
282
|
+
updateTask(data.taskId, { status: 'paused' });
|
|
283
|
+
logger.info(
|
|
284
|
+
{ taskId: data.taskId, sourceGroup },
|
|
285
|
+
'Task paused via IPC',
|
|
286
|
+
);
|
|
287
|
+
deps.onTasksChanged();
|
|
288
|
+
} else {
|
|
289
|
+
logger.warn(
|
|
290
|
+
{ taskId: data.taskId, sourceGroup },
|
|
291
|
+
'Unauthorized task pause attempt',
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
break;
|
|
296
|
+
|
|
297
|
+
case 'resume_task':
|
|
298
|
+
if (data.taskId) {
|
|
299
|
+
const task = getTaskById(data.taskId);
|
|
300
|
+
if (task && (isMain || task.group_folder === sourceGroup)) {
|
|
301
|
+
updateTask(data.taskId, { status: 'active' });
|
|
302
|
+
logger.info(
|
|
303
|
+
{ taskId: data.taskId, sourceGroup },
|
|
304
|
+
'Task resumed via IPC',
|
|
305
|
+
);
|
|
306
|
+
deps.onTasksChanged();
|
|
307
|
+
} else {
|
|
308
|
+
logger.warn(
|
|
309
|
+
{ taskId: data.taskId, sourceGroup },
|
|
310
|
+
'Unauthorized task resume attempt',
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
break;
|
|
315
|
+
|
|
316
|
+
case 'cancel_task':
|
|
317
|
+
if (data.taskId) {
|
|
318
|
+
const task = getTaskById(data.taskId);
|
|
319
|
+
if (task && (isMain || task.group_folder === sourceGroup)) {
|
|
320
|
+
deleteTask(data.taskId);
|
|
321
|
+
logger.info(
|
|
322
|
+
{ taskId: data.taskId, sourceGroup },
|
|
323
|
+
'Task cancelled via IPC',
|
|
324
|
+
);
|
|
325
|
+
deps.onTasksChanged();
|
|
326
|
+
} else {
|
|
327
|
+
logger.warn(
|
|
328
|
+
{ taskId: data.taskId, sourceGroup },
|
|
329
|
+
'Unauthorized task cancel attempt',
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
break;
|
|
334
|
+
|
|
335
|
+
case 'update_task':
|
|
336
|
+
if (data.taskId) {
|
|
337
|
+
const task = getTaskById(data.taskId);
|
|
338
|
+
if (!task) {
|
|
339
|
+
logger.warn(
|
|
340
|
+
{ taskId: data.taskId, sourceGroup },
|
|
341
|
+
'Task not found for update',
|
|
342
|
+
);
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
if (!isMain && task.group_folder !== sourceGroup) {
|
|
346
|
+
logger.warn(
|
|
347
|
+
{ taskId: data.taskId, sourceGroup },
|
|
348
|
+
'Unauthorized task update attempt',
|
|
349
|
+
);
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const updates: Parameters<typeof updateTask>[1] = {};
|
|
354
|
+
if (data.prompt !== undefined) updates.prompt = data.prompt;
|
|
355
|
+
if (data.schedule_type !== undefined)
|
|
356
|
+
updates.schedule_type = data.schedule_type as
|
|
357
|
+
| 'cron'
|
|
358
|
+
| 'interval'
|
|
359
|
+
| 'once';
|
|
360
|
+
if (data.schedule_value !== undefined)
|
|
361
|
+
updates.schedule_value = data.schedule_value;
|
|
362
|
+
|
|
363
|
+
// Recompute next_run if schedule changed
|
|
364
|
+
if (data.schedule_type || data.schedule_value) {
|
|
365
|
+
const updatedTask = {
|
|
366
|
+
...task,
|
|
367
|
+
...updates,
|
|
368
|
+
};
|
|
369
|
+
if (updatedTask.schedule_type === 'cron') {
|
|
370
|
+
try {
|
|
371
|
+
const interval = CronExpressionParser.parse(
|
|
372
|
+
updatedTask.schedule_value,
|
|
373
|
+
{ tz: TIMEZONE },
|
|
374
|
+
);
|
|
375
|
+
updates.next_run = interval.next().toISOString();
|
|
376
|
+
} catch {
|
|
377
|
+
logger.warn(
|
|
378
|
+
{ taskId: data.taskId, value: updatedTask.schedule_value },
|
|
379
|
+
'Invalid cron in task update',
|
|
380
|
+
);
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
} else if (updatedTask.schedule_type === 'interval') {
|
|
384
|
+
const ms = parseInt(updatedTask.schedule_value, 10);
|
|
385
|
+
if (!isNaN(ms) && ms > 0) {
|
|
386
|
+
updates.next_run = new Date(Date.now() + ms).toISOString();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
updateTask(data.taskId, updates);
|
|
392
|
+
logger.info(
|
|
393
|
+
{ taskId: data.taskId, sourceGroup, updates },
|
|
394
|
+
'Task updated via IPC',
|
|
395
|
+
);
|
|
396
|
+
deps.onTasksChanged();
|
|
397
|
+
}
|
|
398
|
+
break;
|
|
399
|
+
|
|
400
|
+
case 'refresh_groups':
|
|
401
|
+
// Only main group can request a refresh
|
|
402
|
+
if (isMain) {
|
|
403
|
+
logger.info(
|
|
404
|
+
{ sourceGroup },
|
|
405
|
+
'Group metadata refresh requested via IPC',
|
|
406
|
+
);
|
|
407
|
+
await deps.syncGroups(true);
|
|
408
|
+
// Write updated snapshot immediately
|
|
409
|
+
const availableGroups = deps.getAvailableGroups();
|
|
410
|
+
deps.writeGroupsSnapshot(
|
|
411
|
+
sourceGroup,
|
|
412
|
+
true,
|
|
413
|
+
availableGroups,
|
|
414
|
+
new Set(Object.keys(registeredGroups)),
|
|
415
|
+
);
|
|
416
|
+
} else {
|
|
417
|
+
logger.warn(
|
|
418
|
+
{ sourceGroup },
|
|
419
|
+
'Unauthorized refresh_groups attempt blocked',
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
break;
|
|
423
|
+
|
|
424
|
+
case 'register_group':
|
|
425
|
+
// Only main group can register new groups
|
|
426
|
+
if (!isMain) {
|
|
427
|
+
logger.warn(
|
|
428
|
+
{ sourceGroup },
|
|
429
|
+
'Unauthorized register_group attempt blocked',
|
|
430
|
+
);
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
if (data.jid && data.name && data.folder && data.trigger) {
|
|
434
|
+
if (!isValidGroupFolder(data.folder)) {
|
|
435
|
+
logger.warn(
|
|
436
|
+
{ sourceGroup, folder: data.folder },
|
|
437
|
+
'Invalid register_group request - unsafe folder name',
|
|
438
|
+
);
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
// Defense in depth: agent cannot set isMain via IPC
|
|
442
|
+
deps.registerGroup(data.jid, {
|
|
443
|
+
name: data.name,
|
|
444
|
+
folder: data.folder,
|
|
445
|
+
trigger: data.trigger,
|
|
446
|
+
added_at: new Date().toISOString(),
|
|
447
|
+
containerConfig: data.containerConfig,
|
|
448
|
+
requiresTrigger: data.requiresTrigger,
|
|
449
|
+
});
|
|
450
|
+
} else {
|
|
451
|
+
logger.warn(
|
|
452
|
+
{ data },
|
|
453
|
+
'Invalid register_group request - missing required fields',
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
break;
|
|
457
|
+
|
|
458
|
+
default:
|
|
459
|
+
logger.warn({ type: data.type }, 'Unknown IPC task type');
|
|
460
|
+
}
|
|
461
|
+
}
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import pino from 'pino';
|
|
2
|
+
|
|
3
|
+
export const logger = pino({
|
|
4
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
5
|
+
transport: { target: 'pino-pretty', options: { colorize: true } },
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// Route uncaught errors through pino so they get timestamps in stderr
|
|
9
|
+
process.on('uncaughtException', (err) => {
|
|
10
|
+
logger.fatal({ err }, 'Uncaught exception');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
process.on('unhandledRejection', (reason) => {
|
|
15
|
+
logger.error({ err: reason }, 'Unhandled rejection');
|
|
16
|
+
});
|