3
.prettierrc
Normal file
3
.prettierrc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
@@ -203,7 +203,7 @@ Groups can have additional directories mounted via `containerConfig` in `data/re
|
|||||||
"containerConfig": {
|
"containerConfig": {
|
||||||
"additionalMounts": [
|
"additionalMounts": [
|
||||||
{
|
{
|
||||||
"hostPath": "/Users/gavriel/projects/webapp",
|
"hostPath": "~/projects/webapp",
|
||||||
"containerPath": "webapp",
|
"containerPath": "webapp",
|
||||||
"readonly": false
|
"readonly": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,22 +31,6 @@ When you learn something important:
|
|||||||
- Add recurring context directly to this CLAUDE.md
|
- Add recurring context directly to this CLAUDE.md
|
||||||
- Always index new memory files at the top of CLAUDE.md
|
- Always index new memory files at the top of CLAUDE.md
|
||||||
|
|
||||||
## Qwibit Ops Access
|
|
||||||
|
|
||||||
You have access to Qwibit operations data at `/workspace/extra/qwibit-ops/` with these key areas:
|
|
||||||
|
|
||||||
- **sales/** - Pipeline, deals, playbooks, pitch materials (see `sales/CLAUDE.md`)
|
|
||||||
- **clients/** - Active accounts, service delivery, client management (see `clients/CLAUDE.md`)
|
|
||||||
- **company/** - Strategy, thesis, operational philosophy (see `company/CLAUDE.md`)
|
|
||||||
|
|
||||||
Read the CLAUDE.md files in each folder for role-specific context and workflows.
|
|
||||||
|
|
||||||
**Key context:**
|
|
||||||
- Qwibit is a B2B GEO (Generative Engine Optimization) agency
|
|
||||||
- Pricing: $2,000-$4,000/month, month-to-month contracts
|
|
||||||
- Team: Gavriel (founder, sales & client work), Lazer (founder, dealflow), Ali (PM)
|
|
||||||
- Obsidian-based workflow with Kanban boards (PIPELINE.md, PORTFOLIO.md)
|
|
||||||
|
|
||||||
## WhatsApp Formatting
|
## WhatsApp Formatting
|
||||||
|
|
||||||
Do NOT use markdown headings (##) in WhatsApp messages. Only use:
|
Do NOT use markdown headings (##) in WhatsApp messages. Only use:
|
||||||
@@ -171,7 +155,7 @@ Groups can have extra directories mounted. Add `containerConfig` to their entry:
|
|||||||
"containerConfig": {
|
"containerConfig": {
|
||||||
"additionalMounts": [
|
"additionalMounts": [
|
||||||
{
|
{
|
||||||
"hostPath": "/Users/gavriel/projects/webapp",
|
"hostPath": "~/projects/webapp",
|
||||||
"containerPath": "webapp",
|
"containerPath": "webapp",
|
||||||
"readonly": false
|
"readonly": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
# NanoClaw Testing
|
|
||||||
|
|
||||||
This group is used for testing NanoClaw features and functionality.
|
|
||||||
|
|
||||||
## What You Can Do
|
|
||||||
|
|
||||||
- Answer questions and have conversations
|
|
||||||
- Search the web and fetch content from URLs
|
|
||||||
- Read and write files in your workspace
|
|
||||||
- Run bash commands in your sandbox
|
|
||||||
- Schedule tasks to run later or on a recurring basis
|
|
||||||
- Send messages back to the chat
|
|
||||||
|
|
||||||
## Qwibit Ops Access
|
|
||||||
|
|
||||||
You have access to Qwibit operations data at `/workspace/extra/qwibit-ops/` with these key areas:
|
|
||||||
|
|
||||||
- **sales/** - Pipeline, deals, playbooks, pitch materials (see `sales/CLAUDE.md`)
|
|
||||||
- **clients/** - Active accounts, service delivery, client management (see `clients/CLAUDE.md`)
|
|
||||||
- **company/** - Strategy, thesis, operational philosophy (see `company/CLAUDE.md`)
|
|
||||||
|
|
||||||
Read the CLAUDE.md files in each folder for role-specific context and workflows.
|
|
||||||
|
|
||||||
**Key context:**
|
|
||||||
- Qwibit is a B2B GEO (Generative Engine Optimization) agency
|
|
||||||
- Pricing: $2,000-$4,000/month, month-to-month contracts
|
|
||||||
- Team: Gavriel (founder, sales & client work), Lazer (founder, dealflow), Ali (PM)
|
|
||||||
- Obsidian-based workflow with Kanban boards (PIPELINE.md, PORTFOLIO.md)
|
|
||||||
|
|
||||||
## WhatsApp Formatting
|
|
||||||
|
|
||||||
Do NOT use markdown headings (##) in WhatsApp messages. Only use:
|
|
||||||
- *Bold* (asterisks)
|
|
||||||
- _Italic_ (underscores)
|
|
||||||
- • Bullets (bullet points)
|
|
||||||
- ```Code blocks``` (triple backticks)
|
|
||||||
|
|
||||||
Keep messages clean and readable for WhatsApp.
|
|
||||||
637
package-lock.json
generated
637
package-lock.json
generated
@@ -20,6 +20,7 @@
|
|||||||
"@types/better-sqlite3": "^7.6.12",
|
"@types/better-sqlite3": "^7.6.12",
|
||||||
"@types/node": "^22.10.0",
|
"@types/node": "^22.10.0",
|
||||||
"@types/qrcode-terminal": "^0.12.2",
|
"@types/qrcode-terminal": "^0.12.2",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
"tsx": "^4.19.0",
|
"tsx": "^4.19.0",
|
||||||
"typescript": "^5.7.0"
|
"typescript": "^5.7.0"
|
||||||
},
|
},
|
||||||
@@ -551,6 +552,120 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@img/sharp-darwin-arm64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-darwin-arm64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-darwin-x64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-darwin-x64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
|
||||||
@@ -602,6 +717,103 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-arm": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-arm": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-arm64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-arm64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@img/sharp-linux-ppc64": {
|
"node_modules/@img/sharp-linux-ppc64": {
|
||||||
"version": "0.34.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
|
||||||
@@ -671,6 +883,75 @@
|
|||||||
"@img/sharp-libvips-linux-s390x": "1.2.4"
|
"@img/sharp-libvips-linux-s390x": "1.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@img/sharp-linux-x64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-x64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@img/sharp-wasm32": {
|
"node_modules/@img/sharp-wasm32": {
|
||||||
"version": "0.34.5",
|
"version": "0.34.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
|
||||||
@@ -731,6 +1012,26 @@
|
|||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@img/sharp-win32-x64": {
|
||||||
|
"version": "0.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
|
||||||
|
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@keyv/bigmap": {
|
"node_modules/@keyv/bigmap": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz",
|
||||||
@@ -863,9 +1164,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.19.7",
|
"version": "22.19.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.8.tgz",
|
||||||
"integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==",
|
"integrity": "sha512-ebO/Yl+EAvVe8DnMfi+iaAyIqYdK0q/q0y0rw82INWEKJOBe6b/P3YWE8NW7oOlF/nXFNrHwhARrN/hdgDkraA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
@@ -1282,9 +1583,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/hookified": {
|
"node_modules/hookified": {
|
||||||
"version": "1.15.0",
|
"version": "1.15.1",
|
||||||
"resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz",
|
||||||
"integrity": "sha512-51w+ZZGt7Zw5q7rM3nC4t3aLn/xvKDETsXqMczndvwyVQhAHfUmUuFBRFcos8Iyebtk7OAE9dL26wFNzZVVOkw==",
|
"integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/ieee754": {
|
"node_modules/ieee754": {
|
||||||
@@ -1452,9 +1753,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/music-metadata": {
|
"node_modules/music-metadata": {
|
||||||
"version": "11.11.1",
|
"version": "11.11.2",
|
||||||
"resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-11.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-11.11.2.tgz",
|
||||||
"integrity": "sha512-8FT+lSLznASDhn5KNJtQE6ZH95VqhxtKWNPrvdfhlqgbdZZEEAXehx+xpUvas4VuEZAu49BhQgLa3NlmPeRaww==",
|
"integrity": "sha512-tJx+lsDg1bGUOxojKKj12BIvccBBUcVa6oWrvOchCF0WAQ9E5t/hK35ILp1z3wWrUSYtgg57LfRbvVMkxGIyzA==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -1476,7 +1777,7 @@
|
|||||||
"strtok3": "^10.3.4",
|
"strtok3": "^10.3.4",
|
||||||
"token-types": "^6.1.2",
|
"token-types": "^6.1.2",
|
||||||
"uint8array-extras": "^1.5.0",
|
"uint8array-extras": "^1.5.0",
|
||||||
"win-guid": "^0.2.0"
|
"win-guid": "^0.2.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -1642,6 +1943,22 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "3.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
|
||||||
|
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin/prettier.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/process-warning": {
|
"node_modules/process-warning": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
|
||||||
@@ -1877,306 +2194,6 @@
|
|||||||
"@img/sharp-win32-x64": "0.34.5"
|
"@img/sharp-win32-x64": "0.34.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sharp/node_modules/@img/sharp-darwin-arm64": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-darwin-arm64": "1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sharp/node_modules/@img/sharp-darwin-x64": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-darwin-x64": "1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sharp/node_modules/@img/sharp-libvips-darwin-arm64": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sharp/node_modules/@img/sharp-libvips-darwin-x64": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sharp/node_modules/@img/sharp-libvips-linux-arm": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sharp/node_modules/@img/sharp-libvips-linux-arm64": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sharp/node_modules/@img/sharp-libvips-linux-x64": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sharp/node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sharp/node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sharp/node_modules/@img/sharp-linux-arm": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-linux-arm": "1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sharp/node_modules/@img/sharp-linux-arm64": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-linux-arm64": "1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sharp/node_modules/@img/sharp-linux-x64": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-linux-x64": "1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sharp/node_modules/@img/sharp-linuxmusl-arm64": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sharp/node_modules/@img/sharp-linuxmusl-x64": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sharp/node_modules/@img/sharp-win32-x64": {
|
|
||||||
"version": "0.34.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
|
|
||||||
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/libvips"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/simple-concat": {
|
"node_modules/simple-concat": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||||
|
|||||||
@@ -9,7 +9,9 @@
|
|||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"dev": "tsx src/index.ts",
|
"dev": "tsx src/index.ts",
|
||||||
"auth": "tsx src/whatsapp-auth.ts",
|
"auth": "tsx src/whatsapp-auth.ts",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\"",
|
||||||
|
"format:check": "prettier --check \"src/**/*.ts\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@whiskeysockets/baileys": "^7.0.0-rc.9",
|
"@whiskeysockets/baileys": "^7.0.0-rc.9",
|
||||||
@@ -24,6 +26,7 @@
|
|||||||
"@types/better-sqlite3": "^7.6.12",
|
"@types/better-sqlite3": "^7.6.12",
|
||||||
"@types/node": "^22.10.0",
|
"@types/node": "^22.10.0",
|
||||||
"@types/qrcode-terminal": "^0.12.2",
|
"@types/qrcode-terminal": "^0.12.2",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
"tsx": "^4.19.0",
|
"tsx": "^4.19.0",
|
||||||
"typescript": "^5.7.0"
|
"typescript": "^5.7.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,23 +9,39 @@ const PROJECT_ROOT = process.cwd();
|
|||||||
const HOME_DIR = process.env.HOME || '/Users/user';
|
const HOME_DIR = process.env.HOME || '/Users/user';
|
||||||
|
|
||||||
// Mount security: allowlist stored OUTSIDE project root, never mounted into containers
|
// Mount security: allowlist stored OUTSIDE project root, never mounted into containers
|
||||||
export const MOUNT_ALLOWLIST_PATH = path.join(HOME_DIR, '.config', 'nanoclaw', 'mount-allowlist.json');
|
export const MOUNT_ALLOWLIST_PATH = path.join(
|
||||||
|
HOME_DIR,
|
||||||
|
'.config',
|
||||||
|
'nanoclaw',
|
||||||
|
'mount-allowlist.json',
|
||||||
|
);
|
||||||
export const STORE_DIR = path.resolve(PROJECT_ROOT, 'store');
|
export const STORE_DIR = path.resolve(PROJECT_ROOT, 'store');
|
||||||
export const GROUPS_DIR = path.resolve(PROJECT_ROOT, 'groups');
|
export const GROUPS_DIR = path.resolve(PROJECT_ROOT, 'groups');
|
||||||
export const DATA_DIR = path.resolve(PROJECT_ROOT, 'data');
|
export const DATA_DIR = path.resolve(PROJECT_ROOT, 'data');
|
||||||
export const MAIN_GROUP_FOLDER = 'main';
|
export const MAIN_GROUP_FOLDER = 'main';
|
||||||
|
|
||||||
export const CONTAINER_IMAGE = process.env.CONTAINER_IMAGE || 'nanoclaw-agent:latest';
|
export const CONTAINER_IMAGE =
|
||||||
export const CONTAINER_TIMEOUT = parseInt(process.env.CONTAINER_TIMEOUT || '300000', 10);
|
process.env.CONTAINER_IMAGE || 'nanoclaw-agent:latest';
|
||||||
export const CONTAINER_MAX_OUTPUT_SIZE = parseInt(process.env.CONTAINER_MAX_OUTPUT_SIZE || '10485760', 10); // 10MB default
|
export const CONTAINER_TIMEOUT = parseInt(
|
||||||
|
process.env.CONTAINER_TIMEOUT || '300000',
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
export const CONTAINER_MAX_OUTPUT_SIZE = parseInt(
|
||||||
|
process.env.CONTAINER_MAX_OUTPUT_SIZE || '10485760',
|
||||||
|
10,
|
||||||
|
); // 10MB default
|
||||||
export const IPC_POLL_INTERVAL = 1000;
|
export const IPC_POLL_INTERVAL = 1000;
|
||||||
|
|
||||||
function escapeRegex(str: string): string {
|
function escapeRegex(str: string): string {
|
||||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TRIGGER_PATTERN = new RegExp(`^@${escapeRegex(ASSISTANT_NAME)}\\b`, 'i');
|
export const TRIGGER_PATTERN = new RegExp(
|
||||||
|
`^@${escapeRegex(ASSISTANT_NAME)}\\b`,
|
||||||
|
'i',
|
||||||
|
);
|
||||||
|
|
||||||
// Timezone for scheduled tasks (cron expressions, etc.)
|
// Timezone for scheduled tasks (cron expressions, etc.)
|
||||||
// Uses system timezone by default
|
// Uses system timezone by default
|
||||||
export const TIMEZONE = process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
export const TIMEZONE =
|
||||||
|
process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
|||||||
@@ -2,25 +2,25 @@
|
|||||||
* Container Runner for NanoClaw
|
* Container Runner for NanoClaw
|
||||||
* Spawns agent execution in Apple Container and handles IPC
|
* Spawns agent execution in Apple Container and handles IPC
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import pino from 'pino';
|
import pino from 'pino';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CONTAINER_IMAGE,
|
CONTAINER_IMAGE,
|
||||||
CONTAINER_TIMEOUT,
|
|
||||||
CONTAINER_MAX_OUTPUT_SIZE,
|
CONTAINER_MAX_OUTPUT_SIZE,
|
||||||
|
CONTAINER_TIMEOUT,
|
||||||
|
DATA_DIR,
|
||||||
GROUPS_DIR,
|
GROUPS_DIR,
|
||||||
DATA_DIR
|
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
import { RegisteredGroup } from './types.js';
|
|
||||||
import { validateAdditionalMounts } from './mount-security.js';
|
import { validateAdditionalMounts } from './mount-security.js';
|
||||||
|
import { RegisteredGroup } from './types.js';
|
||||||
|
|
||||||
const logger = pino({
|
const logger = pino({
|
||||||
level: process.env.LOG_LEVEL || 'info',
|
level: process.env.LOG_LEVEL || 'info',
|
||||||
transport: { target: 'pino-pretty', options: { colorize: true } }
|
transport: { target: 'pino-pretty', options: { colorize: true } },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sentinel markers for robust output parsing (must match agent-runner)
|
// Sentinel markers for robust output parsing (must match agent-runner)
|
||||||
@@ -30,7 +30,9 @@ const OUTPUT_END_MARKER = '---NANOCLAW_OUTPUT_END---';
|
|||||||
function getHomeDir(): string {
|
function getHomeDir(): string {
|
||||||
const home = process.env.HOME || os.homedir();
|
const home = process.env.HOME || os.homedir();
|
||||||
if (!home) {
|
if (!home) {
|
||||||
throw new Error('Unable to determine home directory: HOME environment variable is not set and os.homedir() returned empty');
|
throw new Error(
|
||||||
|
'Unable to determine home directory: HOME environment variable is not set and os.homedir() returned empty',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return home;
|
return home;
|
||||||
}
|
}
|
||||||
@@ -57,7 +59,10 @@ interface VolumeMount {
|
|||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildVolumeMounts(group: RegisteredGroup, isMain: boolean): VolumeMount[] {
|
function buildVolumeMounts(
|
||||||
|
group: RegisteredGroup,
|
||||||
|
isMain: boolean,
|
||||||
|
): VolumeMount[] {
|
||||||
const mounts: VolumeMount[] = [];
|
const mounts: VolumeMount[] = [];
|
||||||
const homeDir = getHomeDir();
|
const homeDir = getHomeDir();
|
||||||
const projectRoot = process.cwd();
|
const projectRoot = process.cwd();
|
||||||
@@ -67,21 +72,21 @@ function buildVolumeMounts(group: RegisteredGroup, isMain: boolean): VolumeMount
|
|||||||
mounts.push({
|
mounts.push({
|
||||||
hostPath: projectRoot,
|
hostPath: projectRoot,
|
||||||
containerPath: '/workspace/project',
|
containerPath: '/workspace/project',
|
||||||
readonly: false
|
readonly: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Main also gets its group folder as the working directory
|
// Main also gets its group folder as the working directory
|
||||||
mounts.push({
|
mounts.push({
|
||||||
hostPath: path.join(GROUPS_DIR, group.folder),
|
hostPath: path.join(GROUPS_DIR, group.folder),
|
||||||
containerPath: '/workspace/group',
|
containerPath: '/workspace/group',
|
||||||
readonly: false
|
readonly: false,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Other groups only get their own folder
|
// Other groups only get their own folder
|
||||||
mounts.push({
|
mounts.push({
|
||||||
hostPath: path.join(GROUPS_DIR, group.folder),
|
hostPath: path.join(GROUPS_DIR, group.folder),
|
||||||
containerPath: '/workspace/group',
|
containerPath: '/workspace/group',
|
||||||
readonly: false
|
readonly: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Global memory directory (read-only for non-main)
|
// Global memory directory (read-only for non-main)
|
||||||
@@ -91,19 +96,24 @@ function buildVolumeMounts(group: RegisteredGroup, isMain: boolean): VolumeMount
|
|||||||
mounts.push({
|
mounts.push({
|
||||||
hostPath: globalDir,
|
hostPath: globalDir,
|
||||||
containerPath: '/workspace/global',
|
containerPath: '/workspace/global',
|
||||||
readonly: true
|
readonly: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per-group Claude sessions directory (isolated from other groups)
|
// Per-group Claude sessions directory (isolated from other groups)
|
||||||
// Each group gets their own .claude/ to prevent cross-group session access
|
// Each group gets their own .claude/ to prevent cross-group session access
|
||||||
const groupSessionsDir = path.join(DATA_DIR, 'sessions', group.folder, '.claude');
|
const groupSessionsDir = path.join(
|
||||||
|
DATA_DIR,
|
||||||
|
'sessions',
|
||||||
|
group.folder,
|
||||||
|
'.claude',
|
||||||
|
);
|
||||||
fs.mkdirSync(groupSessionsDir, { recursive: true });
|
fs.mkdirSync(groupSessionsDir, { recursive: true });
|
||||||
mounts.push({
|
mounts.push({
|
||||||
hostPath: groupSessionsDir,
|
hostPath: groupSessionsDir,
|
||||||
containerPath: '/home/node/.claude',
|
containerPath: '/home/node/.claude',
|
||||||
readonly: false
|
readonly: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Per-group IPC namespace: each group gets its own IPC directory
|
// Per-group IPC namespace: each group gets its own IPC directory
|
||||||
@@ -114,7 +124,7 @@ function buildVolumeMounts(group: RegisteredGroup, isMain: boolean): VolumeMount
|
|||||||
mounts.push({
|
mounts.push({
|
||||||
hostPath: groupIpcDir,
|
hostPath: groupIpcDir,
|
||||||
containerPath: '/workspace/ipc',
|
containerPath: '/workspace/ipc',
|
||||||
readonly: false
|
readonly: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Environment file directory (workaround for Apple Container -i env var bug)
|
// Environment file directory (workaround for Apple Container -i env var bug)
|
||||||
@@ -125,20 +135,21 @@ function buildVolumeMounts(group: RegisteredGroup, isMain: boolean): VolumeMount
|
|||||||
if (fs.existsSync(envFile)) {
|
if (fs.existsSync(envFile)) {
|
||||||
const envContent = fs.readFileSync(envFile, 'utf-8');
|
const envContent = fs.readFileSync(envFile, 'utf-8');
|
||||||
const allowedVars = ['CLAUDE_CODE_OAUTH_TOKEN', 'ANTHROPIC_API_KEY'];
|
const allowedVars = ['CLAUDE_CODE_OAUTH_TOKEN', 'ANTHROPIC_API_KEY'];
|
||||||
const filteredLines = envContent
|
const filteredLines = envContent.split('\n').filter((line) => {
|
||||||
.split('\n')
|
|
||||||
.filter(line => {
|
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
if (!trimmed || trimmed.startsWith('#')) return false;
|
if (!trimmed || trimmed.startsWith('#')) return false;
|
||||||
return allowedVars.some(v => trimmed.startsWith(`${v}=`));
|
return allowedVars.some((v) => trimmed.startsWith(`${v}=`));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (filteredLines.length > 0) {
|
if (filteredLines.length > 0) {
|
||||||
fs.writeFileSync(path.join(envDir, 'env'), filteredLines.join('\n') + '\n');
|
fs.writeFileSync(
|
||||||
|
path.join(envDir, 'env'),
|
||||||
|
filteredLines.join('\n') + '\n',
|
||||||
|
);
|
||||||
mounts.push({
|
mounts.push({
|
||||||
hostPath: envDir,
|
hostPath: envDir,
|
||||||
containerPath: '/workspace/env-dir',
|
containerPath: '/workspace/env-dir',
|
||||||
readonly: true
|
readonly: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,7 +159,7 @@ function buildVolumeMounts(group: RegisteredGroup, isMain: boolean): VolumeMount
|
|||||||
const validatedMounts = validateAdditionalMounts(
|
const validatedMounts = validateAdditionalMounts(
|
||||||
group.containerConfig.additionalMounts,
|
group.containerConfig.additionalMounts,
|
||||||
group.name,
|
group.name,
|
||||||
isMain
|
isMain,
|
||||||
);
|
);
|
||||||
mounts.push(...validatedMounts);
|
mounts.push(...validatedMounts);
|
||||||
}
|
}
|
||||||
@@ -162,7 +173,10 @@ function buildContainerArgs(mounts: VolumeMount[]): string[] {
|
|||||||
// Apple Container: --mount for readonly, -v for read-write
|
// Apple Container: --mount for readonly, -v for read-write
|
||||||
for (const mount of mounts) {
|
for (const mount of mounts) {
|
||||||
if (mount.readonly) {
|
if (mount.readonly) {
|
||||||
args.push('--mount', `type=bind,source=${mount.hostPath},target=${mount.containerPath},readonly`);
|
args.push(
|
||||||
|
'--mount',
|
||||||
|
`type=bind,source=${mount.hostPath},target=${mount.containerPath},readonly`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
args.push('-v', `${mount.hostPath}:${mount.containerPath}`);
|
args.push('-v', `${mount.hostPath}:${mount.containerPath}`);
|
||||||
}
|
}
|
||||||
@@ -175,7 +189,7 @@ function buildContainerArgs(mounts: VolumeMount[]): string[] {
|
|||||||
|
|
||||||
export async function runContainerAgent(
|
export async function runContainerAgent(
|
||||||
group: RegisteredGroup,
|
group: RegisteredGroup,
|
||||||
input: ContainerInput
|
input: ContainerInput,
|
||||||
): Promise<ContainerOutput> {
|
): Promise<ContainerOutput> {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
@@ -185,24 +199,33 @@ export async function runContainerAgent(
|
|||||||
const mounts = buildVolumeMounts(group, input.isMain);
|
const mounts = buildVolumeMounts(group, input.isMain);
|
||||||
const containerArgs = buildContainerArgs(mounts);
|
const containerArgs = buildContainerArgs(mounts);
|
||||||
|
|
||||||
logger.debug({
|
logger.debug(
|
||||||
|
{
|
||||||
group: group.name,
|
group: group.name,
|
||||||
mounts: mounts.map(m => `${m.hostPath} -> ${m.containerPath}${m.readonly ? ' (ro)' : ''}`),
|
mounts: mounts.map(
|
||||||
containerArgs: containerArgs.join(' ')
|
(m) =>
|
||||||
}, 'Container mount configuration');
|
`${m.hostPath} -> ${m.containerPath}${m.readonly ? ' (ro)' : ''}`,
|
||||||
|
),
|
||||||
|
containerArgs: containerArgs.join(' '),
|
||||||
|
},
|
||||||
|
'Container mount configuration',
|
||||||
|
);
|
||||||
|
|
||||||
logger.info({
|
logger.info(
|
||||||
|
{
|
||||||
group: group.name,
|
group: group.name,
|
||||||
mountCount: mounts.length,
|
mountCount: mounts.length,
|
||||||
isMain: input.isMain
|
isMain: input.isMain,
|
||||||
}, 'Spawning container agent');
|
},
|
||||||
|
'Spawning container agent',
|
||||||
|
);
|
||||||
|
|
||||||
const logsDir = path.join(GROUPS_DIR, group.folder, 'logs');
|
const logsDir = path.join(GROUPS_DIR, group.folder, 'logs');
|
||||||
fs.mkdirSync(logsDir, { recursive: true });
|
fs.mkdirSync(logsDir, { recursive: true });
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const container = spawn('container', containerArgs, {
|
const container = spawn('container', containerArgs, {
|
||||||
stdio: ['pipe', 'pipe', 'pipe']
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
});
|
});
|
||||||
|
|
||||||
let stdout = '';
|
let stdout = '';
|
||||||
@@ -220,7 +243,10 @@ export async function runContainerAgent(
|
|||||||
if (chunk.length > remaining) {
|
if (chunk.length > remaining) {
|
||||||
stdout += chunk.slice(0, remaining);
|
stdout += chunk.slice(0, remaining);
|
||||||
stdoutTruncated = true;
|
stdoutTruncated = true;
|
||||||
logger.warn({ group: group.name, size: stdout.length }, 'Container stdout truncated due to size limit');
|
logger.warn(
|
||||||
|
{ group: group.name, size: stdout.length },
|
||||||
|
'Container stdout truncated due to size limit',
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
stdout += chunk;
|
stdout += chunk;
|
||||||
}
|
}
|
||||||
@@ -237,7 +263,10 @@ export async function runContainerAgent(
|
|||||||
if (chunk.length > remaining) {
|
if (chunk.length > remaining) {
|
||||||
stderr += chunk.slice(0, remaining);
|
stderr += chunk.slice(0, remaining);
|
||||||
stderrTruncated = true;
|
stderrTruncated = true;
|
||||||
logger.warn({ group: group.name, size: stderr.length }, 'Container stderr truncated due to size limit');
|
logger.warn(
|
||||||
|
{ group: group.name, size: stderr.length },
|
||||||
|
'Container stderr truncated due to size limit',
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
stderr += chunk;
|
stderr += chunk;
|
||||||
}
|
}
|
||||||
@@ -249,7 +278,7 @@ export async function runContainerAgent(
|
|||||||
resolve({
|
resolve({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
result: null,
|
result: null,
|
||||||
error: `Container timed out after ${CONTAINER_TIMEOUT}ms`
|
error: `Container timed out after ${CONTAINER_TIMEOUT}ms`,
|
||||||
});
|
});
|
||||||
}, group.containerConfig?.timeout || CONTAINER_TIMEOUT);
|
}, group.containerConfig?.timeout || CONTAINER_TIMEOUT);
|
||||||
|
|
||||||
@@ -259,7 +288,8 @@ export async function runContainerAgent(
|
|||||||
|
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
const logFile = path.join(logsDir, `container-${timestamp}.log`);
|
const logFile = path.join(logsDir, `container-${timestamp}.log`);
|
||||||
const isVerbose = process.env.LOG_LEVEL === 'debug' || process.env.LOG_LEVEL === 'trace';
|
const isVerbose =
|
||||||
|
process.env.LOG_LEVEL === 'debug' || process.env.LOG_LEVEL === 'trace';
|
||||||
|
|
||||||
const logLines = [
|
const logLines = [
|
||||||
`=== Container Run Log ===`,
|
`=== Container Run Log ===`,
|
||||||
@@ -270,7 +300,7 @@ export async function runContainerAgent(
|
|||||||
`Exit Code: ${code}`,
|
`Exit Code: ${code}`,
|
||||||
`Stdout Truncated: ${stdoutTruncated}`,
|
`Stdout Truncated: ${stdoutTruncated}`,
|
||||||
`Stderr Truncated: ${stderrTruncated}`,
|
`Stderr Truncated: ${stderrTruncated}`,
|
||||||
``
|
``,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (isVerbose) {
|
if (isVerbose) {
|
||||||
@@ -282,13 +312,18 @@ export async function runContainerAgent(
|
|||||||
containerArgs.join(' '),
|
containerArgs.join(' '),
|
||||||
``,
|
``,
|
||||||
`=== Mounts ===`,
|
`=== Mounts ===`,
|
||||||
mounts.map(m => `${m.hostPath} -> ${m.containerPath}${m.readonly ? ' (ro)' : ''}`).join('\n'),
|
mounts
|
||||||
|
.map(
|
||||||
|
(m) =>
|
||||||
|
`${m.hostPath} -> ${m.containerPath}${m.readonly ? ' (ro)' : ''}`,
|
||||||
|
)
|
||||||
|
.join('\n'),
|
||||||
``,
|
``,
|
||||||
`=== Stderr${stderrTruncated ? ' (TRUNCATED)' : ''} ===`,
|
`=== Stderr${stderrTruncated ? ' (TRUNCATED)' : ''} ===`,
|
||||||
stderr,
|
stderr,
|
||||||
``,
|
``,
|
||||||
`=== Stdout${stdoutTruncated ? ' (TRUNCATED)' : ''} ===`,
|
`=== Stdout${stdoutTruncated ? ' (TRUNCATED)' : ''} ===`,
|
||||||
stdout
|
stdout,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
logLines.push(
|
logLines.push(
|
||||||
@@ -297,15 +332,17 @@ export async function runContainerAgent(
|
|||||||
`Session ID: ${input.sessionId || 'new'}`,
|
`Session ID: ${input.sessionId || 'new'}`,
|
||||||
``,
|
``,
|
||||||
`=== Mounts ===`,
|
`=== Mounts ===`,
|
||||||
mounts.map(m => `${m.containerPath}${m.readonly ? ' (ro)' : ''}`).join('\n'),
|
mounts
|
||||||
``
|
.map((m) => `${m.containerPath}${m.readonly ? ' (ro)' : ''}`)
|
||||||
|
.join('\n'),
|
||||||
|
``,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
logLines.push(
|
logLines.push(
|
||||||
`=== Stderr (last 500 chars) ===`,
|
`=== Stderr (last 500 chars) ===`,
|
||||||
stderr.slice(-500),
|
stderr.slice(-500),
|
||||||
``
|
``,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -314,18 +351,21 @@ export async function runContainerAgent(
|
|||||||
logger.debug({ logFile, verbose: isVerbose }, 'Container log written');
|
logger.debug({ logFile, verbose: isVerbose }, 'Container log written');
|
||||||
|
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
logger.error({
|
logger.error(
|
||||||
|
{
|
||||||
group: group.name,
|
group: group.name,
|
||||||
code,
|
code,
|
||||||
duration,
|
duration,
|
||||||
stderr: stderr.slice(-500),
|
stderr: stderr.slice(-500),
|
||||||
logFile
|
logFile,
|
||||||
}, 'Container exited with error');
|
},
|
||||||
|
'Container exited with error',
|
||||||
|
);
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
result: null,
|
result: null,
|
||||||
error: `Container exited with code ${code}: ${stderr.slice(-200)}`
|
error: `Container exited with code ${code}: ${stderr.slice(-200)}`,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -337,7 +377,9 @@ export async function runContainerAgent(
|
|||||||
|
|
||||||
let jsonLine: string;
|
let jsonLine: string;
|
||||||
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
||||||
jsonLine = stdout.slice(startIdx + OUTPUT_START_MARKER.length, endIdx).trim();
|
jsonLine = stdout
|
||||||
|
.slice(startIdx + OUTPUT_START_MARKER.length, endIdx)
|
||||||
|
.trim();
|
||||||
} else {
|
} else {
|
||||||
// Fallback: last non-empty line (backwards compatibility)
|
// Fallback: last non-empty line (backwards compatibility)
|
||||||
const lines = stdout.trim().split('\n');
|
const lines = stdout.trim().split('\n');
|
||||||
@@ -346,25 +388,31 @@ export async function runContainerAgent(
|
|||||||
|
|
||||||
const output: ContainerOutput = JSON.parse(jsonLine);
|
const output: ContainerOutput = JSON.parse(jsonLine);
|
||||||
|
|
||||||
logger.info({
|
logger.info(
|
||||||
|
{
|
||||||
group: group.name,
|
group: group.name,
|
||||||
duration,
|
duration,
|
||||||
status: output.status,
|
status: output.status,
|
||||||
hasResult: !!output.result
|
hasResult: !!output.result,
|
||||||
}, 'Container completed');
|
},
|
||||||
|
'Container completed',
|
||||||
|
);
|
||||||
|
|
||||||
resolve(output);
|
resolve(output);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error({
|
logger.error(
|
||||||
|
{
|
||||||
group: group.name,
|
group: group.name,
|
||||||
stdout: stdout.slice(-500),
|
stdout: stdout.slice(-500),
|
||||||
error: err
|
error: err,
|
||||||
}, 'Failed to parse container output');
|
},
|
||||||
|
'Failed to parse container output',
|
||||||
|
);
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
result: null,
|
result: null,
|
||||||
error: `Failed to parse container output: ${err instanceof Error ? err.message : String(err)}`
|
error: `Failed to parse container output: ${err instanceof Error ? err.message : String(err)}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -375,7 +423,7 @@ export async function runContainerAgent(
|
|||||||
resolve({
|
resolve({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
result: null,
|
result: null,
|
||||||
error: `Container spawn error: ${err.message}`
|
error: `Container spawn error: ${err.message}`,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -392,7 +440,7 @@ export function writeTasksSnapshot(
|
|||||||
schedule_value: string;
|
schedule_value: string;
|
||||||
status: string;
|
status: string;
|
||||||
next_run: string | null;
|
next_run: string | null;
|
||||||
}>
|
}>,
|
||||||
): void {
|
): void {
|
||||||
// Write filtered tasks to the group's IPC directory
|
// Write filtered tasks to the group's IPC directory
|
||||||
const groupIpcDir = path.join(DATA_DIR, 'ipc', groupFolder);
|
const groupIpcDir = path.join(DATA_DIR, 'ipc', groupFolder);
|
||||||
@@ -401,7 +449,7 @@ export function writeTasksSnapshot(
|
|||||||
// Main sees all tasks, others only see their own
|
// Main sees all tasks, others only see their own
|
||||||
const filteredTasks = isMain
|
const filteredTasks = isMain
|
||||||
? tasks
|
? tasks
|
||||||
: tasks.filter(t => t.groupFolder === groupFolder);
|
: tasks.filter((t) => t.groupFolder === groupFolder);
|
||||||
|
|
||||||
const tasksFile = path.join(groupIpcDir, 'current_tasks.json');
|
const tasksFile = path.join(groupIpcDir, 'current_tasks.json');
|
||||||
fs.writeFileSync(tasksFile, JSON.stringify(filteredTasks, null, 2));
|
fs.writeFileSync(tasksFile, JSON.stringify(filteredTasks, null, 2));
|
||||||
@@ -423,7 +471,7 @@ export function writeGroupsSnapshot(
|
|||||||
groupFolder: string,
|
groupFolder: string,
|
||||||
isMain: boolean,
|
isMain: boolean,
|
||||||
groups: AvailableGroup[],
|
groups: AvailableGroup[],
|
||||||
registeredJids: Set<string>
|
registeredJids: Set<string>,
|
||||||
): void {
|
): void {
|
||||||
const groupIpcDir = path.join(DATA_DIR, 'ipc', groupFolder);
|
const groupIpcDir = path.join(DATA_DIR, 'ipc', groupFolder);
|
||||||
fs.mkdirSync(groupIpcDir, { recursive: true });
|
fs.mkdirSync(groupIpcDir, { recursive: true });
|
||||||
@@ -432,8 +480,15 @@ export function writeGroupsSnapshot(
|
|||||||
const visibleGroups = isMain ? groups : [];
|
const visibleGroups = isMain ? groups : [];
|
||||||
|
|
||||||
const groupsFile = path.join(groupIpcDir, 'available_groups.json');
|
const groupsFile = path.join(groupIpcDir, 'available_groups.json');
|
||||||
fs.writeFileSync(groupsFile, JSON.stringify({
|
fs.writeFileSync(
|
||||||
|
groupsFile,
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
groups: visibleGroups,
|
groups: visibleGroups,
|
||||||
lastSync: new Date().toISOString()
|
lastSync: new Date().toISOString(),
|
||||||
}, null, 2));
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
202
src/db.ts
202
src/db.ts
@@ -1,9 +1,11 @@
|
|||||||
import Database from 'better-sqlite3';
|
import Database from 'better-sqlite3';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { proto } from '@whiskeysockets/baileys';
|
import { proto } from '@whiskeysockets/baileys';
|
||||||
import { NewMessage, ScheduledTask, TaskRunLog } from './types.js';
|
|
||||||
import { STORE_DIR } from './config.js';
|
import { STORE_DIR } from './config.js';
|
||||||
|
import { NewMessage, ScheduledTask, TaskRunLog } from './types.js';
|
||||||
|
|
||||||
let db: Database.Database;
|
let db: Database.Database;
|
||||||
|
|
||||||
@@ -63,34 +65,48 @@ export function initDatabase(): void {
|
|||||||
// Add sender_name column if it doesn't exist (migration for existing DBs)
|
// Add sender_name column if it doesn't exist (migration for existing DBs)
|
||||||
try {
|
try {
|
||||||
db.exec(`ALTER TABLE messages ADD COLUMN sender_name TEXT`);
|
db.exec(`ALTER TABLE messages ADD COLUMN sender_name TEXT`);
|
||||||
} catch { /* column already exists */ }
|
} catch {
|
||||||
|
/* column already exists */
|
||||||
|
}
|
||||||
|
|
||||||
// Add context_mode column if it doesn't exist (migration for existing DBs)
|
// Add context_mode column if it doesn't exist (migration for existing DBs)
|
||||||
try {
|
try {
|
||||||
db.exec(`ALTER TABLE scheduled_tasks ADD COLUMN context_mode TEXT DEFAULT 'isolated'`);
|
db.exec(
|
||||||
} catch { /* column already exists */ }
|
`ALTER TABLE scheduled_tasks ADD COLUMN context_mode TEXT DEFAULT 'isolated'`,
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
/* column already exists */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store chat metadata only (no message content).
|
* Store chat metadata only (no message content).
|
||||||
* Used for all chats to enable group discovery without storing sensitive content.
|
* Used for all chats to enable group discovery without storing sensitive content.
|
||||||
*/
|
*/
|
||||||
export function storeChatMetadata(chatJid: string, timestamp: string, name?: string): void {
|
export function storeChatMetadata(
|
||||||
|
chatJid: string,
|
||||||
|
timestamp: string,
|
||||||
|
name?: string,
|
||||||
|
): void {
|
||||||
if (name) {
|
if (name) {
|
||||||
// Update with name, preserving existing timestamp if newer
|
// Update with name, preserving existing timestamp if newer
|
||||||
db.prepare(`
|
db.prepare(
|
||||||
|
`
|
||||||
INSERT INTO chats (jid, name, last_message_time) VALUES (?, ?, ?)
|
INSERT INTO chats (jid, name, last_message_time) VALUES (?, ?, ?)
|
||||||
ON CONFLICT(jid) DO UPDATE SET
|
ON CONFLICT(jid) DO UPDATE SET
|
||||||
name = excluded.name,
|
name = excluded.name,
|
||||||
last_message_time = MAX(last_message_time, excluded.last_message_time)
|
last_message_time = MAX(last_message_time, excluded.last_message_time)
|
||||||
`).run(chatJid, name, timestamp);
|
`,
|
||||||
|
).run(chatJid, name, timestamp);
|
||||||
} else {
|
} else {
|
||||||
// Update timestamp only, preserve existing name if any
|
// Update timestamp only, preserve existing name if any
|
||||||
db.prepare(`
|
db.prepare(
|
||||||
|
`
|
||||||
INSERT INTO chats (jid, name, last_message_time) VALUES (?, ?, ?)
|
INSERT INTO chats (jid, name, last_message_time) VALUES (?, ?, ?)
|
||||||
ON CONFLICT(jid) DO UPDATE SET
|
ON CONFLICT(jid) DO UPDATE SET
|
||||||
last_message_time = MAX(last_message_time, excluded.last_message_time)
|
last_message_time = MAX(last_message_time, excluded.last_message_time)
|
||||||
`).run(chatJid, chatJid, timestamp);
|
`,
|
||||||
|
).run(chatJid, chatJid, timestamp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,10 +116,12 @@ export function storeChatMetadata(chatJid: string, timestamp: string, name?: str
|
|||||||
* Used during group metadata sync.
|
* Used during group metadata sync.
|
||||||
*/
|
*/
|
||||||
export function updateChatName(chatJid: string, name: string): void {
|
export function updateChatName(chatJid: string, name: string): void {
|
||||||
db.prepare(`
|
db.prepare(
|
||||||
|
`
|
||||||
INSERT INTO chats (jid, name, last_message_time) VALUES (?, ?, ?)
|
INSERT INTO chats (jid, name, last_message_time) VALUES (?, ?, ?)
|
||||||
ON CONFLICT(jid) DO UPDATE SET name = excluded.name
|
ON CONFLICT(jid) DO UPDATE SET name = excluded.name
|
||||||
`).run(chatJid, name, new Date().toISOString());
|
`,
|
||||||
|
).run(chatJid, name, new Date().toISOString());
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatInfo {
|
export interface ChatInfo {
|
||||||
@@ -116,11 +134,15 @@ export interface ChatInfo {
|
|||||||
* Get all known chats, ordered by most recent activity.
|
* Get all known chats, ordered by most recent activity.
|
||||||
*/
|
*/
|
||||||
export function getAllChats(): ChatInfo[] {
|
export function getAllChats(): ChatInfo[] {
|
||||||
return db.prepare(`
|
return db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
SELECT jid, name, last_message_time
|
SELECT jid, name, last_message_time
|
||||||
FROM chats
|
FROM chats
|
||||||
ORDER BY last_message_time DESC
|
ORDER BY last_message_time DESC
|
||||||
`).all() as ChatInfo[];
|
`,
|
||||||
|
)
|
||||||
|
.all() as ChatInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -128,7 +150,9 @@ export function getAllChats(): ChatInfo[] {
|
|||||||
*/
|
*/
|
||||||
export function getLastGroupSync(): string | null {
|
export function getLastGroupSync(): string | null {
|
||||||
// Store sync time in a special chat entry
|
// Store sync time in a special chat entry
|
||||||
const row = db.prepare(`SELECT last_message_time FROM chats WHERE jid = '__group_sync__'`).get() as { last_message_time: string } | undefined;
|
const row = db
|
||||||
|
.prepare(`SELECT last_message_time FROM chats WHERE jid = '__group_sync__'`)
|
||||||
|
.get() as { last_message_time: string } | undefined;
|
||||||
return row?.last_message_time || null;
|
return row?.last_message_time || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,14 +161,21 @@ export function getLastGroupSync(): string | null {
|
|||||||
*/
|
*/
|
||||||
export function setLastGroupSync(): void {
|
export function setLastGroupSync(): void {
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
db.prepare(`INSERT OR REPLACE INTO chats (jid, name, last_message_time) VALUES ('__group_sync__', '__group_sync__', ?)`).run(now);
|
db.prepare(
|
||||||
|
`INSERT OR REPLACE INTO chats (jid, name, last_message_time) VALUES ('__group_sync__', '__group_sync__', ?)`,
|
||||||
|
).run(now);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a message with full content.
|
* Store a message with full content.
|
||||||
* Only call this for registered groups where message history is needed.
|
* Only call this for registered groups where message history is needed.
|
||||||
*/
|
*/
|
||||||
export function storeMessage(msg: proto.IWebMessageInfo, chatJid: string, isFromMe: boolean, pushName?: string): void {
|
export function storeMessage(
|
||||||
|
msg: proto.IWebMessageInfo,
|
||||||
|
chatJid: string,
|
||||||
|
isFromMe: boolean,
|
||||||
|
pushName?: string,
|
||||||
|
): void {
|
||||||
if (!msg.key) return;
|
if (!msg.key) return;
|
||||||
|
|
||||||
const content =
|
const content =
|
||||||
@@ -159,11 +190,24 @@ export function storeMessage(msg: proto.IWebMessageInfo, chatJid: string, isFrom
|
|||||||
const senderName = pushName || sender.split('@')[0];
|
const senderName = pushName || sender.split('@')[0];
|
||||||
const msgId = msg.key.id || '';
|
const msgId = msg.key.id || '';
|
||||||
|
|
||||||
db.prepare(`INSERT OR REPLACE INTO messages (id, chat_jid, sender, sender_name, content, timestamp, is_from_me) VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
db.prepare(
|
||||||
.run(msgId, chatJid, sender, senderName, content, timestamp, isFromMe ? 1 : 0);
|
`INSERT OR REPLACE INTO messages (id, chat_jid, sender, sender_name, content, timestamp, is_from_me) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
).run(
|
||||||
|
msgId,
|
||||||
|
chatJid,
|
||||||
|
sender,
|
||||||
|
senderName,
|
||||||
|
content,
|
||||||
|
timestamp,
|
||||||
|
isFromMe ? 1 : 0,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNewMessages(jids: string[], lastTimestamp: string, botPrefix: string): { messages: NewMessage[]; newTimestamp: string } {
|
export function getNewMessages(
|
||||||
|
jids: string[],
|
||||||
|
lastTimestamp: string,
|
||||||
|
botPrefix: string,
|
||||||
|
): { messages: NewMessage[]; newTimestamp: string } {
|
||||||
if (jids.length === 0) return { messages: [], newTimestamp: lastTimestamp };
|
if (jids.length === 0) return { messages: [], newTimestamp: lastTimestamp };
|
||||||
|
|
||||||
const placeholders = jids.map(() => '?').join(',');
|
const placeholders = jids.map(() => '?').join(',');
|
||||||
@@ -175,7 +219,9 @@ export function getNewMessages(jids: string[], lastTimestamp: string, botPrefix:
|
|||||||
ORDER BY timestamp
|
ORDER BY timestamp
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const rows = db.prepare(sql).all(lastTimestamp, ...jids, `${botPrefix}:%`) as NewMessage[];
|
const rows = db
|
||||||
|
.prepare(sql)
|
||||||
|
.all(lastTimestamp, ...jids, `${botPrefix}:%`) as NewMessage[];
|
||||||
|
|
||||||
let newTimestamp = lastTimestamp;
|
let newTimestamp = lastTimestamp;
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
@@ -185,7 +231,11 @@ export function getNewMessages(jids: string[], lastTimestamp: string, botPrefix:
|
|||||||
return { messages: rows, newTimestamp };
|
return { messages: rows, newTimestamp };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMessagesSince(chatJid: string, sinceTimestamp: string, botPrefix: string): NewMessage[] {
|
export function getMessagesSince(
|
||||||
|
chatJid: string,
|
||||||
|
sinceTimestamp: string,
|
||||||
|
botPrefix: string,
|
||||||
|
): NewMessage[] {
|
||||||
// Filter out bot's own messages by checking content prefix
|
// Filter out bot's own messages by checking content prefix
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT id, chat_jid, sender, sender_name, content, timestamp
|
SELECT id, chat_jid, sender, sender_name, content, timestamp
|
||||||
@@ -193,14 +243,20 @@ export function getMessagesSince(chatJid: string, sinceTimestamp: string, botPre
|
|||||||
WHERE chat_jid = ? AND timestamp > ? AND content NOT LIKE ?
|
WHERE chat_jid = ? AND timestamp > ? AND content NOT LIKE ?
|
||||||
ORDER BY timestamp
|
ORDER BY timestamp
|
||||||
`;
|
`;
|
||||||
return db.prepare(sql).all(chatJid, sinceTimestamp, `${botPrefix}:%`) as NewMessage[];
|
return db
|
||||||
|
.prepare(sql)
|
||||||
|
.all(chatJid, sinceTimestamp, `${botPrefix}:%`) as NewMessage[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createTask(task: Omit<ScheduledTask, 'last_run' | 'last_result'>): void {
|
export function createTask(
|
||||||
db.prepare(`
|
task: Omit<ScheduledTask, 'last_run' | 'last_result'>,
|
||||||
|
): void {
|
||||||
|
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, 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,
|
||||||
@@ -210,36 +266,69 @@ export function createTask(task: Omit<ScheduledTask, 'last_run' | 'last_result'>
|
|||||||
task.context_mode || 'isolated',
|
task.context_mode || 'isolated',
|
||||||
task.next_run,
|
task.next_run,
|
||||||
task.status,
|
task.status,
|
||||||
task.created_at
|
task.created_at,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTaskById(id: string): ScheduledTask | undefined {
|
export function getTaskById(id: string): ScheduledTask | undefined {
|
||||||
return db.prepare('SELECT * FROM scheduled_tasks WHERE id = ?').get(id) as ScheduledTask | undefined;
|
return db.prepare('SELECT * FROM scheduled_tasks WHERE id = ?').get(id) as
|
||||||
|
| ScheduledTask
|
||||||
|
| undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTasksForGroup(groupFolder: string): ScheduledTask[] {
|
export function getTasksForGroup(groupFolder: string): ScheduledTask[] {
|
||||||
return db.prepare('SELECT * FROM scheduled_tasks WHERE group_folder = ? ORDER BY created_at DESC').all(groupFolder) as ScheduledTask[];
|
return db
|
||||||
|
.prepare(
|
||||||
|
'SELECT * FROM scheduled_tasks WHERE group_folder = ? ORDER BY created_at DESC',
|
||||||
|
)
|
||||||
|
.all(groupFolder) as ScheduledTask[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAllTasks(): ScheduledTask[] {
|
export function getAllTasks(): ScheduledTask[] {
|
||||||
return db.prepare('SELECT * FROM scheduled_tasks ORDER BY created_at DESC').all() as ScheduledTask[];
|
return db
|
||||||
|
.prepare('SELECT * FROM scheduled_tasks ORDER BY created_at DESC')
|
||||||
|
.all() as ScheduledTask[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateTask(id: string, updates: Partial<Pick<ScheduledTask, 'prompt' | 'schedule_type' | 'schedule_value' | 'next_run' | 'status'>>): void {
|
export function updateTask(
|
||||||
|
id: string,
|
||||||
|
updates: Partial<
|
||||||
|
Pick<
|
||||||
|
ScheduledTask,
|
||||||
|
'prompt' | 'schedule_type' | 'schedule_value' | 'next_run' | 'status'
|
||||||
|
>
|
||||||
|
>,
|
||||||
|
): void {
|
||||||
const fields: string[] = [];
|
const fields: string[] = [];
|
||||||
const values: unknown[] = [];
|
const values: unknown[] = [];
|
||||||
|
|
||||||
if (updates.prompt !== undefined) { fields.push('prompt = ?'); values.push(updates.prompt); }
|
if (updates.prompt !== undefined) {
|
||||||
if (updates.schedule_type !== undefined) { fields.push('schedule_type = ?'); values.push(updates.schedule_type); }
|
fields.push('prompt = ?');
|
||||||
if (updates.schedule_value !== undefined) { fields.push('schedule_value = ?'); values.push(updates.schedule_value); }
|
values.push(updates.prompt);
|
||||||
if (updates.next_run !== undefined) { fields.push('next_run = ?'); values.push(updates.next_run); }
|
}
|
||||||
if (updates.status !== undefined) { fields.push('status = ?'); values.push(updates.status); }
|
if (updates.schedule_type !== undefined) {
|
||||||
|
fields.push('schedule_type = ?');
|
||||||
|
values.push(updates.schedule_type);
|
||||||
|
}
|
||||||
|
if (updates.schedule_value !== undefined) {
|
||||||
|
fields.push('schedule_value = ?');
|
||||||
|
values.push(updates.schedule_value);
|
||||||
|
}
|
||||||
|
if (updates.next_run !== undefined) {
|
||||||
|
fields.push('next_run = ?');
|
||||||
|
values.push(updates.next_run);
|
||||||
|
}
|
||||||
|
if (updates.status !== undefined) {
|
||||||
|
fields.push('status = ?');
|
||||||
|
values.push(updates.status);
|
||||||
|
}
|
||||||
|
|
||||||
if (fields.length === 0) return;
|
if (fields.length === 0) return;
|
||||||
|
|
||||||
values.push(id);
|
values.push(id);
|
||||||
db.prepare(`UPDATE scheduled_tasks SET ${fields.join(', ')} WHERE id = ?`).run(...values);
|
db.prepare(
|
||||||
|
`UPDATE scheduled_tasks SET ${fields.join(', ')} WHERE id = ?`,
|
||||||
|
).run(...values);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteTask(id: string): void {
|
export function deleteTask(id: string): void {
|
||||||
@@ -250,35 +339,58 @@ export function deleteTask(id: string): void {
|
|||||||
|
|
||||||
export function getDueTasks(): ScheduledTask[] {
|
export function getDueTasks(): ScheduledTask[] {
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
return db.prepare(`
|
return db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
SELECT * FROM scheduled_tasks
|
SELECT * FROM scheduled_tasks
|
||||||
WHERE status = 'active' AND next_run IS NOT NULL AND next_run <= ?
|
WHERE status = 'active' AND next_run IS NOT NULL AND next_run <= ?
|
||||||
ORDER BY next_run
|
ORDER BY next_run
|
||||||
`).all(now) as ScheduledTask[];
|
`,
|
||||||
|
)
|
||||||
|
.all(now) as ScheduledTask[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateTaskAfterRun(id: string, nextRun: string | null, lastResult: string): void {
|
export function updateTaskAfterRun(
|
||||||
|
id: string,
|
||||||
|
nextRun: string | null,
|
||||||
|
lastResult: string,
|
||||||
|
): void {
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
db.prepare(`
|
db.prepare(
|
||||||
|
`
|
||||||
UPDATE scheduled_tasks
|
UPDATE scheduled_tasks
|
||||||
SET next_run = ?, last_run = ?, last_result = ?, status = CASE WHEN ? IS NULL THEN 'completed' ELSE status END
|
SET next_run = ?, last_run = ?, last_result = ?, status = CASE WHEN ? IS NULL THEN 'completed' ELSE status END
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`).run(nextRun, now, lastResult, nextRun, id);
|
`,
|
||||||
|
).run(nextRun, now, lastResult, nextRun, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logTaskRun(log: TaskRunLog): void {
|
export function logTaskRun(log: TaskRunLog): void {
|
||||||
db.prepare(`
|
db.prepare(
|
||||||
|
`
|
||||||
INSERT INTO task_run_logs (task_id, run_at, duration_ms, status, result, error)
|
INSERT INTO task_run_logs (task_id, run_at, duration_ms, status, result, error)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
`).run(log.task_id, log.run_at, log.duration_ms, log.status, log.result, log.error);
|
`,
|
||||||
|
).run(
|
||||||
|
log.task_id,
|
||||||
|
log.run_at,
|
||||||
|
log.duration_ms,
|
||||||
|
log.status,
|
||||||
|
log.result,
|
||||||
|
log.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTaskRunLogs(taskId: string, limit = 10): TaskRunLog[] {
|
export function getTaskRunLogs(taskId: string, limit = 10): TaskRunLog[] {
|
||||||
return db.prepare(`
|
return db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
SELECT task_id, run_at, duration_ms, status, result, error
|
SELECT task_id, run_at, duration_ms, status, result, error
|
||||||
FROM task_run_logs
|
FROM task_run_logs
|
||||||
WHERE task_id = ?
|
WHERE task_id = ?
|
||||||
ORDER BY run_at DESC
|
ORDER BY run_at DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
`).all(taskId, limit) as TaskRunLog[];
|
`,
|
||||||
|
)
|
||||||
|
.all(taskId, limit) as TaskRunLog[];
|
||||||
}
|
}
|
||||||
|
|||||||
378
src/index.ts
378
src/index.ts
@@ -1,35 +1,53 @@
|
|||||||
import makeWASocket, {
|
|
||||||
useMultiFileAuthState,
|
|
||||||
DisconnectReason,
|
|
||||||
makeCacheableSignalKeyStore,
|
|
||||||
WASocket
|
|
||||||
} from '@whiskeysockets/baileys';
|
|
||||||
import pino from 'pino';
|
|
||||||
import { exec, execSync } from 'child_process';
|
import { exec, execSync } from 'child_process';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import pino from 'pino';
|
||||||
|
|
||||||
|
import makeWASocket, {
|
||||||
|
DisconnectReason,
|
||||||
|
WASocket,
|
||||||
|
makeCacheableSignalKeyStore,
|
||||||
|
useMultiFileAuthState,
|
||||||
|
} from '@whiskeysockets/baileys';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ASSISTANT_NAME,
|
ASSISTANT_NAME,
|
||||||
|
DATA_DIR,
|
||||||
|
IPC_POLL_INTERVAL,
|
||||||
|
MAIN_GROUP_FOLDER,
|
||||||
POLL_INTERVAL,
|
POLL_INTERVAL,
|
||||||
STORE_DIR,
|
STORE_DIR,
|
||||||
DATA_DIR,
|
TIMEZONE,
|
||||||
TRIGGER_PATTERN,
|
TRIGGER_PATTERN,
|
||||||
MAIN_GROUP_FOLDER,
|
|
||||||
IPC_POLL_INTERVAL,
|
|
||||||
TIMEZONE
|
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
import { RegisteredGroup, Session, NewMessage } from './types.js';
|
import {
|
||||||
import { initDatabase, storeMessage, storeChatMetadata, getNewMessages, getMessagesSince, getAllTasks, getTaskById, updateChatName, getAllChats, getLastGroupSync, setLastGroupSync } from './db.js';
|
AvailableGroup,
|
||||||
|
runContainerAgent,
|
||||||
|
writeGroupsSnapshot,
|
||||||
|
writeTasksSnapshot,
|
||||||
|
} from './container-runner.js';
|
||||||
|
import {
|
||||||
|
getAllChats,
|
||||||
|
getAllTasks,
|
||||||
|
getLastGroupSync,
|
||||||
|
getMessagesSince,
|
||||||
|
getNewMessages,
|
||||||
|
getTaskById,
|
||||||
|
initDatabase,
|
||||||
|
setLastGroupSync,
|
||||||
|
storeChatMetadata,
|
||||||
|
storeMessage,
|
||||||
|
updateChatName,
|
||||||
|
} from './db.js';
|
||||||
import { startSchedulerLoop } from './task-scheduler.js';
|
import { startSchedulerLoop } from './task-scheduler.js';
|
||||||
import { runContainerAgent, writeTasksSnapshot, writeGroupsSnapshot, AvailableGroup } from './container-runner.js';
|
import { NewMessage, RegisteredGroup, Session } from './types.js';
|
||||||
import { loadJson, saveJson } from './utils.js';
|
import { loadJson, saveJson } from './utils.js';
|
||||||
|
|
||||||
const GROUP_SYNC_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
const GROUP_SYNC_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||||
|
|
||||||
const logger = pino({
|
const logger = pino({
|
||||||
level: process.env.LOG_LEVEL || 'info',
|
level: process.env.LOG_LEVEL || 'info',
|
||||||
transport: { target: 'pino-pretty', options: { colorize: true } }
|
transport: { target: 'pino-pretty', options: { colorize: true } },
|
||||||
});
|
});
|
||||||
|
|
||||||
let sock: WASocket;
|
let sock: WASocket;
|
||||||
@@ -48,16 +66,28 @@ async function setTyping(jid: string, isTyping: boolean): Promise<void> {
|
|||||||
|
|
||||||
function loadState(): void {
|
function loadState(): void {
|
||||||
const statePath = path.join(DATA_DIR, 'router_state.json');
|
const statePath = path.join(DATA_DIR, 'router_state.json');
|
||||||
const state = loadJson<{ last_timestamp?: string; last_agent_timestamp?: Record<string, string> }>(statePath, {});
|
const state = loadJson<{
|
||||||
|
last_timestamp?: string;
|
||||||
|
last_agent_timestamp?: Record<string, string>;
|
||||||
|
}>(statePath, {});
|
||||||
lastTimestamp = state.last_timestamp || '';
|
lastTimestamp = state.last_timestamp || '';
|
||||||
lastAgentTimestamp = state.last_agent_timestamp || {};
|
lastAgentTimestamp = state.last_agent_timestamp || {};
|
||||||
sessions = loadJson(path.join(DATA_DIR, 'sessions.json'), {});
|
sessions = loadJson(path.join(DATA_DIR, 'sessions.json'), {});
|
||||||
registeredGroups = loadJson(path.join(DATA_DIR, 'registered_groups.json'), {});
|
registeredGroups = loadJson(
|
||||||
logger.info({ groupCount: Object.keys(registeredGroups).length }, 'State loaded');
|
path.join(DATA_DIR, 'registered_groups.json'),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
logger.info(
|
||||||
|
{ groupCount: Object.keys(registeredGroups).length },
|
||||||
|
'State loaded',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveState(): void {
|
function saveState(): void {
|
||||||
saveJson(path.join(DATA_DIR, 'router_state.json'), { last_timestamp: lastTimestamp, last_agent_timestamp: lastAgentTimestamp });
|
saveJson(path.join(DATA_DIR, 'router_state.json'), {
|
||||||
|
last_timestamp: lastTimestamp,
|
||||||
|
last_agent_timestamp: lastAgentTimestamp,
|
||||||
|
});
|
||||||
saveJson(path.join(DATA_DIR, 'sessions.json'), sessions);
|
saveJson(path.join(DATA_DIR, 'sessions.json'), sessions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +99,10 @@ function registerGroup(jid: string, group: RegisteredGroup): void {
|
|||||||
const groupDir = path.join(DATA_DIR, '..', 'groups', group.folder);
|
const groupDir = path.join(DATA_DIR, '..', 'groups', group.folder);
|
||||||
fs.mkdirSync(path.join(groupDir, 'logs'), { recursive: true });
|
fs.mkdirSync(path.join(groupDir, 'logs'), { recursive: true });
|
||||||
|
|
||||||
logger.info({ jid, name: group.name, folder: group.folder }, 'Group registered');
|
logger.info(
|
||||||
|
{ jid, name: group.name, folder: group.folder },
|
||||||
|
'Group registered',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -119,12 +152,12 @@ function getAvailableGroups(): AvailableGroup[] {
|
|||||||
const registeredJids = new Set(Object.keys(registeredGroups));
|
const registeredJids = new Set(Object.keys(registeredGroups));
|
||||||
|
|
||||||
return chats
|
return chats
|
||||||
.filter(c => c.jid !== '__group_sync__' && c.jid.endsWith('@g.us'))
|
.filter((c) => c.jid !== '__group_sync__' && c.jid.endsWith('@g.us'))
|
||||||
.map(c => ({
|
.map((c) => ({
|
||||||
jid: c.jid,
|
jid: c.jid,
|
||||||
name: c.name,
|
name: c.name,
|
||||||
lastActivity: c.last_message_time,
|
lastActivity: c.last_message_time,
|
||||||
isRegistered: registeredJids.has(c.jid)
|
isRegistered: registeredJids.has(c.jid),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,11 +173,16 @@ async function processMessage(msg: NewMessage): Promise<void> {
|
|||||||
|
|
||||||
// Get all messages since last agent interaction so the session has full context
|
// Get all messages since last agent interaction so the session has full context
|
||||||
const sinceTimestamp = lastAgentTimestamp[msg.chat_jid] || '';
|
const sinceTimestamp = lastAgentTimestamp[msg.chat_jid] || '';
|
||||||
const missedMessages = getMessagesSince(msg.chat_jid, sinceTimestamp, ASSISTANT_NAME);
|
const missedMessages = getMessagesSince(
|
||||||
|
msg.chat_jid,
|
||||||
|
sinceTimestamp,
|
||||||
|
ASSISTANT_NAME,
|
||||||
|
);
|
||||||
|
|
||||||
const lines = missedMessages.map(m => {
|
const lines = missedMessages.map((m) => {
|
||||||
// Escape XML special characters in content
|
// Escape XML special characters in content
|
||||||
const escapeXml = (s: string) => s
|
const escapeXml = (s: string) =>
|
||||||
|
s
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
.replace(/>/g, '>')
|
.replace(/>/g, '>')
|
||||||
@@ -155,7 +193,10 @@ async function processMessage(msg: NewMessage): Promise<void> {
|
|||||||
|
|
||||||
if (!prompt) return;
|
if (!prompt) return;
|
||||||
|
|
||||||
logger.info({ group: group.name, messageCount: missedMessages.length }, 'Processing message');
|
logger.info(
|
||||||
|
{ group: group.name, messageCount: missedMessages.length },
|
||||||
|
'Processing message',
|
||||||
|
);
|
||||||
|
|
||||||
await setTyping(msg.chat_jid, true);
|
await setTyping(msg.chat_jid, true);
|
||||||
const response = await runAgent(group, prompt, msg.chat_jid);
|
const response = await runAgent(group, prompt, msg.chat_jid);
|
||||||
@@ -167,25 +208,38 @@ async function processMessage(msg: NewMessage): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runAgent(group: RegisteredGroup, prompt: string, chatJid: string): Promise<string | null> {
|
async function runAgent(
|
||||||
|
group: RegisteredGroup,
|
||||||
|
prompt: string,
|
||||||
|
chatJid: string,
|
||||||
|
): Promise<string | null> {
|
||||||
const isMain = group.folder === MAIN_GROUP_FOLDER;
|
const isMain = group.folder === MAIN_GROUP_FOLDER;
|
||||||
const sessionId = sessions[group.folder];
|
const sessionId = sessions[group.folder];
|
||||||
|
|
||||||
// Update tasks snapshot for container to read (filtered by group)
|
// Update tasks snapshot for container to read (filtered by group)
|
||||||
const tasks = getAllTasks();
|
const tasks = getAllTasks();
|
||||||
writeTasksSnapshot(group.folder, isMain, tasks.map(t => ({
|
writeTasksSnapshot(
|
||||||
|
group.folder,
|
||||||
|
isMain,
|
||||||
|
tasks.map((t) => ({
|
||||||
id: t.id,
|
id: t.id,
|
||||||
groupFolder: t.group_folder,
|
groupFolder: t.group_folder,
|
||||||
prompt: t.prompt,
|
prompt: t.prompt,
|
||||||
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,
|
||||||
next_run: t.next_run
|
next_run: t.next_run,
|
||||||
})));
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
// Update available groups snapshot (main group only can see all groups)
|
// Update available groups snapshot (main group only can see all groups)
|
||||||
const availableGroups = getAvailableGroups();
|
const availableGroups = getAvailableGroups();
|
||||||
writeGroupsSnapshot(group.folder, isMain, availableGroups, new Set(Object.keys(registeredGroups)));
|
writeGroupsSnapshot(
|
||||||
|
group.folder,
|
||||||
|
isMain,
|
||||||
|
availableGroups,
|
||||||
|
new Set(Object.keys(registeredGroups)),
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const output = await runContainerAgent(group, {
|
const output = await runContainerAgent(group, {
|
||||||
@@ -193,7 +247,7 @@ async function runAgent(group: RegisteredGroup, prompt: string, chatJid: string)
|
|||||||
sessionId,
|
sessionId,
|
||||||
groupFolder: group.folder,
|
groupFolder: group.folder,
|
||||||
chatJid,
|
chatJid,
|
||||||
isMain
|
isMain,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (output.newSessionId) {
|
if (output.newSessionId) {
|
||||||
@@ -202,7 +256,10 @@ async function runAgent(group: RegisteredGroup, prompt: string, chatJid: string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (output.status === 'error') {
|
if (output.status === 'error') {
|
||||||
logger.error({ group: group.name, error: output.error }, 'Container agent error');
|
logger.error(
|
||||||
|
{ group: group.name, error: output.error },
|
||||||
|
'Container agent error',
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +287,7 @@ function startIpcWatcher(): void {
|
|||||||
// Scan all group IPC directories (identity determined by directory)
|
// Scan all group IPC directories (identity determined by directory)
|
||||||
let groupFolders: string[];
|
let groupFolders: string[];
|
||||||
try {
|
try {
|
||||||
groupFolders = fs.readdirSync(ipcBaseDir).filter(f => {
|
groupFolders = fs.readdirSync(ipcBaseDir).filter((f) => {
|
||||||
const stat = fs.statSync(path.join(ipcBaseDir, f));
|
const stat = fs.statSync(path.join(ipcBaseDir, f));
|
||||||
return stat.isDirectory() && f !== 'errors';
|
return stat.isDirectory() && f !== 'errors';
|
||||||
});
|
});
|
||||||
@@ -248,7 +305,9 @@ function startIpcWatcher(): void {
|
|||||||
// Process messages from this group's IPC directory
|
// Process messages from this group's IPC directory
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(messagesDir)) {
|
if (fs.existsSync(messagesDir)) {
|
||||||
const messageFiles = fs.readdirSync(messagesDir).filter(f => f.endsWith('.json'));
|
const messageFiles = fs
|
||||||
|
.readdirSync(messagesDir)
|
||||||
|
.filter((f) => f.endsWith('.json'));
|
||||||
for (const file of messageFiles) {
|
for (const file of messageFiles) {
|
||||||
const filePath = path.join(messagesDir, file);
|
const filePath = path.join(messagesDir, file);
|
||||||
try {
|
try {
|
||||||
@@ -256,30 +315,53 @@ function startIpcWatcher(): void {
|
|||||||
if (data.type === 'message' && data.chatJid && data.text) {
|
if (data.type === 'message' && data.chatJid && data.text) {
|
||||||
// Authorization: verify this group can send to this chatJid
|
// Authorization: verify this group can send to this chatJid
|
||||||
const targetGroup = registeredGroups[data.chatJid];
|
const targetGroup = registeredGroups[data.chatJid];
|
||||||
if (isMain || (targetGroup && targetGroup.folder === sourceGroup)) {
|
if (
|
||||||
await sendMessage(data.chatJid, `${ASSISTANT_NAME}: ${data.text}`);
|
isMain ||
|
||||||
logger.info({ chatJid: data.chatJid, sourceGroup }, 'IPC message sent');
|
(targetGroup && targetGroup.folder === sourceGroup)
|
||||||
|
) {
|
||||||
|
await sendMessage(
|
||||||
|
data.chatJid,
|
||||||
|
`${ASSISTANT_NAME}: ${data.text}`,
|
||||||
|
);
|
||||||
|
logger.info(
|
||||||
|
{ chatJid: data.chatJid, sourceGroup },
|
||||||
|
'IPC message sent',
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
logger.warn({ chatJid: data.chatJid, sourceGroup }, 'Unauthorized IPC message attempt blocked');
|
logger.warn(
|
||||||
|
{ chatJid: data.chatJid, sourceGroup },
|
||||||
|
'Unauthorized IPC message attempt blocked',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error({ file, sourceGroup, err }, 'Error processing IPC message');
|
logger.error(
|
||||||
|
{ file, sourceGroup, err },
|
||||||
|
'Error processing IPC message',
|
||||||
|
);
|
||||||
const errorDir = path.join(ipcBaseDir, 'errors');
|
const errorDir = path.join(ipcBaseDir, 'errors');
|
||||||
fs.mkdirSync(errorDir, { recursive: true });
|
fs.mkdirSync(errorDir, { recursive: true });
|
||||||
fs.renameSync(filePath, path.join(errorDir, `${sourceGroup}-${file}`));
|
fs.renameSync(
|
||||||
|
filePath,
|
||||||
|
path.join(errorDir, `${sourceGroup}-${file}`),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error({ err, sourceGroup }, 'Error reading IPC messages directory');
|
logger.error(
|
||||||
|
{ err, sourceGroup },
|
||||||
|
'Error reading IPC messages directory',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process tasks from this group's IPC directory
|
// Process tasks from this group's IPC directory
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(tasksDir)) {
|
if (fs.existsSync(tasksDir)) {
|
||||||
const taskFiles = fs.readdirSync(tasksDir).filter(f => f.endsWith('.json'));
|
const taskFiles = fs
|
||||||
|
.readdirSync(tasksDir)
|
||||||
|
.filter((f) => f.endsWith('.json'));
|
||||||
for (const file of taskFiles) {
|
for (const file of taskFiles) {
|
||||||
const filePath = path.join(tasksDir, file);
|
const filePath = path.join(tasksDir, file);
|
||||||
try {
|
try {
|
||||||
@@ -288,10 +370,16 @@ function startIpcWatcher(): void {
|
|||||||
await processTaskIpc(data, sourceGroup, isMain);
|
await processTaskIpc(data, sourceGroup, isMain);
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error({ file, sourceGroup, err }, 'Error processing IPC task');
|
logger.error(
|
||||||
|
{ file, sourceGroup, err },
|
||||||
|
'Error processing IPC task',
|
||||||
|
);
|
||||||
const errorDir = path.join(ipcBaseDir, 'errors');
|
const errorDir = path.join(ipcBaseDir, 'errors');
|
||||||
fs.mkdirSync(errorDir, { recursive: true });
|
fs.mkdirSync(errorDir, { recursive: true });
|
||||||
fs.renameSync(filePath, path.join(errorDir, `${sourceGroup}-${file}`));
|
fs.renameSync(
|
||||||
|
filePath,
|
||||||
|
path.join(errorDir, `${sourceGroup}-${file}`),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -325,29 +413,45 @@ async function processTaskIpc(
|
|||||||
containerConfig?: RegisteredGroup['containerConfig'];
|
containerConfig?: RegisteredGroup['containerConfig'];
|
||||||
},
|
},
|
||||||
sourceGroup: string, // Verified identity from IPC directory
|
sourceGroup: string, // Verified identity from IPC directory
|
||||||
isMain: boolean // Verified from directory path
|
isMain: boolean, // Verified from directory path
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Import db functions dynamically to avoid circular deps
|
// Import db functions dynamically to avoid circular deps
|
||||||
const { createTask, updateTask, deleteTask, getTaskById: getTask } = await import('./db.js');
|
const {
|
||||||
|
createTask,
|
||||||
|
updateTask,
|
||||||
|
deleteTask,
|
||||||
|
getTaskById: getTask,
|
||||||
|
} = await import('./db.js');
|
||||||
const { CronExpressionParser } = await import('cron-parser');
|
const { CronExpressionParser } = await import('cron-parser');
|
||||||
|
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case 'schedule_task':
|
case 'schedule_task':
|
||||||
if (data.prompt && data.schedule_type && data.schedule_value && data.groupFolder) {
|
if (
|
||||||
|
data.prompt &&
|
||||||
|
data.schedule_type &&
|
||||||
|
data.schedule_value &&
|
||||||
|
data.groupFolder
|
||||||
|
) {
|
||||||
// Authorization: non-main groups can only schedule for themselves
|
// Authorization: non-main groups can only schedule for themselves
|
||||||
const targetGroup = data.groupFolder;
|
const targetGroup = data.groupFolder;
|
||||||
if (!isMain && targetGroup !== sourceGroup) {
|
if (!isMain && targetGroup !== sourceGroup) {
|
||||||
logger.warn({ sourceGroup, targetGroup }, 'Unauthorized schedule_task attempt blocked');
|
logger.warn(
|
||||||
|
{ sourceGroup, targetGroup },
|
||||||
|
'Unauthorized schedule_task attempt blocked',
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve the correct JID for the target group (don't trust IPC payload)
|
// Resolve the correct JID for the target group (don't trust IPC payload)
|
||||||
const targetJid = Object.entries(registeredGroups).find(
|
const targetJid = Object.entries(registeredGroups).find(
|
||||||
([, group]) => group.folder === targetGroup
|
([, group]) => group.folder === targetGroup,
|
||||||
)?.[0];
|
)?.[0];
|
||||||
|
|
||||||
if (!targetJid) {
|
if (!targetJid) {
|
||||||
logger.warn({ targetGroup }, 'Cannot schedule task: target group not registered');
|
logger.warn(
|
||||||
|
{ targetGroup },
|
||||||
|
'Cannot schedule task: target group not registered',
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,30 +460,42 @@ async function processTaskIpc(
|
|||||||
let nextRun: string | null = null;
|
let nextRun: string | null = null;
|
||||||
if (scheduleType === 'cron') {
|
if (scheduleType === 'cron') {
|
||||||
try {
|
try {
|
||||||
const interval = CronExpressionParser.parse(data.schedule_value, { tz: TIMEZONE });
|
const interval = CronExpressionParser.parse(data.schedule_value, {
|
||||||
|
tz: TIMEZONE,
|
||||||
|
});
|
||||||
nextRun = interval.next().toISOString();
|
nextRun = interval.next().toISOString();
|
||||||
} catch {
|
} catch {
|
||||||
logger.warn({ scheduleValue: data.schedule_value }, 'Invalid cron expression');
|
logger.warn(
|
||||||
|
{ scheduleValue: data.schedule_value },
|
||||||
|
'Invalid cron expression',
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else if (scheduleType === 'interval') {
|
} else if (scheduleType === 'interval') {
|
||||||
const ms = parseInt(data.schedule_value, 10);
|
const ms = parseInt(data.schedule_value, 10);
|
||||||
if (isNaN(ms) || ms <= 0) {
|
if (isNaN(ms) || ms <= 0) {
|
||||||
logger.warn({ scheduleValue: data.schedule_value }, 'Invalid interval');
|
logger.warn(
|
||||||
|
{ scheduleValue: data.schedule_value },
|
||||||
|
'Invalid interval',
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
nextRun = new Date(Date.now() + ms).toISOString();
|
nextRun = new Date(Date.now() + ms).toISOString();
|
||||||
} else if (scheduleType === 'once') {
|
} else if (scheduleType === 'once') {
|
||||||
const scheduled = new Date(data.schedule_value);
|
const scheduled = new Date(data.schedule_value);
|
||||||
if (isNaN(scheduled.getTime())) {
|
if (isNaN(scheduled.getTime())) {
|
||||||
logger.warn({ scheduleValue: data.schedule_value }, 'Invalid timestamp');
|
logger.warn(
|
||||||
|
{ scheduleValue: data.schedule_value },
|
||||||
|
'Invalid timestamp',
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
nextRun = scheduled.toISOString();
|
nextRun = scheduled.toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
const contextMode = (data.context_mode === 'group' || data.context_mode === 'isolated')
|
const contextMode =
|
||||||
|
data.context_mode === 'group' || data.context_mode === 'isolated'
|
||||||
? data.context_mode
|
? data.context_mode
|
||||||
: 'isolated';
|
: 'isolated';
|
||||||
createTask({
|
createTask({
|
||||||
@@ -392,9 +508,12 @@ async function processTaskIpc(
|
|||||||
context_mode: contextMode,
|
context_mode: contextMode,
|
||||||
next_run: nextRun,
|
next_run: nextRun,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
created_at: new Date().toISOString()
|
created_at: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
logger.info({ taskId, sourceGroup, targetGroup, contextMode }, 'Task created via IPC');
|
logger.info(
|
||||||
|
{ taskId, sourceGroup, targetGroup, contextMode },
|
||||||
|
'Task created via IPC',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -403,9 +522,15 @@ async function processTaskIpc(
|
|||||||
const task = getTask(data.taskId);
|
const task = getTask(data.taskId);
|
||||||
if (task && (isMain || task.group_folder === sourceGroup)) {
|
if (task && (isMain || task.group_folder === sourceGroup)) {
|
||||||
updateTask(data.taskId, { status: 'paused' });
|
updateTask(data.taskId, { status: 'paused' });
|
||||||
logger.info({ taskId: data.taskId, sourceGroup }, 'Task paused via IPC');
|
logger.info(
|
||||||
|
{ taskId: data.taskId, sourceGroup },
|
||||||
|
'Task paused via IPC',
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
logger.warn({ taskId: data.taskId, sourceGroup }, 'Unauthorized task pause attempt');
|
logger.warn(
|
||||||
|
{ taskId: data.taskId, sourceGroup },
|
||||||
|
'Unauthorized task pause attempt',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -415,9 +540,15 @@ async function processTaskIpc(
|
|||||||
const task = getTask(data.taskId);
|
const task = getTask(data.taskId);
|
||||||
if (task && (isMain || task.group_folder === sourceGroup)) {
|
if (task && (isMain || task.group_folder === sourceGroup)) {
|
||||||
updateTask(data.taskId, { status: 'active' });
|
updateTask(data.taskId, { status: 'active' });
|
||||||
logger.info({ taskId: data.taskId, sourceGroup }, 'Task resumed via IPC');
|
logger.info(
|
||||||
|
{ taskId: data.taskId, sourceGroup },
|
||||||
|
'Task resumed via IPC',
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
logger.warn({ taskId: data.taskId, sourceGroup }, 'Unauthorized task resume attempt');
|
logger.warn(
|
||||||
|
{ taskId: data.taskId, sourceGroup },
|
||||||
|
'Unauthorized task resume attempt',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -427,9 +558,15 @@ async function processTaskIpc(
|
|||||||
const task = getTask(data.taskId);
|
const task = getTask(data.taskId);
|
||||||
if (task && (isMain || task.group_folder === sourceGroup)) {
|
if (task && (isMain || task.group_folder === sourceGroup)) {
|
||||||
deleteTask(data.taskId);
|
deleteTask(data.taskId);
|
||||||
logger.info({ taskId: data.taskId, sourceGroup }, 'Task cancelled via IPC');
|
logger.info(
|
||||||
|
{ taskId: data.taskId, sourceGroup },
|
||||||
|
'Task cancelled via IPC',
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
logger.warn({ taskId: data.taskId, sourceGroup }, 'Unauthorized task cancel attempt');
|
logger.warn(
|
||||||
|
{ taskId: data.taskId, sourceGroup },
|
||||||
|
'Unauthorized task cancel attempt',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -437,21 +574,36 @@ async function processTaskIpc(
|
|||||||
case 'refresh_groups':
|
case 'refresh_groups':
|
||||||
// Only main group can request a refresh
|
// Only main group can request a refresh
|
||||||
if (isMain) {
|
if (isMain) {
|
||||||
logger.info({ sourceGroup }, 'Group metadata refresh requested via IPC');
|
logger.info(
|
||||||
|
{ sourceGroup },
|
||||||
|
'Group metadata refresh requested via IPC',
|
||||||
|
);
|
||||||
await syncGroupMetadata(true);
|
await syncGroupMetadata(true);
|
||||||
// Write updated snapshot immediately
|
// Write updated snapshot immediately
|
||||||
const availableGroups = getAvailableGroups();
|
const availableGroups = getAvailableGroups();
|
||||||
const { writeGroupsSnapshot: writeGroups } = await import('./container-runner.js');
|
const { writeGroupsSnapshot: writeGroups } =
|
||||||
writeGroups(sourceGroup, true, availableGroups, new Set(Object.keys(registeredGroups)));
|
await import('./container-runner.js');
|
||||||
|
writeGroups(
|
||||||
|
sourceGroup,
|
||||||
|
true,
|
||||||
|
availableGroups,
|
||||||
|
new Set(Object.keys(registeredGroups)),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
logger.warn({ sourceGroup }, 'Unauthorized refresh_groups attempt blocked');
|
logger.warn(
|
||||||
|
{ sourceGroup },
|
||||||
|
'Unauthorized refresh_groups attempt blocked',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'register_group':
|
case 'register_group':
|
||||||
// Only main group can register new groups
|
// Only main group can register new groups
|
||||||
if (!isMain) {
|
if (!isMain) {
|
||||||
logger.warn({ sourceGroup }, 'Unauthorized register_group attempt blocked');
|
logger.warn(
|
||||||
|
{ sourceGroup },
|
||||||
|
'Unauthorized register_group attempt blocked',
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (data.jid && data.name && data.folder && data.trigger) {
|
if (data.jid && data.name && data.folder && data.trigger) {
|
||||||
@@ -460,10 +612,13 @@ async function processTaskIpc(
|
|||||||
folder: data.folder,
|
folder: data.folder,
|
||||||
trigger: data.trigger,
|
trigger: data.trigger,
|
||||||
added_at: new Date().toISOString(),
|
added_at: new Date().toISOString(),
|
||||||
containerConfig: data.containerConfig
|
containerConfig: data.containerConfig,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.warn({ data }, 'Invalid register_group request - missing required fields');
|
logger.warn(
|
||||||
|
{ data },
|
||||||
|
'Invalid register_group request - missing required fields',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -479,19 +634,25 @@ async function connectWhatsApp(): Promise<void> {
|
|||||||
const { state, saveCreds } = await useMultiFileAuthState(authDir);
|
const { state, saveCreds } = await useMultiFileAuthState(authDir);
|
||||||
|
|
||||||
sock = makeWASocket({
|
sock = makeWASocket({
|
||||||
auth: { creds: state.creds, keys: makeCacheableSignalKeyStore(state.keys, logger) },
|
auth: {
|
||||||
|
creds: state.creds,
|
||||||
|
keys: makeCacheableSignalKeyStore(state.keys, logger),
|
||||||
|
},
|
||||||
printQRInTerminal: false,
|
printQRInTerminal: false,
|
||||||
logger,
|
logger,
|
||||||
browser: ['NanoClaw', 'Chrome', '1.0.0']
|
browser: ['NanoClaw', 'Chrome', '1.0.0'],
|
||||||
});
|
});
|
||||||
|
|
||||||
sock.ev.on('connection.update', (update) => {
|
sock.ev.on('connection.update', (update) => {
|
||||||
const { connection, lastDisconnect, qr } = update;
|
const { connection, lastDisconnect, qr } = update;
|
||||||
|
|
||||||
if (qr) {
|
if (qr) {
|
||||||
const msg = 'WhatsApp authentication required. Run /setup in Claude Code.';
|
const msg =
|
||||||
|
'WhatsApp authentication required. Run /setup in Claude Code.';
|
||||||
logger.error(msg);
|
logger.error(msg);
|
||||||
exec(`osascript -e 'display notification "${msg}" with title "NanoClaw" sound name "Basso"'`);
|
exec(
|
||||||
|
`osascript -e 'display notification "${msg}" with title "NanoClaw" sound name "Basso"'`,
|
||||||
|
);
|
||||||
setTimeout(() => process.exit(1), 1000);
|
setTimeout(() => process.exit(1), 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,15 +671,19 @@ async function connectWhatsApp(): Promise<void> {
|
|||||||
} else if (connection === 'open') {
|
} else if (connection === 'open') {
|
||||||
logger.info('Connected to WhatsApp');
|
logger.info('Connected to WhatsApp');
|
||||||
// Sync group metadata on startup (respects 24h cache)
|
// Sync group metadata on startup (respects 24h cache)
|
||||||
syncGroupMetadata().catch(err => logger.error({ err }, 'Initial group sync failed'));
|
syncGroupMetadata().catch((err) =>
|
||||||
|
logger.error({ err }, 'Initial group sync failed'),
|
||||||
|
);
|
||||||
// Set up daily sync timer
|
// Set up daily sync timer
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
syncGroupMetadata().catch(err => logger.error({ err }, 'Periodic group sync failed'));
|
syncGroupMetadata().catch((err) =>
|
||||||
|
logger.error({ err }, 'Periodic group sync failed'),
|
||||||
|
);
|
||||||
}, GROUP_SYNC_INTERVAL_MS);
|
}, GROUP_SYNC_INTERVAL_MS);
|
||||||
startSchedulerLoop({
|
startSchedulerLoop({
|
||||||
sendMessage,
|
sendMessage,
|
||||||
registeredGroups: () => registeredGroups,
|
registeredGroups: () => registeredGroups,
|
||||||
getSessions: () => sessions
|
getSessions: () => sessions,
|
||||||
});
|
});
|
||||||
startIpcWatcher();
|
startIpcWatcher();
|
||||||
startMessageLoop();
|
startMessageLoop();
|
||||||
@@ -533,14 +698,21 @@ async function connectWhatsApp(): Promise<void> {
|
|||||||
const chatJid = msg.key.remoteJid;
|
const chatJid = msg.key.remoteJid;
|
||||||
if (!chatJid || chatJid === 'status@broadcast') continue;
|
if (!chatJid || chatJid === 'status@broadcast') continue;
|
||||||
|
|
||||||
const timestamp = new Date(Number(msg.messageTimestamp) * 1000).toISOString();
|
const timestamp = new Date(
|
||||||
|
Number(msg.messageTimestamp) * 1000,
|
||||||
|
).toISOString();
|
||||||
|
|
||||||
// Always store chat metadata for group discovery
|
// Always store chat metadata for group discovery
|
||||||
storeChatMetadata(chatJid, timestamp);
|
storeChatMetadata(chatJid, timestamp);
|
||||||
|
|
||||||
// Only store full message content for registered groups
|
// Only store full message content for registered groups
|
||||||
if (registeredGroups[chatJid]) {
|
if (registeredGroups[chatJid]) {
|
||||||
storeMessage(msg, chatJid, msg.key.fromMe || false, msg.pushName || undefined);
|
storeMessage(
|
||||||
|
msg,
|
||||||
|
chatJid,
|
||||||
|
msg.key.fromMe || false,
|
||||||
|
msg.pushName || undefined,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -554,7 +726,8 @@ async function startMessageLoop(): Promise<void> {
|
|||||||
const jids = Object.keys(registeredGroups);
|
const jids = Object.keys(registeredGroups);
|
||||||
const { messages } = getNewMessages(jids, lastTimestamp, ASSISTANT_NAME);
|
const { messages } = getNewMessages(jids, lastTimestamp, ASSISTANT_NAME);
|
||||||
|
|
||||||
if (messages.length > 0) logger.info({ count: messages.length }, 'New messages');
|
if (messages.length > 0)
|
||||||
|
logger.info({ count: messages.length }, 'New messages');
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
try {
|
try {
|
||||||
await processMessage(msg);
|
await processMessage(msg);
|
||||||
@@ -562,7 +735,10 @@ async function startMessageLoop(): Promise<void> {
|
|||||||
lastTimestamp = msg.timestamp;
|
lastTimestamp = msg.timestamp;
|
||||||
saveState();
|
saveState();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error({ err, msg: msg.id }, 'Error processing message, will retry');
|
logger.error(
|
||||||
|
{ err, msg: msg.id },
|
||||||
|
'Error processing message, will retry',
|
||||||
|
);
|
||||||
// Stop processing this batch - failed message will be retried next loop
|
// Stop processing this batch - failed message will be retried next loop
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -570,7 +746,7 @@ async function startMessageLoop(): Promise<void> {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error({ err }, 'Error in message loop');
|
logger.error({ err }, 'Error in message loop');
|
||||||
}
|
}
|
||||||
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL));
|
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,14 +761,30 @@ function ensureContainerSystemRunning(): void {
|
|||||||
logger.info('Apple Container system started');
|
logger.info('Apple Container system started');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error({ err }, 'Failed to start Apple Container system');
|
logger.error({ err }, 'Failed to start Apple Container system');
|
||||||
console.error('\n╔════════════════════════════════════════════════════════════════╗');
|
console.error(
|
||||||
console.error('║ FATAL: Apple Container system failed to start ║');
|
'\n╔════════════════════════════════════════════════════════════════╗',
|
||||||
console.error('║ ║');
|
);
|
||||||
console.error('║ Agents cannot run without Apple Container. To fix: ║');
|
console.error(
|
||||||
console.error('║ 1. Install from: https://github.com/apple/container/releases ║');
|
'║ FATAL: Apple Container system failed to start ║',
|
||||||
console.error('║ 2. Run: container system start ║');
|
);
|
||||||
console.error('║ 3. Restart NanoClaw ║');
|
console.error(
|
||||||
console.error('╚════════════════════════════════════════════════════════════════╝\n');
|
'║ ║',
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
'║ Agents cannot run without Apple Container. To fix: ║',
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
'║ 1. Install from: https://github.com/apple/container/releases ║',
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
'║ 2. Run: container system start ║',
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
'║ 3. Restart NanoClaw ║',
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
'╚════════════════════════════════════════════════════════════════╝\n',
|
||||||
|
);
|
||||||
throw new Error('Apple Container system is required but failed to start');
|
throw new Error('Apple Container system is required but failed to start');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -606,7 +798,7 @@ async function main(): Promise<void> {
|
|||||||
await connectWhatsApp();
|
await connectWhatsApp();
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch(err => {
|
main().catch((err) => {
|
||||||
logger.error({ err }, 'Failed to start NanoClaw');
|
logger.error({ err }, 'Failed to start NanoClaw');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,16 +6,16 @@
|
|||||||
*
|
*
|
||||||
* Allowlist location: ~/.config/nanoclaw/mount-allowlist.json
|
* Allowlist location: ~/.config/nanoclaw/mount-allowlist.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import pino from 'pino';
|
import pino from 'pino';
|
||||||
|
|
||||||
import { MOUNT_ALLOWLIST_PATH } from './config.js';
|
import { MOUNT_ALLOWLIST_PATH } from './config.js';
|
||||||
import { AdditionalMount, MountAllowlist, AllowedRoot } from './types.js';
|
import { AdditionalMount, AllowedRoot, MountAllowlist } from './types.js';
|
||||||
|
|
||||||
const logger = pino({
|
const logger = pino({
|
||||||
level: process.env.LOG_LEVEL || 'info',
|
level: process.env.LOG_LEVEL || 'info',
|
||||||
transport: { target: 'pino-pretty', options: { colorize: true } }
|
transport: { target: 'pino-pretty', options: { colorize: true } },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cache the allowlist in memory - only reloads on process restart
|
// Cache the allowlist in memory - only reloads on process restart
|
||||||
@@ -63,9 +63,11 @@ export function loadMountAllowlist(): MountAllowlist | null {
|
|||||||
try {
|
try {
|
||||||
if (!fs.existsSync(MOUNT_ALLOWLIST_PATH)) {
|
if (!fs.existsSync(MOUNT_ALLOWLIST_PATH)) {
|
||||||
allowlistLoadError = `Mount allowlist not found at ${MOUNT_ALLOWLIST_PATH}`;
|
allowlistLoadError = `Mount allowlist not found at ${MOUNT_ALLOWLIST_PATH}`;
|
||||||
logger.warn({ path: MOUNT_ALLOWLIST_PATH },
|
logger.warn(
|
||||||
|
{ path: MOUNT_ALLOWLIST_PATH },
|
||||||
'Mount allowlist not found - additional mounts will be BLOCKED. ' +
|
'Mount allowlist not found - additional mounts will be BLOCKED. ' +
|
||||||
'Create the file to enable additional mounts.');
|
'Create the file to enable additional mounts.',
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,24 +89,30 @@ export function loadMountAllowlist(): MountAllowlist | null {
|
|||||||
|
|
||||||
// Merge with default blocked patterns
|
// Merge with default blocked patterns
|
||||||
const mergedBlockedPatterns = [
|
const mergedBlockedPatterns = [
|
||||||
...new Set([...DEFAULT_BLOCKED_PATTERNS, ...allowlist.blockedPatterns])
|
...new Set([...DEFAULT_BLOCKED_PATTERNS, ...allowlist.blockedPatterns]),
|
||||||
];
|
];
|
||||||
allowlist.blockedPatterns = mergedBlockedPatterns;
|
allowlist.blockedPatterns = mergedBlockedPatterns;
|
||||||
|
|
||||||
cachedAllowlist = allowlist;
|
cachedAllowlist = allowlist;
|
||||||
logger.info({
|
logger.info(
|
||||||
|
{
|
||||||
path: MOUNT_ALLOWLIST_PATH,
|
path: MOUNT_ALLOWLIST_PATH,
|
||||||
allowedRoots: allowlist.allowedRoots.length,
|
allowedRoots: allowlist.allowedRoots.length,
|
||||||
blockedPatterns: allowlist.blockedPatterns.length
|
blockedPatterns: allowlist.blockedPatterns.length,
|
||||||
}, 'Mount allowlist loaded successfully');
|
},
|
||||||
|
'Mount allowlist loaded successfully',
|
||||||
|
);
|
||||||
|
|
||||||
return cachedAllowlist;
|
return cachedAllowlist;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
allowlistLoadError = err instanceof Error ? err.message : String(err);
|
allowlistLoadError = err instanceof Error ? err.message : String(err);
|
||||||
logger.error({
|
logger.error(
|
||||||
|
{
|
||||||
path: MOUNT_ALLOWLIST_PATH,
|
path: MOUNT_ALLOWLIST_PATH,
|
||||||
error: allowlistLoadError
|
error: allowlistLoadError,
|
||||||
}, 'Failed to load mount allowlist - additional mounts will be BLOCKED');
|
},
|
||||||
|
'Failed to load mount allowlist - additional mounts will be BLOCKED',
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,7 +146,10 @@ function getRealPath(p: string): string | null {
|
|||||||
/**
|
/**
|
||||||
* Check if a path matches any blocked pattern
|
* Check if a path matches any blocked pattern
|
||||||
*/
|
*/
|
||||||
function matchesBlockedPattern(realPath: string, blockedPatterns: string[]): string | null {
|
function matchesBlockedPattern(
|
||||||
|
realPath: string,
|
||||||
|
blockedPatterns: string[],
|
||||||
|
): string | null {
|
||||||
const pathParts = realPath.split(path.sep);
|
const pathParts = realPath.split(path.sep);
|
||||||
|
|
||||||
for (const pattern of blockedPatterns) {
|
for (const pattern of blockedPatterns) {
|
||||||
@@ -161,7 +172,10 @@ function matchesBlockedPattern(realPath: string, blockedPatterns: string[]): str
|
|||||||
/**
|
/**
|
||||||
* Check if a real path is under an allowed root
|
* Check if a real path is under an allowed root
|
||||||
*/
|
*/
|
||||||
function findAllowedRoot(realPath: string, allowedRoots: AllowedRoot[]): AllowedRoot | null {
|
function findAllowedRoot(
|
||||||
|
realPath: string,
|
||||||
|
allowedRoots: AllowedRoot[],
|
||||||
|
): AllowedRoot | null {
|
||||||
for (const root of allowedRoots) {
|
for (const root of allowedRoots) {
|
||||||
const expandedRoot = expandPath(root.path);
|
const expandedRoot = expandPath(root.path);
|
||||||
const realRoot = getRealPath(expandedRoot);
|
const realRoot = getRealPath(expandedRoot);
|
||||||
@@ -216,7 +230,7 @@ export interface MountValidationResult {
|
|||||||
*/
|
*/
|
||||||
export function validateMount(
|
export function validateMount(
|
||||||
mount: AdditionalMount,
|
mount: AdditionalMount,
|
||||||
isMain: boolean
|
isMain: boolean,
|
||||||
): MountValidationResult {
|
): MountValidationResult {
|
||||||
const allowlist = loadMountAllowlist();
|
const allowlist = loadMountAllowlist();
|
||||||
|
|
||||||
@@ -224,7 +238,7 @@ export function validateMount(
|
|||||||
if (allowlist === null) {
|
if (allowlist === null) {
|
||||||
return {
|
return {
|
||||||
allowed: false,
|
allowed: false,
|
||||||
reason: `No mount allowlist configured at ${MOUNT_ALLOWLIST_PATH}`
|
reason: `No mount allowlist configured at ${MOUNT_ALLOWLIST_PATH}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,7 +246,7 @@ export function validateMount(
|
|||||||
if (!isValidContainerPath(mount.containerPath)) {
|
if (!isValidContainerPath(mount.containerPath)) {
|
||||||
return {
|
return {
|
||||||
allowed: false,
|
allowed: false,
|
||||||
reason: `Invalid container path: "${mount.containerPath}" - must be relative, non-empty, and not contain ".."`
|
reason: `Invalid container path: "${mount.containerPath}" - must be relative, non-empty, and not contain ".."`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,16 +257,19 @@ export function validateMount(
|
|||||||
if (realPath === null) {
|
if (realPath === null) {
|
||||||
return {
|
return {
|
||||||
allowed: false,
|
allowed: false,
|
||||||
reason: `Host path does not exist: "${mount.hostPath}" (expanded: "${expandedPath}")`
|
reason: `Host path does not exist: "${mount.hostPath}" (expanded: "${expandedPath}")`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check against blocked patterns
|
// Check against blocked patterns
|
||||||
const blockedMatch = matchesBlockedPattern(realPath, allowlist.blockedPatterns);
|
const blockedMatch = matchesBlockedPattern(
|
||||||
|
realPath,
|
||||||
|
allowlist.blockedPatterns,
|
||||||
|
);
|
||||||
if (blockedMatch !== null) {
|
if (blockedMatch !== null) {
|
||||||
return {
|
return {
|
||||||
allowed: false,
|
allowed: false,
|
||||||
reason: `Path matches blocked pattern "${blockedMatch}": "${realPath}"`
|
reason: `Path matches blocked pattern "${blockedMatch}": "${realPath}"`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,9 +278,9 @@ export function validateMount(
|
|||||||
if (allowedRoot === null) {
|
if (allowedRoot === null) {
|
||||||
return {
|
return {
|
||||||
allowed: false,
|
allowed: false,
|
||||||
reason: `Path "${realPath}" is not under any allowed root. Allowed roots: ${
|
reason: `Path "${realPath}" is not under any allowed root. Allowed roots: ${allowlist.allowedRoots
|
||||||
allowlist.allowedRoots.map(r => expandPath(r.path)).join(', ')
|
.map((r) => expandPath(r.path))
|
||||||
}`
|
.join(', ')}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,16 +292,22 @@ export function validateMount(
|
|||||||
if (!isMain && allowlist.nonMainReadOnly) {
|
if (!isMain && allowlist.nonMainReadOnly) {
|
||||||
// Non-main groups forced to read-only
|
// Non-main groups forced to read-only
|
||||||
effectiveReadonly = true;
|
effectiveReadonly = true;
|
||||||
logger.info({
|
logger.info(
|
||||||
mount: mount.hostPath
|
{
|
||||||
}, 'Mount forced to read-only for non-main group');
|
mount: mount.hostPath,
|
||||||
|
},
|
||||||
|
'Mount forced to read-only for non-main group',
|
||||||
|
);
|
||||||
} else if (!allowedRoot.allowReadWrite) {
|
} else if (!allowedRoot.allowReadWrite) {
|
||||||
// Root doesn't allow read-write
|
// Root doesn't allow read-write
|
||||||
effectiveReadonly = true;
|
effectiveReadonly = true;
|
||||||
logger.info({
|
logger.info(
|
||||||
|
{
|
||||||
mount: mount.hostPath,
|
mount: mount.hostPath,
|
||||||
root: allowedRoot.path
|
root: allowedRoot.path,
|
||||||
}, 'Mount forced to read-only - root does not allow read-write');
|
},
|
||||||
|
'Mount forced to read-only - root does not allow read-write',
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Read-write allowed
|
// Read-write allowed
|
||||||
effectiveReadonly = false;
|
effectiveReadonly = false;
|
||||||
@@ -295,7 +318,7 @@ export function validateMount(
|
|||||||
allowed: true,
|
allowed: true,
|
||||||
reason: `Allowed under root "${allowedRoot.path}"${allowedRoot.description ? ` (${allowedRoot.description})` : ''}`,
|
reason: `Allowed under root "${allowedRoot.path}"${allowedRoot.description ? ` (${allowedRoot.description})` : ''}`,
|
||||||
realHostPath: realPath,
|
realHostPath: realPath,
|
||||||
effectiveReadonly
|
effectiveReadonly,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,7 +330,7 @@ export function validateMount(
|
|||||||
export function validateAdditionalMounts(
|
export function validateAdditionalMounts(
|
||||||
mounts: AdditionalMount[],
|
mounts: AdditionalMount[],
|
||||||
groupName: string,
|
groupName: string,
|
||||||
isMain: boolean
|
isMain: boolean,
|
||||||
): Array<{
|
): Array<{
|
||||||
hostPath: string;
|
hostPath: string;
|
||||||
containerPath: string;
|
containerPath: string;
|
||||||
@@ -326,23 +349,29 @@ export function validateAdditionalMounts(
|
|||||||
validatedMounts.push({
|
validatedMounts.push({
|
||||||
hostPath: result.realHostPath!,
|
hostPath: result.realHostPath!,
|
||||||
containerPath: `/workspace/extra/${mount.containerPath}`,
|
containerPath: `/workspace/extra/${mount.containerPath}`,
|
||||||
readonly: result.effectiveReadonly!
|
readonly: result.effectiveReadonly!,
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug({
|
logger.debug(
|
||||||
|
{
|
||||||
group: groupName,
|
group: groupName,
|
||||||
hostPath: result.realHostPath,
|
hostPath: result.realHostPath,
|
||||||
containerPath: mount.containerPath,
|
containerPath: mount.containerPath,
|
||||||
readonly: result.effectiveReadonly,
|
readonly: result.effectiveReadonly,
|
||||||
reason: result.reason
|
reason: result.reason,
|
||||||
}, 'Mount validated successfully');
|
},
|
||||||
|
'Mount validated successfully',
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
logger.warn({
|
logger.warn(
|
||||||
|
{
|
||||||
group: groupName,
|
group: groupName,
|
||||||
requestedPath: mount.hostPath,
|
requestedPath: mount.hostPath,
|
||||||
containerPath: mount.containerPath,
|
containerPath: mount.containerPath,
|
||||||
reason: result.reason
|
reason: result.reason,
|
||||||
}, 'Additional mount REJECTED');
|
},
|
||||||
|
'Additional mount REJECTED',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,26 +387,26 @@ export function generateAllowlistTemplate(): string {
|
|||||||
{
|
{
|
||||||
path: '~/projects',
|
path: '~/projects',
|
||||||
allowReadWrite: true,
|
allowReadWrite: true,
|
||||||
description: 'Development projects'
|
description: 'Development projects',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '~/repos',
|
path: '~/repos',
|
||||||
allowReadWrite: true,
|
allowReadWrite: true,
|
||||||
description: 'Git repositories'
|
description: 'Git repositories',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '~/Documents/work',
|
path: '~/Documents/work',
|
||||||
allowReadWrite: false,
|
allowReadWrite: false,
|
||||||
description: 'Work documents (read-only)'
|
description: 'Work documents (read-only)',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
blockedPatterns: [
|
blockedPatterns: [
|
||||||
// Additional patterns beyond defaults
|
// Additional patterns beyond defaults
|
||||||
'password',
|
'password',
|
||||||
'secret',
|
'secret',
|
||||||
'token'
|
'token',
|
||||||
],
|
],
|
||||||
nonMainReadOnly: true
|
nonMainReadOnly: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
return JSON.stringify(template, null, 2);
|
return JSON.stringify(template, null, 2);
|
||||||
|
|||||||
@@ -1,15 +1,28 @@
|
|||||||
|
import { CronExpressionParser } from 'cron-parser';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import pino from 'pino';
|
import pino from 'pino';
|
||||||
import { CronExpressionParser } from 'cron-parser';
|
|
||||||
import { getDueTasks, updateTaskAfterRun, logTaskRun, getTaskById, getAllTasks } from './db.js';
|
import {
|
||||||
import { ScheduledTask, RegisteredGroup } from './types.js';
|
DATA_DIR,
|
||||||
import { GROUPS_DIR, SCHEDULER_POLL_INTERVAL, DATA_DIR, MAIN_GROUP_FOLDER, TIMEZONE } from './config.js';
|
GROUPS_DIR,
|
||||||
|
MAIN_GROUP_FOLDER,
|
||||||
|
SCHEDULER_POLL_INTERVAL,
|
||||||
|
TIMEZONE,
|
||||||
|
} from './config.js';
|
||||||
import { runContainerAgent, writeTasksSnapshot } from './container-runner.js';
|
import { runContainerAgent, writeTasksSnapshot } from './container-runner.js';
|
||||||
|
import {
|
||||||
|
getAllTasks,
|
||||||
|
getDueTasks,
|
||||||
|
getTaskById,
|
||||||
|
logTaskRun,
|
||||||
|
updateTaskAfterRun,
|
||||||
|
} from './db.js';
|
||||||
|
import { RegisteredGroup, ScheduledTask } from './types.js';
|
||||||
|
|
||||||
const logger = pino({
|
const logger = pino({
|
||||||
level: process.env.LOG_LEVEL || 'info',
|
level: process.env.LOG_LEVEL || 'info',
|
||||||
transport: { target: 'pino-pretty', options: { colorize: true } }
|
transport: { target: 'pino-pretty', options: { colorize: true } },
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface SchedulerDependencies {
|
export interface SchedulerDependencies {
|
||||||
@@ -18,25 +31,36 @@ export interface SchedulerDependencies {
|
|||||||
getSessions: () => Record<string, string>;
|
getSessions: () => Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runTask(task: ScheduledTask, deps: SchedulerDependencies): Promise<void> {
|
async function runTask(
|
||||||
|
task: ScheduledTask,
|
||||||
|
deps: SchedulerDependencies,
|
||||||
|
): Promise<void> {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const groupDir = path.join(GROUPS_DIR, task.group_folder);
|
const groupDir = path.join(GROUPS_DIR, task.group_folder);
|
||||||
fs.mkdirSync(groupDir, { recursive: true });
|
fs.mkdirSync(groupDir, { recursive: true });
|
||||||
|
|
||||||
logger.info({ taskId: task.id, group: task.group_folder }, 'Running scheduled task');
|
logger.info(
|
||||||
|
{ taskId: task.id, group: task.group_folder },
|
||||||
|
'Running scheduled task',
|
||||||
|
);
|
||||||
|
|
||||||
const groups = deps.registeredGroups();
|
const groups = deps.registeredGroups();
|
||||||
const group = Object.values(groups).find(g => g.folder === task.group_folder);
|
const group = Object.values(groups).find(
|
||||||
|
(g) => g.folder === task.group_folder,
|
||||||
|
);
|
||||||
|
|
||||||
if (!group) {
|
if (!group) {
|
||||||
logger.error({ taskId: task.id, groupFolder: task.group_folder }, 'Group not found for task');
|
logger.error(
|
||||||
|
{ taskId: task.id, groupFolder: task.group_folder },
|
||||||
|
'Group not found for task',
|
||||||
|
);
|
||||||
logTaskRun({
|
logTaskRun({
|
||||||
task_id: task.id,
|
task_id: task.id,
|
||||||
run_at: new Date().toISOString(),
|
run_at: new Date().toISOString(),
|
||||||
duration_ms: Date.now() - startTime,
|
duration_ms: Date.now() - startTime,
|
||||||
status: 'error',
|
status: 'error',
|
||||||
result: null,
|
result: null,
|
||||||
error: `Group not found: ${task.group_folder}`
|
error: `Group not found: ${task.group_folder}`,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -44,22 +68,27 @@ async function runTask(task: ScheduledTask, deps: SchedulerDependencies): Promis
|
|||||||
// Update tasks snapshot for container to read (filtered by group)
|
// Update tasks snapshot for container to read (filtered by group)
|
||||||
const isMain = task.group_folder === MAIN_GROUP_FOLDER;
|
const isMain = task.group_folder === MAIN_GROUP_FOLDER;
|
||||||
const tasks = getAllTasks();
|
const tasks = getAllTasks();
|
||||||
writeTasksSnapshot(task.group_folder, isMain, tasks.map(t => ({
|
writeTasksSnapshot(
|
||||||
|
task.group_folder,
|
||||||
|
isMain,
|
||||||
|
tasks.map((t) => ({
|
||||||
id: t.id,
|
id: t.id,
|
||||||
groupFolder: t.group_folder,
|
groupFolder: t.group_folder,
|
||||||
prompt: t.prompt,
|
prompt: t.prompt,
|
||||||
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,
|
||||||
next_run: t.next_run
|
next_run: t.next_run,
|
||||||
})));
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
let result: string | null = null;
|
let result: string | null = null;
|
||||||
let error: string | null = null;
|
let error: string | null = null;
|
||||||
|
|
||||||
// For group context mode, use the group's current session
|
// For group context mode, use the group's current session
|
||||||
const sessions = deps.getSessions();
|
const sessions = deps.getSessions();
|
||||||
const sessionId = task.context_mode === 'group' ? sessions[task.group_folder] : undefined;
|
const sessionId =
|
||||||
|
task.context_mode === 'group' ? sessions[task.group_folder] : undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const output = await runContainerAgent(group, {
|
const output = await runContainerAgent(group, {
|
||||||
@@ -68,7 +97,7 @@ async function runTask(task: ScheduledTask, deps: SchedulerDependencies): Promis
|
|||||||
groupFolder: task.group_folder,
|
groupFolder: task.group_folder,
|
||||||
chatJid: task.chat_jid,
|
chatJid: task.chat_jid,
|
||||||
isMain,
|
isMain,
|
||||||
isScheduledTask: true
|
isScheduledTask: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (output.status === 'error') {
|
if (output.status === 'error') {
|
||||||
@@ -77,7 +106,10 @@ async function runTask(task: ScheduledTask, deps: SchedulerDependencies): Promis
|
|||||||
result = output.result;
|
result = output.result;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info({ taskId: task.id, durationMs: Date.now() - startTime }, 'Task completed');
|
logger.info(
|
||||||
|
{ taskId: task.id, durationMs: Date.now() - startTime },
|
||||||
|
'Task completed',
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err instanceof Error ? err.message : String(err);
|
error = err instanceof Error ? err.message : String(err);
|
||||||
logger.error({ taskId: task.id, error }, 'Task failed');
|
logger.error({ taskId: task.id, error }, 'Task failed');
|
||||||
@@ -91,12 +123,14 @@ async function runTask(task: ScheduledTask, deps: SchedulerDependencies): Promis
|
|||||||
duration_ms: durationMs,
|
duration_ms: durationMs,
|
||||||
status: error ? 'error' : 'success',
|
status: error ? 'error' : 'success',
|
||||||
result,
|
result,
|
||||||
error
|
error,
|
||||||
});
|
});
|
||||||
|
|
||||||
let nextRun: string | null = null;
|
let nextRun: string | null = null;
|
||||||
if (task.schedule_type === 'cron') {
|
if (task.schedule_type === 'cron') {
|
||||||
const interval = CronExpressionParser.parse(task.schedule_value, { tz: TIMEZONE });
|
const interval = CronExpressionParser.parse(task.schedule_value, {
|
||||||
|
tz: TIMEZONE,
|
||||||
|
});
|
||||||
nextRun = interval.next().toISOString();
|
nextRun = interval.next().toISOString();
|
||||||
} else if (task.schedule_type === 'interval') {
|
} else if (task.schedule_type === 'interval') {
|
||||||
const ms = parseInt(task.schedule_value, 10);
|
const ms = parseInt(task.schedule_value, 10);
|
||||||
@@ -104,7 +138,11 @@ async function runTask(task: ScheduledTask, deps: SchedulerDependencies): Promis
|
|||||||
}
|
}
|
||||||
// 'once' tasks have no next run
|
// 'once' tasks have no next run
|
||||||
|
|
||||||
const resultSummary = error ? `Error: ${error}` : (result ? result.slice(0, 200) : 'Completed');
|
const resultSummary = error
|
||||||
|
? `Error: ${error}`
|
||||||
|
: result
|
||||||
|
? result.slice(0, 200)
|
||||||
|
: 'Completed';
|
||||||
updateTaskAfterRun(task.id, nextRun, resultSummary);
|
updateTaskAfterRun(task.id, nextRun, resultSummary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,16 +6,16 @@
|
|||||||
*
|
*
|
||||||
* Usage: npx tsx src/whatsapp-auth.ts
|
* Usage: npx tsx src/whatsapp-auth.ts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import makeWASocket, {
|
|
||||||
useMultiFileAuthState,
|
|
||||||
DisconnectReason,
|
|
||||||
makeCacheableSignalKeyStore,
|
|
||||||
} from '@whiskeysockets/baileys';
|
|
||||||
import pino from 'pino';
|
|
||||||
import qrcode from 'qrcode-terminal';
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import pino from 'pino';
|
||||||
|
import qrcode from 'qrcode-terminal';
|
||||||
|
|
||||||
|
import makeWASocket, {
|
||||||
|
DisconnectReason,
|
||||||
|
makeCacheableSignalKeyStore,
|
||||||
|
useMultiFileAuthState,
|
||||||
|
} from '@whiskeysockets/baileys';
|
||||||
|
|
||||||
const AUTH_DIR = './store/auth';
|
const AUTH_DIR = './store/auth';
|
||||||
|
|
||||||
@@ -30,7 +30,9 @@ async function authenticate(): Promise<void> {
|
|||||||
|
|
||||||
if (state.creds.registered) {
|
if (state.creds.registered) {
|
||||||
console.log('✓ Already authenticated with WhatsApp');
|
console.log('✓ Already authenticated with WhatsApp');
|
||||||
console.log(' To re-authenticate, delete the store/auth folder and run again.');
|
console.log(
|
||||||
|
' To re-authenticate, delete the store/auth folder and run again.',
|
||||||
|
);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user