digital-brain 1.1.9 → 1.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -1
- package/bin/digital-brain.js +65 -1
- package/docs/AUTOMATIONS.md +29 -3
- package/docs/PRIVACY.md +1 -1
- package/package.json +1 -1
- package/whatsapp-web/auto-reply.mjs +295 -10
package/README.md
CHANGED
|
@@ -83,7 +83,8 @@ node ./bin/digital-brain.js init ./Digital Brain\ Vault
|
|
|
83
83
|
- Generate reply-ready person context that keeps WhatsApp, iMessage, Slack, and LinkedIn evidence separate under the same person.
|
|
84
84
|
- Create AI-readable memory files for future prompts.
|
|
85
85
|
- Draft WhatsApp sends by default, send with explicit `--yes`, or configure auto-send mode during init.
|
|
86
|
-
- Run an explicit WhatsApp auto-responder that uses
|
|
86
|
+
- Run an explicit WhatsApp auto-responder that uses Ollama or a Codex command plus vault memory while the command is running.
|
|
87
|
+
- Choose the WhatsApp auto-reply provider during init: Ollama, Codex app bridge, or Codex CLI.
|
|
87
88
|
- Enforce an AI-disclosure guard after repeated AI-assisted sends.
|
|
88
89
|
|
|
89
90
|
## Core Commands
|
|
@@ -98,6 +99,8 @@ digital-brain import-linkedin --input ./linkedin-archive.zip
|
|
|
98
99
|
digital-brain send-whatsapp --to "Name" --message "text"
|
|
99
100
|
digital-brain auto-whatsapp --allow "Name" --model llama3.1
|
|
100
101
|
digital-brain auto-whatsapp --contact "+15551234567" --model llama3.1
|
|
102
|
+
digital-brain auto-whatsapp --allow-all --provider codex --yes
|
|
103
|
+
digital-brain auto-whatsapp --allow-all --provider codex-app --yes
|
|
101
104
|
```
|
|
102
105
|
|
|
103
106
|
`init` remembers your vault globally, so `run` works from anywhere. `run` syncs the live local sources you selected, extracts relationships, and writes interpreted memory in one command.
|
|
@@ -124,10 +127,32 @@ digital-brain auto-whatsapp --allow "Mom" --model llama3.1
|
|
|
124
127
|
digital-brain auto-whatsapp --allow "Mom" --model llama3.1 --yes
|
|
125
128
|
digital-brain auto-whatsapp --allow "Mom" --model llama3.1 --yes --no-process-unread
|
|
126
129
|
digital-brain auto-whatsapp --contact "+15551234567" --model llama3.1 --yes
|
|
130
|
+
digital-brain auto-whatsapp --allow-all --provider codex --yes
|
|
131
|
+
digital-brain auto-whatsapp --allow-all --provider codex-app --yes
|
|
127
132
|
```
|
|
128
133
|
|
|
134
|
+
If you start without `--allow`, `--contact`, or `--allow-all` in an interactive terminal, Digital Brain asks whether to cover all contacts or select contacts from your WhatsApp chat list. With `--allow-all`, it still asks once before the first AI reply to each new chat and stores the decision in `08 Sources/WhatsApp/Outbound/auto-reply-whitelist.json`. Use `--auto-approve-new-chats` only if you intentionally want unattended first sends.
|
|
135
|
+
|
|
129
136
|
Even with `--allow-all`, likely business, notification, OTP, delivery, bank, and support chats are skipped by default. Use explicit `--allow "Name"` or `--contact "+15551234567"` for trusted personal chats. Pass `--include-businesses` only if you intentionally want those chats included.
|
|
130
137
|
|
|
138
|
+
The default provider is local Ollama. `--provider codex` uses the Codex CLI. `--provider codex-app` uses a file bridge for the Codex desktop app.
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
digital-brain auto-whatsapp --allow-all --provider codex --yes
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
If your Codex CLI needs a custom command, pass `--codex-command "..."` or set `DIGITAL_BRAIN_CODEX_COMMAND`. If the command contains `{promptFile}`, Digital Brain writes the reply prompt to a temp file and substitutes that path; otherwise it pipes the prompt to stdin.
|
|
145
|
+
|
|
146
|
+
For the Codex desktop app bridge:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
digital-brain auto-whatsapp --allow-all --provider codex-app --yes
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Digital Brain writes requests to `08 Sources/WhatsApp/Outbound/Codex App Bridge/requests` and waits for matching JSON responses in `responses`.
|
|
153
|
+
|
|
154
|
+
If you select `Codex app bridge` during `digital-brain init`, the vault also gets `Tools/Codex App Bridge Automation.md` with the exact prompt to use in the Codex app.
|
|
155
|
+
|
|
131
156
|
If Digital Brain has already sent two AI-assisted messages to the same chat in the last 24 hours, the next send must disclose that AI is helping. Once that chat has received an AI disclosure, Digital Brain will not keep repeating it.
|
|
132
157
|
|
|
133
158
|
## Automation
|
package/bin/digital-brain.js
CHANGED
|
@@ -49,6 +49,7 @@ async function init(argv, args) {
|
|
|
49
49
|
let activeWindow = args["active-window"] || "08:00-12:00";
|
|
50
50
|
let timezone = args.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || "local";
|
|
51
51
|
let outboundMode = args["outbound-mode"] || "draft";
|
|
52
|
+
let autoReplyProvider = args["auto-reply-provider"] || args.provider || "ollama";
|
|
52
53
|
let privacyMode = args["privacy-mode"] || "standard";
|
|
53
54
|
let sourceMarkdownMode = args["source-markdown-mode"] || "none";
|
|
54
55
|
let selectedSources = parseList(args.sources || "whatsapp");
|
|
@@ -105,6 +106,13 @@ async function init(argv, args) {
|
|
|
105
106
|
["send-with-confirmation", "Send with confirmation", "Can send only after explicit command confirmation.", "✅"],
|
|
106
107
|
["auto-send", "Auto-send while running", "Lets auto-whatsapp send from allowlisted chats while it is running.", "🚦"],
|
|
107
108
|
], outboundMode);
|
|
109
|
+
if (outboundMode !== "disabled") {
|
|
110
|
+
autoReplyProvider = await select(rl, "WhatsApp auto-reply brain", [
|
|
111
|
+
["ollama", "Ollama local model", "Runs fully local if Ollama and the model are installed.", "🦙"],
|
|
112
|
+
["codex-app", "Codex app bridge", "Uses request/response files for a Codex desktop automation or thread.", "🧠"],
|
|
113
|
+
["codex", "Codex CLI", "Uses a local codex command; only choose this if the CLI works.", "⌨️"],
|
|
114
|
+
], autoReplyProvider);
|
|
115
|
+
}
|
|
108
116
|
connectAi = await confirm(rl, "🔗 Add global AI pointers for Codex/Claude/Gemini?", true);
|
|
109
117
|
responsibilityAccepted = await responsibilityGate(rl, { schedule, outboundMode });
|
|
110
118
|
if (!responsibilityAccepted && needsResponsibilityGate({ schedule, outboundMode })) {
|
|
@@ -132,6 +140,7 @@ async function init(argv, args) {
|
|
|
132
140
|
activeWindow,
|
|
133
141
|
timezone,
|
|
134
142
|
outboundMode,
|
|
143
|
+
autoReplyProvider,
|
|
135
144
|
privacyMode,
|
|
136
145
|
sourceMarkdownMode,
|
|
137
146
|
selectedSources,
|
|
@@ -154,6 +163,7 @@ async function init(argv, args) {
|
|
|
154
163
|
writeDefaultVault(vault);
|
|
155
164
|
writeRefreshScript(vault, config);
|
|
156
165
|
writeWatchScript(vault, config);
|
|
166
|
+
writeCodexAppBridgeGuide(vault, config);
|
|
157
167
|
|
|
158
168
|
if (connectAi) {
|
|
159
169
|
addGlobalPointer(path.join(os.homedir(), ".codex", "AGENTS.md"), vault, "Codex");
|
|
@@ -166,6 +176,14 @@ async function init(argv, args) {
|
|
|
166
176
|
console.log(`Default vault saved: ${vault}`);
|
|
167
177
|
console.log(`Refresh script: ${path.join(vault, "Tools", "digital-brain-refresh.sh")}`);
|
|
168
178
|
console.log(`Always-on script: ${path.join(vault, "Tools", "digital-brain-watch.sh")}`);
|
|
179
|
+
if (autoReplyProvider === "codex-app") {
|
|
180
|
+
console.log(`Codex app bridge guide: ${path.join(vault, "Tools", "Codex App Bridge Automation.md")}`);
|
|
181
|
+
if (codexAppLooksAvailable()) {
|
|
182
|
+
console.log("Codex app detected. Add the generated bridge prompt as a Codex automation/thread to answer WhatsApp reply requests.");
|
|
183
|
+
} else {
|
|
184
|
+
console.log("Codex app config was not detected. The bridge guide was still generated for later use.");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
169
187
|
console.log("Next:");
|
|
170
188
|
console.log(" digital-brain run");
|
|
171
189
|
if (schedule === "always-on") console.log(` "${path.join(vault, "Tools", "digital-brain-watch.sh")}"`);
|
|
@@ -408,6 +426,48 @@ done
|
|
|
408
426
|
fs.chmodSync(scriptPath, 0o755);
|
|
409
427
|
}
|
|
410
428
|
|
|
429
|
+
function writeCodexAppBridgeGuide(vault, config) {
|
|
430
|
+
const toolsDir = path.join(vault, "Tools");
|
|
431
|
+
ensureDir(toolsDir);
|
|
432
|
+
const bridgeDir = path.join(vault, "08 Sources", "WhatsApp", "Outbound", "Codex App Bridge");
|
|
433
|
+
const requestsDir = path.join(bridgeDir, "requests");
|
|
434
|
+
const responsesDir = path.join(bridgeDir, "responses");
|
|
435
|
+
ensureDir(requestsDir);
|
|
436
|
+
ensureDir(responsesDir);
|
|
437
|
+
const prompt = `# Codex App Bridge Automation
|
|
438
|
+
|
|
439
|
+
Use this prompt in a Codex desktop automation or a live Codex thread when Digital Brain is configured with:
|
|
440
|
+
|
|
441
|
+
\`\`\`bash
|
|
442
|
+
digital-brain auto-whatsapp --provider codex-app --yes
|
|
443
|
+
\`\`\`
|
|
444
|
+
|
|
445
|
+
Request folder:
|
|
446
|
+
|
|
447
|
+
\`${requestsDir}\`
|
|
448
|
+
|
|
449
|
+
Response folder:
|
|
450
|
+
|
|
451
|
+
\`${responsesDir}\`
|
|
452
|
+
|
|
453
|
+
Automation prompt:
|
|
454
|
+
|
|
455
|
+
\`\`\`text
|
|
456
|
+
Check for pending Digital Brain WhatsApp reply requests in ${requestsDir}. For each .json request that does not already have its response file present, read the request JSON, use its prompt field to produce exactly one WhatsApp reply as the user, and write JSON to the request's responsePath in the exact shape {"reply":"..."}. Do not send any WhatsApp message yourself. Do not write markdown or explanations in the response file. If a request cannot be answered, write {"error":"short reason"} to responsePath.
|
|
457
|
+
\`\`\`
|
|
458
|
+
|
|
459
|
+
Notes:
|
|
460
|
+
|
|
461
|
+
- Digital Brain sends the WhatsApp message after the response file appears.
|
|
462
|
+
- Keep the automation active while \`digital-brain auto-whatsapp --provider codex-app\` is running.
|
|
463
|
+
- The default wait timeout is 5 minutes. Override with \`--provider-timeout-ms\`.
|
|
464
|
+
`;
|
|
465
|
+
writeFileAtomic(path.join(toolsDir, "Codex App Bridge Automation.md"), prompt);
|
|
466
|
+
if (config.autoReplyProvider === "codex-app") {
|
|
467
|
+
writeFileAtomic(path.join(bridgeDir, "README.md"), prompt);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
411
471
|
function addGlobalPointer(file, vault, label) {
|
|
412
472
|
ensureDir(path.dirname(file));
|
|
413
473
|
const block = `
|
|
@@ -522,6 +582,10 @@ function needsResponsibilityGate({ schedule, outboundMode }) {
|
|
|
522
582
|
return schedule === "always-on" || ["send-with-confirmation", "auto-send"].includes(outboundMode);
|
|
523
583
|
}
|
|
524
584
|
|
|
585
|
+
function codexAppLooksAvailable() {
|
|
586
|
+
return fs.existsSync(path.join(os.homedir(), ".codex"));
|
|
587
|
+
}
|
|
588
|
+
|
|
525
589
|
function letterFor(index) {
|
|
526
590
|
return String.fromCharCode(65 + index);
|
|
527
591
|
}
|
|
@@ -593,6 +657,6 @@ Usage:
|
|
|
593
657
|
digital-brain extract --days 30
|
|
594
658
|
digital-brain interpret --days 30
|
|
595
659
|
digital-brain send-whatsapp --to "Name" --message "Text" [--yes]
|
|
596
|
-
digital-brain auto-whatsapp --allow "Name" --contact "+15551234567" --model llama3.1 [--yes] [--no-process-unread]
|
|
660
|
+
digital-brain auto-whatsapp --allow "Name" --contact "+15551234567" --provider ollama|codex --model llama3.1 [--yes] [--no-process-unread]
|
|
597
661
|
`);
|
|
598
662
|
}
|
package/docs/AUTOMATIONS.md
CHANGED
|
@@ -31,6 +31,7 @@ digital-brain run
|
|
|
31
31
|
- refresh interval in minutes for always-on mode, clamped to a minimum of 1
|
|
32
32
|
- active time window
|
|
33
33
|
- WhatsApp outbound mode
|
|
34
|
+
- WhatsApp auto-reply provider: Ollama, Codex app bridge, or Codex CLI
|
|
34
35
|
- whether to add AI adapter pointers
|
|
35
36
|
|
|
36
37
|
Most questions are multiple choice. Pick with `A/B/C`, `1/2/3`, the exact value, or press Enter to use the displayed default.
|
|
@@ -45,6 +46,7 @@ Important defaults:
|
|
|
45
46
|
- skipped always-on interval uses 5 minutes, with a hard minimum of 1 minute
|
|
46
47
|
- skipped active window uses `08:00-12:00`
|
|
47
48
|
- skipped outbound mode uses draft-only
|
|
49
|
+
- skipped auto-reply provider uses Ollama
|
|
48
50
|
- auto-send mode can be selected during init, but only after the responsibility check
|
|
49
51
|
- skipped AI pointers are added during the guided quiz
|
|
50
52
|
|
|
@@ -96,13 +98,15 @@ The generated script loops forever and sleeps for `refreshIntervalMinutes`. The
|
|
|
96
98
|
|
|
97
99
|
## WhatsApp Auto-Reply
|
|
98
100
|
|
|
99
|
-
`digital-brain auto-whatsapp` is separate from refresh automation. It uses WhatsApp Web for live incoming messages and Ollama for
|
|
101
|
+
`digital-brain auto-whatsapp` is separate from refresh automation. It uses WhatsApp Web for live incoming messages and either Ollama or a Codex command for reply generation. On startup it scans unread WhatsApp Web chats, then continues listening for new messages.
|
|
100
102
|
|
|
101
103
|
Draft-only:
|
|
102
104
|
|
|
103
105
|
```bash
|
|
104
106
|
digital-brain auto-whatsapp --allow "Mom" --model llama3.1
|
|
105
107
|
digital-brain auto-whatsapp --contact "+15551234567" --model llama3.1
|
|
108
|
+
digital-brain auto-whatsapp --allow-all --provider codex
|
|
109
|
+
digital-brain auto-whatsapp --allow-all --provider codex-app
|
|
106
110
|
```
|
|
107
111
|
|
|
108
112
|
Auto-send while the command is running:
|
|
@@ -110,6 +114,8 @@ Auto-send while the command is running:
|
|
|
110
114
|
```bash
|
|
111
115
|
digital-brain auto-whatsapp --allow "Mom" --model llama3.1 --yes
|
|
112
116
|
digital-brain auto-whatsapp --contact "+15551234567" --model llama3.1 --yes
|
|
117
|
+
digital-brain auto-whatsapp --allow-all --provider codex --yes
|
|
118
|
+
digital-brain auto-whatsapp --allow-all --provider codex-app --yes
|
|
113
119
|
```
|
|
114
120
|
|
|
115
121
|
Broad auto-send for personal chats:
|
|
@@ -120,6 +126,23 @@ digital-brain auto-whatsapp --allow-all --model llama3.1 --yes
|
|
|
120
126
|
|
|
121
127
|
`--allow-all` still skips likely business, notification, OTP, bank, delivery, and support chats by default. Use `--include-businesses` only when you intentionally want those chats included. Prefer explicit `--allow "Name"` or `--contact "+15551234567"` for friends and family.
|
|
122
128
|
|
|
129
|
+
When `--allow-all` is used, Digital Brain asks once before the first AI reply to each new chat and stores allow/deny decisions in `08 Sources/WhatsApp/Outbound/auto-reply-whitelist.json`. Use `--auto-approve-new-chats` only for fully unattended first sends.
|
|
130
|
+
|
|
131
|
+
Provider options:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
digital-brain auto-whatsapp --allow "Mom" --provider ollama --model llama3.1 --yes
|
|
135
|
+
digital-brain auto-whatsapp --allow "Mom" --provider codex --yes
|
|
136
|
+
digital-brain auto-whatsapp --allow "Mom" --provider codex-app --yes
|
|
137
|
+
digital-brain auto-whatsapp --allow "Mom" --provider codex --codex-command "codex exec --skip-git-repo-check" --yes
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`--provider codex` runs a local Codex command. If `--codex-command` contains `{promptFile}`, Digital Brain writes the prompt to a temp file and substitutes the path; otherwise it pipes the prompt to stdin.
|
|
141
|
+
|
|
142
|
+
`--provider codex-app` does not use the Codex CLI. It writes request JSON files to `08 Sources/WhatsApp/Outbound/Codex App Bridge/requests` and waits for response JSON files in `responses`. A Codex desktop automation or live Codex thread must process those request files and write `{"reply":"..."}` to the provided `responsePath`.
|
|
143
|
+
|
|
144
|
+
When `codex-app` is selected during `init`, Digital Brain creates `Tools/Codex App Bridge Automation.md` with the exact Codex automation prompt and bridge folder paths.
|
|
145
|
+
|
|
123
146
|
If you selected `Auto-send while running` during init, `auto-whatsapp` can send without `--yes` while it is running:
|
|
124
147
|
|
|
125
148
|
```bash
|
|
@@ -128,9 +151,12 @@ digital-brain auto-whatsapp --allow "Mom" --model llama3.1
|
|
|
128
151
|
|
|
129
152
|
Guardrails:
|
|
130
153
|
|
|
131
|
-
- requires Ollama running locally
|
|
132
|
-
- requires the selected model, for example `ollama pull llama3.1`
|
|
154
|
+
- with `--provider ollama`, requires Ollama running locally
|
|
155
|
+
- with `--provider ollama`, requires the selected model, for example `ollama pull llama3.1`
|
|
156
|
+
- with `--provider codex`, requires a working local Codex command
|
|
157
|
+
- with `--provider codex-app`, requires a Codex desktop bridge automation/thread that writes response files
|
|
133
158
|
- requires `--allow "Name"` or `--contact "+15551234567"` unless `--allow-all` is explicitly passed
|
|
159
|
+
- single-threads reply generation so multiple incoming chats do not trigger overlapping sends
|
|
134
160
|
- skips likely business, notification, OTP, and service chats unless `--include-businesses` is passed or the chat is explicitly allowlisted by name or contact number
|
|
135
161
|
- processes unread chats on startup unless `--no-process-unread` is passed
|
|
136
162
|
- skips groups unless `--include-groups` is passed
|
package/docs/PRIVACY.md
CHANGED
|
@@ -7,7 +7,7 @@ Digital Brain is designed for local use.
|
|
|
7
7
|
- No cloud API is called by default.
|
|
8
8
|
- Ollama interpretation is local when enabled.
|
|
9
9
|
- WhatsApp sending uses a local WhatsApp Web session.
|
|
10
|
-
- WhatsApp auto-reply uses local Ollama by default, runs only while the command is active
|
|
10
|
+
- WhatsApp auto-reply uses local Ollama by default, a configured local Codex command, or a Codex desktop file bridge. It runs only while the command is active and requires an allowlist unless explicitly overridden. If init is configured for auto-send mode, it can send without `--yes`.
|
|
11
11
|
- Raw source data stays under `08 Sources/`; normal AI context should use `06 AI Memory/` and human notes under `04 People/`.
|
|
12
12
|
- Same-person matching across sources is provisional and file-based; keep source evidence visible when using merged person context.
|
|
13
13
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import readline from "node:readline/promises";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
4
7
|
import qrcode from "qrcode-terminal";
|
|
5
8
|
import pkg from "whatsapp-web.js";
|
|
6
9
|
|
|
@@ -16,14 +19,20 @@ const whatsAppDir = path.join(vault, "08 Sources", "WhatsApp");
|
|
|
16
19
|
const outboundDir = path.join(whatsAppDir, "Outbound");
|
|
17
20
|
const sessionDir = path.join(whatsAppDir, ".session");
|
|
18
21
|
const statePath = path.join(outboundDir, "auto-reply-state.json");
|
|
22
|
+
const whitelistPath = path.join(outboundDir, "auto-reply-whitelist.json");
|
|
23
|
+
const codexAppBridgeDir = path.join(outboundDir, "Codex App Bridge");
|
|
19
24
|
const config = readConfig(vault);
|
|
25
|
+
const provider = args.provider || config.autoReplyProvider || "ollama";
|
|
20
26
|
const model = args.model || config.autoReplyModel || "llama3.1";
|
|
27
|
+
const codexCommand = args["codex-command"] || process.env.DIGITAL_BRAIN_CODEX_COMMAND || config.codexCommand || "codex exec --skip-git-repo-check";
|
|
21
28
|
const allow = parseList(args.allow || "");
|
|
22
29
|
const deny = parseList(args.deny || "");
|
|
23
30
|
const contactNumbers = parseList([args.contact, args.phone, args["contact-number"]].filter(Boolean).join(","))
|
|
24
31
|
.map(normalizePhone)
|
|
25
32
|
.filter(Boolean);
|
|
26
33
|
const allowAll = Boolean(args["allow-all"]);
|
|
34
|
+
let runtimeAllowAll = allowAll;
|
|
35
|
+
const autoApproveNewChats = Boolean(args["auto-approve-new-chats"]);
|
|
27
36
|
const includeGroups = Boolean(args["include-groups"]);
|
|
28
37
|
const includeBusinesses = Boolean(args["include-businesses"]);
|
|
29
38
|
const sendEnabled = Boolean(args.yes) || config.outboundMode === "auto-send";
|
|
@@ -33,6 +42,12 @@ const maxRepliesPerChat = numberArg("max-replies-per-chat", 5);
|
|
|
33
42
|
const maxContextChars = numberArg("max-context-chars", 12000);
|
|
34
43
|
const outboundLogMode = args["log-mode"] || config.outboundLogMode || "metadata";
|
|
35
44
|
const state = loadState();
|
|
45
|
+
const whitelist = loadWhitelist();
|
|
46
|
+
const hasStoredScope = Object.keys(whitelist.allowedChats || {}).length > 0;
|
|
47
|
+
const hasInitialScope = allowAll || allow.length > 0 || contactNumbers.length > 0 || hasStoredScope;
|
|
48
|
+
const interactiveTerminal = Boolean(input.isTTY && output.isTTY);
|
|
49
|
+
let queueTail = Promise.resolve();
|
|
50
|
+
const rl = interactiveTerminal ? readline.createInterface({ input, output }) : null;
|
|
36
51
|
|
|
37
52
|
fs.mkdirSync(outboundDir, { recursive: true });
|
|
38
53
|
|
|
@@ -41,12 +56,16 @@ if (config.outboundMode === "disabled") {
|
|
|
41
56
|
process.exit(1);
|
|
42
57
|
}
|
|
43
58
|
|
|
44
|
-
if (!
|
|
59
|
+
if (!hasInitialScope && !interactiveTerminal) {
|
|
45
60
|
console.error('Refusing to auto-reply without an allowlist. Add --allow "Name", --contact "+15551234567", or pass --allow-all explicitly.');
|
|
46
61
|
process.exit(1);
|
|
47
62
|
}
|
|
48
63
|
|
|
49
|
-
|
|
64
|
+
if (provider === "ollama") {
|
|
65
|
+
await assertOllamaModel(model);
|
|
66
|
+
} else if (!["codex", "codex-app"].includes(provider)) {
|
|
67
|
+
throw new Error(`Unsupported auto-reply provider "${provider}". Use "ollama", "codex", or "codex-app".`);
|
|
68
|
+
}
|
|
50
69
|
|
|
51
70
|
const client = new Client({
|
|
52
71
|
authStrategy: new LocalAuth({ clientId: "digital-brain", dataPath: sessionDir }),
|
|
@@ -59,9 +78,12 @@ client.on("qr", (qr) => {
|
|
|
59
78
|
});
|
|
60
79
|
|
|
61
80
|
client.on("ready", async () => {
|
|
62
|
-
console.log(`Digital Brain WhatsApp auto-reply running with
|
|
81
|
+
console.log(`Digital Brain WhatsApp auto-reply running with provider: ${provider}${provider === "ollama" ? ` (${model})` : ""}`);
|
|
63
82
|
console.log(sendEnabled ? "Auto-send is enabled." : "Draft mode. Replies will be logged but not sent. Add --yes or set outboundMode=auto-send to send.");
|
|
64
|
-
|
|
83
|
+
if (!hasInitialScope) await configureInteractiveScope();
|
|
84
|
+
console.log(runtimeAllowAll ? "Allowlist: all chats, with first-send approval per new chat." : allowlistSummary());
|
|
85
|
+
if (provider === "codex") console.log(`Codex command: ${codexCommand}`);
|
|
86
|
+
if (provider === "codex-app") console.log(`Codex App bridge: ${codexAppBridgeDir}`);
|
|
65
87
|
if (!includeBusinesses) console.log("Likely business, notification, OTP, and service chats are skipped by default.");
|
|
66
88
|
try {
|
|
67
89
|
if (processUnreadOnStart) {
|
|
@@ -78,7 +100,7 @@ client.on("ready", async () => {
|
|
|
78
100
|
client.on("message", async (message) => {
|
|
79
101
|
try {
|
|
80
102
|
console.log(`Received WhatsApp message event: ${summarize(message.body || "[non-text message]")}`);
|
|
81
|
-
|
|
103
|
+
enqueueMessage(message);
|
|
82
104
|
} catch (error) {
|
|
83
105
|
console.error(`Auto-reply error: ${error.message}`);
|
|
84
106
|
}
|
|
@@ -115,8 +137,86 @@ async function processUnreadChats() {
|
|
|
115
137
|
continue;
|
|
116
138
|
}
|
|
117
139
|
console.log(`Processing unread chat: ${name}`);
|
|
118
|
-
await
|
|
140
|
+
await enqueueMessage(latestInbound, chat);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function configureInteractiveScope() {
|
|
145
|
+
console.log("");
|
|
146
|
+
console.log("Auto-reply scope:");
|
|
147
|
+
console.log(" A) all contacts, but ask once before the first AI reply to each new chat");
|
|
148
|
+
console.log(" S) select contacts from your WhatsApp chat list");
|
|
149
|
+
console.log(" Q) quit");
|
|
150
|
+
const answer = (await rl.question("Choose A/S/Q [S]: ")).trim().toLowerCase() || "s";
|
|
151
|
+
if (answer.startsWith("q")) {
|
|
152
|
+
await shutdown(0);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (answer.startsWith("a")) {
|
|
156
|
+
runtimeAllowAll = true;
|
|
157
|
+
console.log("Scope set to all contacts with first-send approval.");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const chats = (await client.getChats())
|
|
161
|
+
.filter((chat) => includeGroups || !chat.isGroup)
|
|
162
|
+
.sort((a, b) => Number(b.timestamp || 0) - Number(a.timestamp || 0))
|
|
163
|
+
.slice(0, numberArg("select-limit", 60));
|
|
164
|
+
if (chats.length === 0) {
|
|
165
|
+
console.log("No WhatsApp chats found to select.");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
console.log("");
|
|
169
|
+
console.log("Select contacts:");
|
|
170
|
+
chats.forEach((chat, index) => {
|
|
171
|
+
console.log(` ${index + 1}) ${chatName(chat)}`);
|
|
172
|
+
});
|
|
173
|
+
const selection = await rl.question("Numbers to whitelist, comma-separated: ");
|
|
174
|
+
const selected = parseIndexSelection(selection, chats.length).map((index) => chats[index]);
|
|
175
|
+
for (const chat of selected) {
|
|
176
|
+
setWhitelistDecision(chat, "allow", "startup-select");
|
|
177
|
+
}
|
|
178
|
+
console.log(`Whitelisted ${selected.length} chat(s).`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function enqueueMessage(message, knownChat = null) {
|
|
182
|
+
queueTail = queueTail
|
|
183
|
+
.catch(() => undefined)
|
|
184
|
+
.then(() => handleMessage(message, knownChat))
|
|
185
|
+
.catch((error) => console.error(`Auto-reply error: ${error.message}`));
|
|
186
|
+
return queueTail;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function ensureChatApproved(chat, recentMessages) {
|
|
190
|
+
const name = chatName(chat);
|
|
191
|
+
if (isExplicitlyAllowed(chat) || isChatWhitelisted(chat)) return true;
|
|
192
|
+
if (isChatDenied(chat)) {
|
|
193
|
+
console.log(`Skipping stored denied chat: ${name}`);
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
if (!runtimeAllowAll) return false;
|
|
197
|
+
if (autoApproveNewChats) {
|
|
198
|
+
setWhitelistDecision(chat, "allow", "auto-approve-new-chats");
|
|
199
|
+
return true;
|
|
119
200
|
}
|
|
201
|
+
if (!interactiveTerminal) {
|
|
202
|
+
console.log(`Skipping ${name}: first-send approval required but no interactive terminal is available.`);
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
console.log("");
|
|
206
|
+
console.log(`New chat needs AI approval: ${name}`);
|
|
207
|
+
const inbound = recentMessages.filter((item) => !item.fromMe && !item.isStatus).slice(-3);
|
|
208
|
+
for (const item of inbound) {
|
|
209
|
+
console.log(` ${name}: ${summarize(item.body || "[non-text message]")}`);
|
|
210
|
+
}
|
|
211
|
+
const answer = (await rl.question("Whitelist this chat for AI auto-replies? [y/N]: ")).trim().toLowerCase();
|
|
212
|
+
if (answer === "y" || answer === "yes") {
|
|
213
|
+
setWhitelistDecision(chat, "allow", "first-send-prompt");
|
|
214
|
+
console.log(`Whitelisted ${name}.`);
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
setWhitelistDecision(chat, "deny", "first-send-prompt");
|
|
218
|
+
console.log(`Denied ${name}.`);
|
|
219
|
+
return false;
|
|
120
220
|
}
|
|
121
221
|
|
|
122
222
|
async function handleMessage(message, knownChat = null) {
|
|
@@ -154,6 +254,10 @@ async function handleMessage(message, knownChat = null) {
|
|
|
154
254
|
markProcessed(message, name, { sent: false });
|
|
155
255
|
return;
|
|
156
256
|
}
|
|
257
|
+
if (!(await ensureChatApproved(chat, recentMessages))) {
|
|
258
|
+
markProcessed(message, name, { sent: false });
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
157
261
|
const disclosure = disclosureStatus(name);
|
|
158
262
|
const prompt = buildPrompt({
|
|
159
263
|
chatName: name,
|
|
@@ -161,7 +265,7 @@ async function handleMessage(message, knownChat = null) {
|
|
|
161
265
|
recentMessages,
|
|
162
266
|
disclosureRequired: disclosure.required,
|
|
163
267
|
});
|
|
164
|
-
console.log(`Generating reply for ${name} with ${model}...`);
|
|
268
|
+
console.log(`Generating reply for ${name} with ${provider}${provider === "ollama" ? `:${model}` : ""}...`);
|
|
165
269
|
const startedAt = Date.now();
|
|
166
270
|
const reply = await generateReply(prompt);
|
|
167
271
|
console.log(`Generated reply for ${name} in ${Date.now() - startedAt}ms: ${summarize(reply || "[empty reply]")}`);
|
|
@@ -192,6 +296,9 @@ function buildPrompt({ chatName, incomingBody, recentMessages, disclosureRequire
|
|
|
192
296
|
"You are helping the user reply on WhatsApp.",
|
|
193
297
|
"Write exactly one message to send as the user.",
|
|
194
298
|
"Be natural, concise, and relationship-appropriate.",
|
|
299
|
+
"First infer the immediate intent of the current conversation from the recent chat. Continue that thread only.",
|
|
300
|
+
"Do not start with hey/hi unless the recent chat itself uses that greeting pattern.",
|
|
301
|
+
"Mirror the vocabulary, register, and pacing already present in this chat.",
|
|
195
302
|
"Match the user's own communication style from My Communication Style. If it says lowercase-heavy or undercapitalized, prefer lowercase casual texting.",
|
|
196
303
|
"Use lexical signals from My Communication Style: recurring style words, openers, short phrase shapes, punctuation habits, and lowercase-i behavior.",
|
|
197
304
|
"Do not sound like customer support, corporate email, or a generic AI assistant.",
|
|
@@ -238,6 +345,12 @@ function readMemoryContext(chatName) {
|
|
|
238
345
|
}
|
|
239
346
|
|
|
240
347
|
async function generateReply(prompt) {
|
|
348
|
+
if (provider === "codex") return generateCodexReply(prompt);
|
|
349
|
+
if (provider === "codex-app") return generateCodexAppReply(prompt);
|
|
350
|
+
return generateOllamaReply(prompt);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function generateOllamaReply(prompt) {
|
|
241
354
|
const response = await fetch("http://127.0.0.1:11434/api/generate", {
|
|
242
355
|
method: "POST",
|
|
243
356
|
headers: { "content-type": "application/json" },
|
|
@@ -253,6 +366,90 @@ async function generateReply(prompt) {
|
|
|
253
366
|
return cleanReply(body.response || "");
|
|
254
367
|
}
|
|
255
368
|
|
|
369
|
+
async function generateCodexReply(prompt) {
|
|
370
|
+
return runReplyCommand(codexCommand, prompt, "codex");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function generateCodexAppReply(prompt) {
|
|
374
|
+
const timeoutMs = numberArg("provider-timeout-ms", 300000);
|
|
375
|
+
const request = createCodexAppRequest(prompt);
|
|
376
|
+
console.log(`Waiting for Codex App bridge response: ${request.responsePath}`);
|
|
377
|
+
return await waitForCodexAppResponse(request.responsePath, timeoutMs);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function createCodexAppRequest(prompt) {
|
|
381
|
+
const requestId = `${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
|
|
382
|
+
const requestsDir = path.join(codexAppBridgeDir, "requests");
|
|
383
|
+
const responsesDir = path.join(codexAppBridgeDir, "responses");
|
|
384
|
+
fs.mkdirSync(requestsDir, { recursive: true });
|
|
385
|
+
fs.mkdirSync(responsesDir, { recursive: true });
|
|
386
|
+
const requestPath = path.join(requestsDir, `${requestId}.json`);
|
|
387
|
+
const responsePath = path.join(responsesDir, `${requestId}.json`);
|
|
388
|
+
writeJsonAtomic(requestPath, {
|
|
389
|
+
schemaVersion: 1,
|
|
390
|
+
requestId,
|
|
391
|
+
createdAt: new Date().toISOString(),
|
|
392
|
+
responsePath,
|
|
393
|
+
prompt,
|
|
394
|
+
instructions: [
|
|
395
|
+
"Write exactly one WhatsApp reply as the user.",
|
|
396
|
+
"Return JSON only: {\"reply\":\"...\"}.",
|
|
397
|
+
"No markdown, no explanations, no surrounding text.",
|
|
398
|
+
],
|
|
399
|
+
});
|
|
400
|
+
return { requestId, requestPath, responsePath };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function waitForCodexAppResponse(responsePath, timeoutMs) {
|
|
404
|
+
const startedAt = Date.now();
|
|
405
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
406
|
+
if (fs.existsSync(responsePath)) {
|
|
407
|
+
const response = parseJsonLine(fs.readFileSync(responsePath, "utf8"));
|
|
408
|
+
if (!response) throw new Error(`Codex App bridge wrote invalid JSON: ${responsePath}`);
|
|
409
|
+
if (response.error) throw new Error(`Codex App bridge error: ${response.error}`);
|
|
410
|
+
const reply = cleanReply(response.reply || "");
|
|
411
|
+
if (!reply) throw new Error(`Codex App bridge wrote an empty reply: ${responsePath}`);
|
|
412
|
+
return reply;
|
|
413
|
+
}
|
|
414
|
+
await sleep(1000);
|
|
415
|
+
}
|
|
416
|
+
throw new Error(`Codex App bridge timed out after ${timeoutMs}ms. No response at ${responsePath}`);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function runReplyCommand(command, prompt, label) {
|
|
420
|
+
const timeoutMs = numberArg("provider-timeout-ms", 120000);
|
|
421
|
+
const usesPromptFile = command.includes("{promptFile}");
|
|
422
|
+
const promptFile = usesPromptFile ? path.join(outboundDir, `reply-prompt-${Date.now()}-${Math.random().toString(16).slice(2)}.txt`) : "";
|
|
423
|
+
if (promptFile) fs.writeFileSync(promptFile, prompt, "utf8");
|
|
424
|
+
const renderedCommand = promptFile ? command.replaceAll("{promptFile}", shellQuote(promptFile)) : command;
|
|
425
|
+
return await new Promise((resolve, reject) => {
|
|
426
|
+
const child = spawn(renderedCommand, { shell: true, cwd: vault, env: process.env });
|
|
427
|
+
let stdout = "";
|
|
428
|
+
let stderr = "";
|
|
429
|
+
const timer = setTimeout(() => {
|
|
430
|
+
child.kill("SIGTERM");
|
|
431
|
+
reject(new Error(`${label} reply command timed out after ${timeoutMs}ms`));
|
|
432
|
+
}, timeoutMs);
|
|
433
|
+
child.stdout.on("data", (chunk) => { stdout += chunk.toString(); });
|
|
434
|
+
child.stderr.on("data", (chunk) => { stderr += chunk.toString(); });
|
|
435
|
+
child.on("error", (error) => {
|
|
436
|
+
clearTimeout(timer);
|
|
437
|
+
cleanupPromptFile(promptFile);
|
|
438
|
+
reject(error);
|
|
439
|
+
});
|
|
440
|
+
child.on("close", (code) => {
|
|
441
|
+
clearTimeout(timer);
|
|
442
|
+
cleanupPromptFile(promptFile);
|
|
443
|
+
if (code !== 0) {
|
|
444
|
+
reject(new Error(`${label} reply command failed with ${code}: ${summarize(stderr || stdout)}`));
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
resolve(cleanReply(stdout));
|
|
448
|
+
});
|
|
449
|
+
if (!usesPromptFile) child.stdin.end(prompt);
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
256
453
|
async function assertOllamaModel(modelName) {
|
|
257
454
|
let response;
|
|
258
455
|
try {
|
|
@@ -281,7 +478,9 @@ function cleanReply(value) {
|
|
|
281
478
|
}
|
|
282
479
|
|
|
283
480
|
function isAllowed(chatOrName) {
|
|
284
|
-
if (
|
|
481
|
+
if (isChatDenied(chatOrName)) return false;
|
|
482
|
+
if (isChatWhitelisted(chatOrName)) return true;
|
|
483
|
+
if (runtimeAllowAll) return true;
|
|
285
484
|
const name = typeof chatOrName === "string" ? chatOrName : chatName(chatOrName);
|
|
286
485
|
if (contactNumbers.some((number) => chatMatchesContact(chatOrName, number))) return true;
|
|
287
486
|
return allow.some((item) => name.toLowerCase().includes(item.toLowerCase()));
|
|
@@ -298,7 +497,7 @@ function isDenied(name) {
|
|
|
298
497
|
}
|
|
299
498
|
|
|
300
499
|
function shouldSkipNonPersonalChat(chatOrName, recentMessages) {
|
|
301
|
-
if (includeBusinesses || isExplicitlyAllowed(chatOrName)) return false;
|
|
500
|
+
if (includeBusinesses || isExplicitlyAllowed(chatOrName) || isChatWhitelisted(chatOrName)) return false;
|
|
302
501
|
const name = typeof chatOrName === "string" ? chatOrName : chatName(chatOrName);
|
|
303
502
|
return isLikelyBusinessOrAutomation(name, recentMessages);
|
|
304
503
|
}
|
|
@@ -356,9 +555,71 @@ function allowlistSummary() {
|
|
|
356
555
|
const parts = [];
|
|
357
556
|
if (allow.length) parts.push(`names: ${allow.join(", ")}`);
|
|
358
557
|
if (contactNumbers.length) parts.push(`contacts: ${contactNumbers.map(maskPhone).join(", ")}`);
|
|
558
|
+
const storedCount = Object.keys(whitelist.allowedChats || {}).length;
|
|
559
|
+
if (storedCount) parts.push(`stored: ${storedCount} chat(s)`);
|
|
359
560
|
return `Allowlist: ${parts.join("; ")}`;
|
|
360
561
|
}
|
|
361
562
|
|
|
563
|
+
function isChatWhitelisted(chatOrName) {
|
|
564
|
+
return Boolean(whitelist.allowedChats?.[chatKey(chatOrName)]);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function isChatDenied(chatOrName) {
|
|
568
|
+
return Boolean(whitelist.deniedChats?.[chatKey(chatOrName)]);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function setWhitelistDecision(chat, decision, source) {
|
|
572
|
+
const key = chatKey(chat);
|
|
573
|
+
const record = {
|
|
574
|
+
chatName: chatName(chat),
|
|
575
|
+
chatId: typeof chat === "string" ? null : chat.id?._serialized || null,
|
|
576
|
+
source,
|
|
577
|
+
updatedAt: new Date().toISOString(),
|
|
578
|
+
};
|
|
579
|
+
if (decision === "allow") {
|
|
580
|
+
whitelist.allowedChats[key] = record;
|
|
581
|
+
delete whitelist.deniedChats[key];
|
|
582
|
+
} else {
|
|
583
|
+
whitelist.deniedChats[key] = record;
|
|
584
|
+
delete whitelist.allowedChats[key];
|
|
585
|
+
}
|
|
586
|
+
writeJsonAtomic(whitelistPath, whitelist);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function chatKey(chatOrName) {
|
|
590
|
+
if (typeof chatOrName === "string") return `name:${chatOrName.toLowerCase()}`;
|
|
591
|
+
return chatOrName.id?._serialized || `name:${chatName(chatOrName).toLowerCase()}`;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function loadWhitelist() {
|
|
595
|
+
if (!fs.existsSync(whitelistPath)) return emptyWhitelist();
|
|
596
|
+
try {
|
|
597
|
+
return normalizeWhitelist(JSON.parse(fs.readFileSync(whitelistPath, "utf8")));
|
|
598
|
+
} catch {
|
|
599
|
+
return emptyWhitelist();
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function normalizeWhitelist(value) {
|
|
604
|
+
return {
|
|
605
|
+
schemaVersion: 1,
|
|
606
|
+
allowedChats: value?.allowedChats || {},
|
|
607
|
+
deniedChats: value?.deniedChats || {},
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function emptyWhitelist() {
|
|
612
|
+
return { schemaVersion: 1, allowedChats: {}, deniedChats: {} };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function parseIndexSelection(value, max) {
|
|
616
|
+
return [...new Set(String(value || "")
|
|
617
|
+
.split(/[,\s]+/)
|
|
618
|
+
.map((item) => Number(item.trim()))
|
|
619
|
+
.filter((item) => Number.isInteger(item) && item >= 1 && item <= max)
|
|
620
|
+
.map((item) => item - 1))];
|
|
621
|
+
}
|
|
622
|
+
|
|
362
623
|
function isCoolingDown(name) {
|
|
363
624
|
const last = state.lastSentAtByChat[name];
|
|
364
625
|
if (!last) return false;
|
|
@@ -463,6 +724,30 @@ function writeJsonAtomic(file, value) {
|
|
|
463
724
|
fs.renameSync(temp, file);
|
|
464
725
|
}
|
|
465
726
|
|
|
727
|
+
function cleanupPromptFile(file) {
|
|
728
|
+
if (file) {
|
|
729
|
+
try {
|
|
730
|
+
fs.unlinkSync(file);
|
|
731
|
+
} catch {
|
|
732
|
+
// ignore cleanup errors
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function sleep(ms) {
|
|
738
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function shellQuote(value) {
|
|
742
|
+
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
async function shutdown(code) {
|
|
746
|
+
if (rl) rl.close();
|
|
747
|
+
await client.destroy().catch(() => undefined);
|
|
748
|
+
process.exit(code);
|
|
749
|
+
}
|
|
750
|
+
|
|
466
751
|
function visibleMessage(record, reply) {
|
|
467
752
|
return outboundLogMode === "full" ? reply : `[metadata only, ${record.messageCharCount} chars, ${record.messageHash.slice(0, 12)}]`;
|
|
468
753
|
}
|
|
@@ -520,6 +805,6 @@ function parseArgs(argv) {
|
|
|
520
805
|
}
|
|
521
806
|
|
|
522
807
|
function usage() {
|
|
523
|
-
console.error('Usage: digital-brain auto-whatsapp --allow "Name" --contact "+15551234567" --model llama3.1 [--yes] [--allow-all] [--include-groups] [--include-businesses]');
|
|
808
|
+
console.error('Usage: digital-brain auto-whatsapp --allow "Name" --contact "+15551234567" --provider ollama|codex|codex-app --model llama3.1 [--yes] [--allow-all] [--include-groups] [--include-businesses]');
|
|
524
809
|
process.exit(1);
|
|
525
810
|
}
|