digital-brain 0.1.3 → 1.0.3
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 +38 -7
- package/bin/digital-brain.js +275 -38
- package/docs/AUTOMATIONS.md +13 -4
- package/docs/INTEGRATIONS.md +72 -0
- package/docs/PRIVACY.md +3 -1
- package/docs/SETUP.md +78 -0
- package/examples/sample-vault/{04 People/Interpreted Relationships/Close Friend.md → 06 AI Memory/Generated Relationship Drafts/Close Friend (WhatsApp).md } +4 -3
- package/examples/sample-vault/{04 People/Interpreted Relationships/Mom.md → 06 AI Memory/Generated Relationship Drafts/Mom (WhatsApp).md } +4 -3
- package/examples/sample-vault/{08 Sources/WhatsApp/Analysis/Interpreted/Project Team.md → 06 AI Memory/Generated Relationship Drafts/Project Team (WhatsApp).md } +4 -3
- package/examples/sample-vault/06 AI Memory/Interpreted Relationship Memory.md +3 -3
- package/examples/sample-vault/06 AI Memory/Person Context Index.md +26 -0
- package/examples/sample-vault/06 AI Memory/Person Reply Context.md +26 -0
- package/examples/sample-vault/08 Sources/{WhatsApp/Analysis/Interpreted/Close Friend.md → Analysis/Interpreted/Close Friend (WhatsApp).md } +4 -3
- package/examples/sample-vault/08 Sources/{WhatsApp/Analysis/Interpreted/Mom.md → Analysis/Interpreted/Mom (WhatsApp).md } +4 -3
- package/examples/sample-vault/{04 People/Interpreted Relationships/Project Team.md → 08 Sources/Analysis/Interpreted/Project Team (WhatsApp).md } +4 -3
- package/examples/sample-vault/08 Sources/Analysis/Relationship Map.md +38 -0
- package/examples/sample-vault/08 Sources/Analysis/interpreted_relationship_models.json +175 -0
- package/examples/sample-vault/08 Sources/Analysis/person_identity_map.json +78 -0
- package/examples/sample-vault/08 Sources/Analysis/relationship_profiles.json +122 -0
- package/examples/sample-vault/08 Sources/WhatsApp/Analysis/Interpreted/Close Friend (WhatsApp).md +44 -0
- package/examples/sample-vault/08 Sources/WhatsApp/Analysis/Interpreted/Mom (WhatsApp).md +45 -0
- package/examples/sample-vault/08 Sources/WhatsApp/Analysis/Interpreted/Project Team (WhatsApp).md +45 -0
- package/examples/sample-vault/08 Sources/WhatsApp/Analysis/Relationship Map.md +9 -3
- package/examples/sample-vault/08 Sources/WhatsApp/Analysis/interpreted_relationship_models.json +18 -0
- package/examples/sample-vault/08 Sources/WhatsApp/Analysis/person_identity_map.json +78 -0
- package/examples/sample-vault/08 Sources/WhatsApp/Analysis/relationship_profiles.json +18 -0
- package/examples/sample-vault/08 Sources/WhatsApp/Raw/2026-01-01.jsonl +6 -6
- package/lib/fs.js +33 -0
- package/package.json +2 -1
- package/scripts/digital_brain_imessage_sync.py +175 -0
- package/scripts/digital_brain_linkedin_export_import.py +214 -0
- package/scripts/digital_brain_relationship_extractor.py +189 -12
- package/scripts/digital_brain_relationship_interpreter.py +104 -15
- package/scripts/digital_brain_slack_export_import.py +181 -0
- package/scripts/digital_brain_whatsapp_mac_sync.py +37 -8
- package/templates/vault/00 Home/How AI Should Use This Vault.md +1 -1
- package/templates/vault/00 Home/Start Here.md +2 -1
- package/templates/vault/04 People/Relationship Overrides.md +2 -1
- package/templates/vault/06 AI Memory/Generated Relationship Drafts/README.md +5 -0
- package/templates/vault/06 AI Memory/Interpreted Relationship Memory.md +1 -2
- package/templates/vault/06 AI Memory/Person Context Index.md +4 -0
- package/templates/vault/06 AI Memory/Person Reply Context.md +4 -0
- package/templates/vault/08 Sources/README.md +5 -0
- package/templates/vault/08 Sources/WhatsApp/Outbound/README.md +2 -2
- package/templates/vault/AGENTS.md +5 -1
- package/templates/vault/CLAUDE.md +3 -0
- package/templates/vault/GEMINI.md +4 -0
- package/whatsapp-web/send.mjs +32 -5
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ Digital Brain gives them a structured, local map of:
|
|
|
24
24
|
npx digital-brain init
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
The installer asks a short setup quiz: history window, primary focus, refresh cadence, always-on interval, active time window, outbound mode, and AI adapter setup.
|
|
27
|
+
The installer asks a short setup quiz: setup mode, history window, primary focus, refresh cadence, always-on interval, active time window, outbound mode, and AI adapter setup.
|
|
28
28
|
|
|
29
29
|
The quiz is mostly multiple choice. Pick with `A/B/C`, `1/2/3`, the exact value, or press Enter to accept the default. If you skip the vault path, Digital Brain creates a new folder in the current directory:
|
|
30
30
|
|
|
@@ -38,7 +38,7 @@ For a non-interactive setup:
|
|
|
38
38
|
npx digital-brain init --yes
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
Inside the quiz, choose `Auto mode` to use recommended always-on local refresh settings. You can also start there directly:
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
44
|
npx digital-brain init --full-auto
|
|
@@ -46,6 +46,8 @@ npx digital-brain init --full-auto
|
|
|
46
46
|
|
|
47
47
|
Full-auto means local repeated refreshes. It does not mean blind auto-send. WhatsApp sending still defaults to drafts or explicit confirmation, and the AI-disclosure guard stays enabled.
|
|
48
48
|
|
|
49
|
+
`init` also runs a setup check. npm installs the package dependencies automatically, and Digital Brain does not require pip packages. If Python or a selected source is missing, the check prints the exact next step and setup link. See [docs/SETUP.md](docs/SETUP.md).
|
|
50
|
+
|
|
49
51
|
For local development:
|
|
50
52
|
|
|
51
53
|
```bash
|
|
@@ -58,6 +60,9 @@ node ./bin/digital-brain.js init ./Digital Brain\ Vault
|
|
|
58
60
|
- An Obsidian-friendly Markdown vault.
|
|
59
61
|
- AI adapter files for Codex, Claude, and Gemini.
|
|
60
62
|
- WhatsApp Mac import tools.
|
|
63
|
+
- Apple iMessage import tools.
|
|
64
|
+
- Slack export import tools.
|
|
65
|
+
- LinkedIn data archive import tools.
|
|
61
66
|
- Relationship extraction and interpretation models.
|
|
62
67
|
- Optional WhatsApp Web outbound sender.
|
|
63
68
|
- A refresh script based on your install-time answers.
|
|
@@ -66,10 +71,15 @@ node ./bin/digital-brain.js init ./Digital Brain\ Vault
|
|
|
66
71
|
## What It Can Do
|
|
67
72
|
|
|
68
73
|
- Import recent WhatsApp history from the local macOS WhatsApp database.
|
|
74
|
+
- Import recent iMessage history from the local macOS Messages database.
|
|
75
|
+
- Import Slack workspace exports.
|
|
76
|
+
- Import LinkedIn data archives for connections and messages when available.
|
|
69
77
|
- Build relationship profiles from message patterns.
|
|
78
|
+
- Merge confirmed-looking same-person profiles across sources into a person context index.
|
|
70
79
|
- Infer provisional roles like parent, family group, work collaborator, close personal contact, or unlabeled contact.
|
|
71
80
|
- Extract relationship-specific typing style: casing, message length, punctuation, emoji, and slang.
|
|
72
81
|
- Generate "how to continue this relationship" notes.
|
|
82
|
+
- Generate reply-ready person context that keeps WhatsApp, iMessage, Slack, and LinkedIn evidence separate under the same person.
|
|
73
83
|
- Create AI-readable memory files for future prompts.
|
|
74
84
|
- Draft WhatsApp sends by default, and only send with explicit `--yes`.
|
|
75
85
|
- Enforce an AI-disclosure guard after repeated AI-assisted sends.
|
|
@@ -77,11 +87,28 @@ node ./bin/digital-brain.js init ./Digital Brain\ Vault
|
|
|
77
87
|
## Core Commands
|
|
78
88
|
|
|
79
89
|
```bash
|
|
80
|
-
digital-brain init
|
|
81
|
-
digital-brain
|
|
82
|
-
digital-brain
|
|
83
|
-
digital-brain
|
|
84
|
-
digital-brain
|
|
90
|
+
digital-brain init
|
|
91
|
+
digital-brain run
|
|
92
|
+
digital-brain doctor
|
|
93
|
+
digital-brain sync-imessage --days 30
|
|
94
|
+
digital-brain import-slack --input ./slack-export.zip
|
|
95
|
+
digital-brain import-linkedin --input ./linkedin-archive.zip
|
|
96
|
+
digital-brain send-whatsapp --to "Name" --message "text"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`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.
|
|
100
|
+
|
|
101
|
+
Slack and LinkedIn are import-based. Digital Brain reads official export archives; it does not scrape LinkedIn or automate private app UIs. See [docs/INTEGRATIONS.md](docs/INTEGRATIONS.md).
|
|
102
|
+
|
|
103
|
+
Use `digital-brain doctor` or `digital-brain tutorial` anytime to see dependency status and next steps.
|
|
104
|
+
|
|
105
|
+
The lower-level commands still exist for debugging:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
digital-brain sync-whatsapp
|
|
109
|
+
digital-brain sync-imessage
|
|
110
|
+
digital-brain extract
|
|
111
|
+
digital-brain interpret
|
|
85
112
|
```
|
|
86
113
|
|
|
87
114
|
The sender drafts by default. Add `--yes` to actually send.
|
|
@@ -132,6 +159,10 @@ Digital Brain is local-first. It does not upload messages or notes.
|
|
|
132
159
|
|
|
133
160
|
WhatsApp support reads the local macOS WhatsApp database when available. This is experimental and unofficial. Outbound messaging uses WhatsApp Web through `whatsapp-web.js`.
|
|
134
161
|
|
|
162
|
+
Apple iMessage support reads the local macOS Messages `chat.db` when available. If the database is missing or inaccessible and iMessage was selected, `digital-brain run` fails with a clear setup error instead of silently skipping it.
|
|
163
|
+
|
|
164
|
+
Slack support reads Slack workspace export JSON. LinkedIn support reads LinkedIn data archive CSV files when LinkedIn includes the relevant files in your archive.
|
|
165
|
+
|
|
135
166
|
Relationship labels are working notes, not truth. You can edit them with `relationship_overrides.json`.
|
|
136
167
|
|
|
137
168
|
Always-on and outbound modes depend on local app databases, WhatsApp Web, and third-party behavior that can change. You are responsible for consent, privacy, message content, and anything sent from your machine.
|
package/bin/digital-brain.js
CHANGED
|
@@ -5,9 +5,10 @@ import os from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import readline from "node:readline/promises";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { copyDir, ensureDir, packageRoot, resolveVault } from "../lib/fs.js";
|
|
8
|
+
import { copyDir, ensureDir, packageRoot, resolveVault, writeDefaultVault } from "../lib/fs.js";
|
|
9
9
|
|
|
10
10
|
const root = packageRoot(import.meta.url);
|
|
11
|
+
const CONFIG_SCHEMA_VERSION = 1;
|
|
11
12
|
|
|
12
13
|
main().catch((error) => {
|
|
13
14
|
console.error(error.message);
|
|
@@ -19,8 +20,13 @@ async function main() {
|
|
|
19
20
|
const args = parseArgs(argv);
|
|
20
21
|
|
|
21
22
|
if (command === "init") await init(argv, args);
|
|
23
|
+
else if (command === "run" || command === "refresh") runRefresh(argv, args);
|
|
22
24
|
else if (command === "doctor") doctor();
|
|
25
|
+
else if (command === "tutorial" || command === "setup-check") doctor({ tutorial: true });
|
|
23
26
|
else if (command === "sync-whatsapp") runPython("digital_brain_whatsapp_mac_sync.py", argv);
|
|
27
|
+
else if (command === "sync-imessage") runPython("digital_brain_imessage_sync.py", argv);
|
|
28
|
+
else if (command === "import-slack") runPython("digital_brain_slack_export_import.py", argv);
|
|
29
|
+
else if (command === "import-linkedin") runPython("digital_brain_linkedin_export_import.py", argv);
|
|
24
30
|
else if (command === "extract") runPython("digital_brain_relationship_extractor.py", argv);
|
|
25
31
|
else if (command === "interpret") runPython("digital_brain_relationship_interpreter.py", argv);
|
|
26
32
|
else if (command === "send-whatsapp") runNode("whatsapp-web/send.mjs", argv);
|
|
@@ -31,6 +37,7 @@ async function init(argv, args) {
|
|
|
31
37
|
const positional = argv.filter((arg) => !arg.startsWith("--"));
|
|
32
38
|
const defaultVault = path.resolve(process.cwd(), "Digital Brain Vault");
|
|
33
39
|
const fullAuto = toBoolean(args["full-auto"]);
|
|
40
|
+
let setupMode = fullAuto ? "full-auto" : "guided";
|
|
34
41
|
let vault = positional[0] ? path.resolve(positional[0]) : args.yes ? defaultVault : "";
|
|
35
42
|
let selfName = args["self-name"] || "";
|
|
36
43
|
let connectAi = toBoolean(args["connect-ai"]);
|
|
@@ -41,6 +48,9 @@ async function init(argv, args) {
|
|
|
41
48
|
let activeWindow = args["active-window"] || "08:00-12:00";
|
|
42
49
|
let timezone = args.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || "local";
|
|
43
50
|
let outboundMode = args["outbound-mode"] || "draft";
|
|
51
|
+
let privacyMode = args["privacy-mode"] || "standard";
|
|
52
|
+
let sourceMarkdownMode = args["source-markdown-mode"] || "none";
|
|
53
|
+
let selectedSources = parseList(args.sources || "whatsapp");
|
|
44
54
|
let responsibilityAccepted = fullAuto || schedule === "always-on";
|
|
45
55
|
|
|
46
56
|
if (!args.yes) {
|
|
@@ -49,18 +59,28 @@ async function init(argv, args) {
|
|
|
49
59
|
vault ||= path.resolve(await ask(rl, "📁 Vault path", defaultVault, "Enter creates this folder if it does not exist."));
|
|
50
60
|
selfName ||= await ask(rl, "👤 Your name", "Me");
|
|
51
61
|
dataWindowDays = await askNumber(rl, "🕰️ History to import", dataWindowDays, { suffix: "days", min: 1 });
|
|
62
|
+
setupMode = await select(rl, "Setup mode", [
|
|
63
|
+
["guided", "Guided", "Pick each setting yourself.", "🧭"],
|
|
64
|
+
["full-auto", "Auto mode", "Use recommended always-on local refresh settings.", "⚡"],
|
|
65
|
+
], setupMode);
|
|
52
66
|
focus ||= await select(rl, "Primary focus", [
|
|
53
67
|
["relationship-memory", "Relationship memory", "Map people, tone, and recurring patterns.", "🧠"],
|
|
54
68
|
["reply-help", "Reply help", "Prioritize drafting guidance and typing-style matching.", "💬"],
|
|
55
69
|
["work-context", "Work context", "Prioritize collaborators, projects, and operational notes.", "💼"],
|
|
56
70
|
], "relationship-memory");
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
if (setupMode === "full-auto") {
|
|
72
|
+
schedule = "always-on";
|
|
73
|
+
console.log("");
|
|
74
|
+
console.log("⚡ Auto mode selected: always-on local refresh, draft-first outbound, AI disclosure guard enabled.");
|
|
75
|
+
} else {
|
|
76
|
+
schedule = await select(rl, "Refresh cadence", [
|
|
77
|
+
["manual", "Manual", "Only runs when you run a command.", "🖐️"],
|
|
78
|
+
["daily", "Daily", "Good for a low-maintenance personal vault.", "🌅"],
|
|
79
|
+
["hourly", "Hourly", "Keeps memory warm without running constantly.", "⏱️"],
|
|
80
|
+
["every-30-min", "Every 30 minutes", "Useful for morning or work-window guidance.", "🔁"],
|
|
81
|
+
["always-on", "Always-on local loop", "Runs repeatedly while your computer is awake.", "⚡"],
|
|
82
|
+
], schedule);
|
|
83
|
+
}
|
|
64
84
|
if (schedule === "always-on") {
|
|
65
85
|
refreshIntervalMinutes = await askNumber(rl, "⏳ Always-on pull interval", refreshIntervalMinutes, {
|
|
66
86
|
suffix: "minutes",
|
|
@@ -68,6 +88,16 @@ async function init(argv, args) {
|
|
|
68
88
|
});
|
|
69
89
|
}
|
|
70
90
|
activeWindow = await ask(rl, "🪟 Active window for frequent refreshes", activeWindow);
|
|
91
|
+
selectedSources = await multiSelect(rl, "Sources to set up", [
|
|
92
|
+
["whatsapp", "WhatsApp Mac", "Live local sync from WhatsApp for Mac database.", "💚"],
|
|
93
|
+
["imessage", "Apple iMessage", "Live local sync from macOS Messages database.", "💬"],
|
|
94
|
+
["slack", "Slack export", "Import official Slack workspace export ZIP/folder.", "🧵"],
|
|
95
|
+
["linkedin", "LinkedIn archive", "Import official LinkedIn data archive ZIP/folder.", "💼"],
|
|
96
|
+
], selectedSources);
|
|
97
|
+
privacyMode = await select(rl, "Privacy mode", [
|
|
98
|
+
["standard", "Standard", "Keep raw JSONL locally for analysis, but do not generate raw chat Markdown.", "🔐"],
|
|
99
|
+
["metadata-only", "Metadata only", "Store timestamps and participants, but omit message bodies.", "🧼"],
|
|
100
|
+
], privacyMode);
|
|
71
101
|
outboundMode = await select(rl, "WhatsApp outbound mode", [
|
|
72
102
|
["disabled", "Disabled", "Never prepares WhatsApp sends.", "🔒"],
|
|
73
103
|
["draft", "Draft only", "Prepares text and requires you to send it.", "✍️"],
|
|
@@ -79,6 +109,7 @@ async function init(argv, args) {
|
|
|
79
109
|
console.log("Full-auto/outbound confirmation was not accepted. Using manual refresh and draft-only outbound.");
|
|
80
110
|
schedule = "manual";
|
|
81
111
|
outboundMode = "draft";
|
|
112
|
+
setupMode = "guided";
|
|
82
113
|
}
|
|
83
114
|
rl.close();
|
|
84
115
|
}
|
|
@@ -86,6 +117,7 @@ async function init(argv, args) {
|
|
|
86
117
|
ensureDir(vault);
|
|
87
118
|
copyDir(path.join(root, "templates", "vault"), vault);
|
|
88
119
|
const config = {
|
|
120
|
+
schemaVersion: CONFIG_SCHEMA_VERSION,
|
|
89
121
|
selfName: selfName || "Me",
|
|
90
122
|
dataWindowDays,
|
|
91
123
|
focus: focus || "relationship-memory",
|
|
@@ -94,7 +126,11 @@ async function init(argv, args) {
|
|
|
94
126
|
activeWindow,
|
|
95
127
|
timezone,
|
|
96
128
|
outboundMode,
|
|
97
|
-
|
|
129
|
+
privacyMode,
|
|
130
|
+
sourceMarkdownMode,
|
|
131
|
+
selectedSources,
|
|
132
|
+
outboundLogMode: args["outbound-log-mode"] || "metadata",
|
|
133
|
+
setupMode,
|
|
98
134
|
responsibilityAccepted,
|
|
99
135
|
defaults: {
|
|
100
136
|
enterUsesDefault: true,
|
|
@@ -109,6 +145,7 @@ async function init(argv, args) {
|
|
|
109
145
|
},
|
|
110
146
|
};
|
|
111
147
|
writeConfig(vault, config);
|
|
148
|
+
writeDefaultVault(vault);
|
|
112
149
|
writeRefreshScript(vault, config);
|
|
113
150
|
writeWatchScript(vault, config);
|
|
114
151
|
|
|
@@ -120,47 +157,207 @@ async function init(argv, args) {
|
|
|
120
157
|
|
|
121
158
|
console.log(`Digital Brain vault created: ${vault}`);
|
|
122
159
|
console.log(`Config: ${path.join(vault, "digital-brain.config.json")}`);
|
|
160
|
+
console.log(`Default vault saved: ${vault}`);
|
|
123
161
|
console.log(`Refresh script: ${path.join(vault, "Tools", "digital-brain-refresh.sh")}`);
|
|
124
162
|
console.log(`Always-on script: ${path.join(vault, "Tools", "digital-brain-watch.sh")}`);
|
|
125
163
|
console.log("Next:");
|
|
126
|
-
console.log(
|
|
127
|
-
console.log(` digital-brain extract --vault "${vault}" --days ${dataWindowDays}`);
|
|
128
|
-
console.log(` digital-brain interpret --vault "${vault}" --days ${dataWindowDays}`);
|
|
164
|
+
console.log(" digital-brain run");
|
|
129
165
|
if (schedule === "always-on") console.log(` "${path.join(vault, "Tools", "digital-brain-watch.sh")}"`);
|
|
166
|
+
printSetupCheck(vault, { tutorial: true });
|
|
130
167
|
}
|
|
131
168
|
|
|
132
|
-
function doctor() {
|
|
133
|
-
|
|
134
|
-
["node", process.version],
|
|
135
|
-
["python3", shell("python3", ["--version"])],
|
|
136
|
-
["sqlite3", shell("sqlite3", ["--version"])],
|
|
137
|
-
["ollama", shell("ollama", ["--version"], true)],
|
|
138
|
-
];
|
|
139
|
-
for (const [name, value] of checks) {
|
|
140
|
-
console.log(`${name}: ${value || "not found"}`);
|
|
141
|
-
}
|
|
142
|
-
const macDb = path.join(os.homedir(), "Library/Group Containers/group.net.whatsapp.WhatsApp.shared/ChatStorage.sqlite");
|
|
143
|
-
console.log(`WhatsApp Mac DB: ${fs.existsSync(macDb) ? macDb : "not found"}`);
|
|
169
|
+
function doctor(options = {}) {
|
|
170
|
+
printSetupCheck(resolveVault(process.cwd()), options);
|
|
144
171
|
}
|
|
145
172
|
|
|
146
173
|
function runPython(script, argv) {
|
|
147
|
-
const
|
|
148
|
-
const result = spawnSync("python3", [path.join(root, "scripts", script), ...resolved], { stdio: "inherit" });
|
|
174
|
+
const result = runPythonStep(script, withVault(argv));
|
|
149
175
|
process.exit(result.status ?? 1);
|
|
150
176
|
}
|
|
151
177
|
|
|
178
|
+
function runRefresh(argv, args) {
|
|
179
|
+
const vault = getVaultFromArgs(argv);
|
|
180
|
+
const config = readVaultConfig(vault);
|
|
181
|
+
const days = String(args.days || args["data-window-days"] || config.dataWindowDays || 30);
|
|
182
|
+
const markdownMode = args["markdown-mode"] || config.sourceMarkdownMode || "none";
|
|
183
|
+
const privacyMode = args["privacy-mode"] || config.privacyMode || "standard";
|
|
184
|
+
const selectedSources = parseList(args.sources || "").length ? parseList(args.sources) : config.selectedSources || ["whatsapp"];
|
|
185
|
+
const syncArgs = ["--vault", vault, "--days", days, "--markdown-mode", markdownMode, "--privacy-mode", privacyMode];
|
|
186
|
+
const extractArgs = ["--vault", vault, "--days", days];
|
|
187
|
+
const interpretArgs = ["--vault", vault, "--days", days];
|
|
188
|
+
if (args["min-messages"]) extractArgs.push("--min-messages", String(args["min-messages"]));
|
|
189
|
+
console.log(`Digital Brain refresh: ${vault}`);
|
|
190
|
+
if (toBoolean(args["dry-run"])) {
|
|
191
|
+
console.log("Dry run. Planned steps:");
|
|
192
|
+
if (selectedSources.includes("whatsapp")) console.log(` sync WhatsApp: days=${days}, markdown=${markdownMode}, privacy=${privacyMode}`);
|
|
193
|
+
if (selectedSources.includes("imessage")) console.log(` sync iMessage: days=${days}, markdown=${markdownMode}, privacy=${privacyMode}`);
|
|
194
|
+
if (selectedSources.includes("slack")) console.log(" Slack: import-only; run digital-brain import-slack --input <export.zip>");
|
|
195
|
+
if (selectedSources.includes("linkedin")) console.log(" LinkedIn: import-only; run digital-brain import-linkedin --input <archive.zip>");
|
|
196
|
+
console.log(` extract relationships: days=${days}`);
|
|
197
|
+
console.log(` interpret relationship drafts: days=${days}`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const steps = [];
|
|
201
|
+
if (selectedSources.includes("whatsapp")) steps.push(["sync WhatsApp", "sync-whatsapp", "digital_brain_whatsapp_mac_sync.py", syncArgs]);
|
|
202
|
+
if (selectedSources.includes("imessage")) steps.push(["sync iMessage", "sync-imessage", "digital_brain_imessage_sync.py", syncArgs]);
|
|
203
|
+
steps.push(
|
|
204
|
+
["extract", "extract", "digital_brain_relationship_extractor.py", extractArgs],
|
|
205
|
+
["interpret", "interpret", "digital_brain_relationship_interpreter.py", interpretArgs],
|
|
206
|
+
);
|
|
207
|
+
for (const [label, skipKey, script, stepArgs] of steps) {
|
|
208
|
+
const skipRequested = toBoolean(args[`skip-${skipKey}`]) || (skipKey.startsWith("sync-") && toBoolean(args["skip-sync"]));
|
|
209
|
+
if (skipRequested) {
|
|
210
|
+
console.log(`\n→ ${label} skipped`);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
console.log(`\n→ ${label}`);
|
|
214
|
+
const result = runPythonStep(script, stepArgs);
|
|
215
|
+
if ((result.status ?? 1) !== 0) process.exit(result.status ?? 1);
|
|
216
|
+
}
|
|
217
|
+
console.log("\nDigital Brain refresh complete.");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function runPythonStep(script, argv) {
|
|
221
|
+
return spawnSync("python3", [path.join(root, "scripts", script), ...argv], { stdio: "inherit" });
|
|
222
|
+
}
|
|
223
|
+
|
|
152
224
|
function runNode(script, argv) {
|
|
153
|
-
const result = spawnSync(process.execPath, [path.join(root, script), ...argv], { stdio: "inherit" });
|
|
225
|
+
const result = spawnSync(process.execPath, [path.join(root, script), ...withVault(argv)], { stdio: "inherit" });
|
|
154
226
|
process.exit(result.status ?? 1);
|
|
155
227
|
}
|
|
156
228
|
|
|
157
229
|
function withVault(argv) {
|
|
158
230
|
if (argv.includes("--vault") || argv.some((arg) => arg.startsWith("--vault="))) return argv;
|
|
159
|
-
return ["--vault",
|
|
231
|
+
return ["--vault", getVaultFromArgs(argv), ...argv];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function getVaultFromArgs(argv) {
|
|
235
|
+
const args = parseArgs(argv);
|
|
236
|
+
if (args.vault) return path.resolve(String(args.vault));
|
|
237
|
+
return resolveVault(process.cwd());
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function readVaultConfig(vault) {
|
|
241
|
+
const file = path.join(vault, "digital-brain.config.json");
|
|
242
|
+
if (!fs.existsSync(file)) {
|
|
243
|
+
console.error(`No Digital Brain vault found. Run "digital-brain init" first, or pass --vault <path>.`);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function printSetupCheck(vault, options = {}) {
|
|
250
|
+
const config = fs.existsSync(path.join(vault, "digital-brain.config.json")) ? readVaultConfig(vault) : {};
|
|
251
|
+
const selectedSources = config.selectedSources || ["whatsapp"];
|
|
252
|
+
const pythonVersion = shell("python3", ["--version"], true);
|
|
253
|
+
const pythonSqlite = shell("python3", ["-c", "import sqlite3; print('ok')"], true);
|
|
254
|
+
const ollamaVersion = shell("ollama", ["--version"], true);
|
|
255
|
+
const macDb = path.join(os.homedir(), "Library/Group Containers/group.net.whatsapp.WhatsApp.shared/ChatStorage.sqlite");
|
|
256
|
+
const messagesDb = path.join(os.homedir(), "Library/Messages/chat.db");
|
|
257
|
+
const checks = [
|
|
258
|
+
{
|
|
259
|
+
label: "Node",
|
|
260
|
+
ok: nodeMajor() >= 20,
|
|
261
|
+
value: process.version,
|
|
262
|
+
hint: "Install Node 20 or newer from https://nodejs.org.",
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
label: "Package dependencies",
|
|
266
|
+
ok: true,
|
|
267
|
+
value: "installed by npm",
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
label: "Python 3",
|
|
271
|
+
ok: Boolean(pythonVersion),
|
|
272
|
+
value: pythonVersion || "not found",
|
|
273
|
+
hint: "Install with: brew install python",
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
label: "Python sqlite3",
|
|
277
|
+
ok: pythonSqlite === "ok",
|
|
278
|
+
value: pythonSqlite === "ok" ? "available" : "not found",
|
|
279
|
+
hint: "Use a Python 3 build with sqlite3 support.",
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
label: "Ollama",
|
|
283
|
+
ok: Boolean(ollamaVersion),
|
|
284
|
+
value: ollamaVersion || "optional",
|
|
285
|
+
hint: "Optional local LLM: brew install ollama",
|
|
286
|
+
optional: true,
|
|
287
|
+
},
|
|
288
|
+
];
|
|
289
|
+
if (selectedSources.includes("whatsapp")) {
|
|
290
|
+
checks.push({
|
|
291
|
+
label: "WhatsApp Mac database",
|
|
292
|
+
ok: fs.existsSync(macDb),
|
|
293
|
+
value: fs.existsSync(macDb) ? "found" : "not found yet",
|
|
294
|
+
hint: "Install/open WhatsApp for Mac, log in, then grant Terminal Full Disk Access if needed: https://faq.whatsapp.com/686469079565350",
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
if (selectedSources.includes("imessage")) {
|
|
298
|
+
checks.push({
|
|
299
|
+
label: "Apple Messages database",
|
|
300
|
+
ok: fs.existsSync(messagesDb),
|
|
301
|
+
value: fs.existsSync(messagesDb) ? "found" : "not found yet",
|
|
302
|
+
hint: "Open Messages on macOS and grant Terminal Full Disk Access if needed: https://support.apple.com/guide/messages/welcome/mac",
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
if (selectedSources.includes("slack")) {
|
|
306
|
+
checks.push({
|
|
307
|
+
label: "Slack export",
|
|
308
|
+
ok: true,
|
|
309
|
+
value: "import manually",
|
|
310
|
+
hint: "Export guide: https://slack.com/help/articles/201658943-Export-your-workspace-data",
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
if (selectedSources.includes("linkedin")) {
|
|
314
|
+
checks.push({
|
|
315
|
+
label: "LinkedIn archive",
|
|
316
|
+
ok: true,
|
|
317
|
+
value: "import manually",
|
|
318
|
+
hint: "Export guide: https://www.linkedin.com/help/linkedin/answer/a566336",
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
console.log("");
|
|
323
|
+
console.log("Setup check");
|
|
324
|
+
for (const check of checks) {
|
|
325
|
+
const icon = check.ok ? "✓" : check.optional ? "○" : "!";
|
|
326
|
+
console.log(` ${icon} ${check.label}: ${check.value}`);
|
|
327
|
+
if (!check.ok && check.hint) console.log(` ${check.hint}`);
|
|
328
|
+
}
|
|
329
|
+
if (fs.existsSync(path.join(vault, "digital-brain.config.json"))) {
|
|
330
|
+
console.log(` ✓ Default vault: ${vault}`);
|
|
331
|
+
} else {
|
|
332
|
+
console.log(" ! Default vault: not set");
|
|
333
|
+
console.log(" Run: digital-brain init");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (options.tutorial) {
|
|
337
|
+
console.log("");
|
|
338
|
+
console.log("How to use it");
|
|
339
|
+
let step = 1;
|
|
340
|
+
if (selectedSources.includes("whatsapp")) {
|
|
341
|
+
console.log(` ${step++}. Open WhatsApp for Mac once and keep it logged in.`);
|
|
342
|
+
}
|
|
343
|
+
if (selectedSources.includes("imessage")) {
|
|
344
|
+
console.log(` ${step++}. Open Messages on macOS once and grant Terminal Full Disk Access if needed.`);
|
|
345
|
+
}
|
|
346
|
+
if (selectedSources.includes("slack")) {
|
|
347
|
+
console.log(` ${step++}. Download a Slack export, then run: digital-brain import-slack --input <export.zip>`);
|
|
348
|
+
}
|
|
349
|
+
if (selectedSources.includes("linkedin")) {
|
|
350
|
+
console.log(` ${step++}. Download a LinkedIn archive, then run: digital-brain import-linkedin --input <archive.zip>`);
|
|
351
|
+
}
|
|
352
|
+
console.log(` ${step++}. Run: digital-brain run`);
|
|
353
|
+
console.log(` ${step}. Ask your AI to use the generated vault notes for personal context.`);
|
|
354
|
+
console.log("");
|
|
355
|
+
console.log("No pip install is needed. npm installs the package dependencies.");
|
|
356
|
+
}
|
|
160
357
|
}
|
|
161
358
|
|
|
162
359
|
function writeConfig(vault, config) {
|
|
163
|
-
|
|
360
|
+
writeFileAtomic(path.join(vault, "digital-brain.config.json"), `${JSON.stringify(config, null, 2)}\n`);
|
|
164
361
|
}
|
|
165
362
|
|
|
166
363
|
function writeRefreshScript(vault, config) {
|
|
@@ -173,14 +370,12 @@ set -euo pipefail
|
|
|
173
370
|
VAULT="${vault.replace(/"/g, '\\"')}"
|
|
174
371
|
DAYS="${days}"
|
|
175
372
|
|
|
176
|
-
digital-brain
|
|
177
|
-
digital-brain extract --vault "$VAULT" --days "$DAYS"
|
|
178
|
-
digital-brain interpret --vault "$VAULT" --days "$DAYS"
|
|
373
|
+
digital-brain run --vault "$VAULT" --days "$DAYS"
|
|
179
374
|
|
|
180
375
|
echo "Digital Brain refresh complete for $VAULT"
|
|
181
376
|
`;
|
|
182
377
|
const scriptPath = path.join(toolsDir, "digital-brain-refresh.sh");
|
|
183
|
-
|
|
378
|
+
writeFileAtomic(scriptPath, content);
|
|
184
379
|
fs.chmodSync(scriptPath, 0o755);
|
|
185
380
|
}
|
|
186
381
|
|
|
@@ -203,7 +398,7 @@ while true; do
|
|
|
203
398
|
done
|
|
204
399
|
`;
|
|
205
400
|
const scriptPath = path.join(toolsDir, "digital-brain-watch.sh");
|
|
206
|
-
|
|
401
|
+
writeFileAtomic(scriptPath, content);
|
|
207
402
|
fs.chmodSync(scriptPath, 0o755);
|
|
208
403
|
}
|
|
209
404
|
|
|
@@ -224,6 +419,12 @@ Use it as local personal context when the user asks about preferences, relations
|
|
|
224
419
|
console.log(`${label} pointer: ${file}`);
|
|
225
420
|
}
|
|
226
421
|
|
|
422
|
+
function writeFileAtomic(file, content) {
|
|
423
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
424
|
+
fs.writeFileSync(temp, content);
|
|
425
|
+
fs.renameSync(temp, file);
|
|
426
|
+
}
|
|
427
|
+
|
|
227
428
|
function printSetupHeader(defaultVault) {
|
|
228
429
|
console.log("");
|
|
229
430
|
console.log("╭────────────────────────────────────────╮");
|
|
@@ -271,6 +472,28 @@ async function select(rl, label, options, fallback) {
|
|
|
271
472
|
return exact ? exact[0] : options[defaultIndex][0];
|
|
272
473
|
}
|
|
273
474
|
|
|
475
|
+
async function multiSelect(rl, label, options, fallbackValues) {
|
|
476
|
+
const fallback = fallbackValues.length ? fallbackValues : [options[0][0]];
|
|
477
|
+
console.log("");
|
|
478
|
+
console.log(`◇ ${label}`);
|
|
479
|
+
options.forEach(([, title, description, icon = "•"], index) => {
|
|
480
|
+
const selected = fallback.includes(options[index][0]) ? " ← default" : "";
|
|
481
|
+
const letter = letterFor(index);
|
|
482
|
+
console.log(` ${letter}) ${icon} ${title}${selected}`);
|
|
483
|
+
console.log(` ${description}`);
|
|
484
|
+
});
|
|
485
|
+
const answer = await rl.question(`Choose one or more, comma-separated [${fallback.join(",")}]: `);
|
|
486
|
+
if (!answer.trim()) return fallback;
|
|
487
|
+
const values = [];
|
|
488
|
+
for (const token of answer.split(",").map((value) => value.trim()).filter(Boolean)) {
|
|
489
|
+
const letterIndex = indexFromLetter(token);
|
|
490
|
+
const numericIndex = Number(token) - 1;
|
|
491
|
+
const option = options[letterIndex] || options[numericIndex] || options.find(([value, title]) => value === token || title.toLowerCase() === token.toLowerCase());
|
|
492
|
+
if (option && !values.includes(option[0])) values.push(option[0]);
|
|
493
|
+
}
|
|
494
|
+
return values.length ? values : fallback;
|
|
495
|
+
}
|
|
496
|
+
|
|
274
497
|
async function confirm(rl, label, fallback) {
|
|
275
498
|
const hint = fallback ? "Y/n" : "y/N";
|
|
276
499
|
const answer = (await rl.question(`${label} [${hint}]: `)).trim().toLowerCase();
|
|
@@ -306,6 +529,10 @@ function shell(command, args, optional = false) {
|
|
|
306
529
|
return (result.stdout || result.stderr).trim().split("\n")[0];
|
|
307
530
|
}
|
|
308
531
|
|
|
532
|
+
function nodeMajor() {
|
|
533
|
+
return Number(process.version.replace(/^v/, "").split(".")[0]);
|
|
534
|
+
}
|
|
535
|
+
|
|
309
536
|
function parseArgs(argv) {
|
|
310
537
|
const out = {};
|
|
311
538
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -324,6 +551,11 @@ function parseArgs(argv) {
|
|
|
324
551
|
return out;
|
|
325
552
|
}
|
|
326
553
|
|
|
554
|
+
function parseList(value) {
|
|
555
|
+
if (!value) return [];
|
|
556
|
+
return String(value).split(",").map((item) => item.trim()).filter(Boolean);
|
|
557
|
+
}
|
|
558
|
+
|
|
327
559
|
function toBoolean(value) {
|
|
328
560
|
if (value === undefined) return false;
|
|
329
561
|
if (value === true) return true;
|
|
@@ -341,10 +573,15 @@ function help() {
|
|
|
341
573
|
|
|
342
574
|
Usage:
|
|
343
575
|
digital-brain init [vault]
|
|
576
|
+
digital-brain run
|
|
344
577
|
digital-brain doctor
|
|
345
|
-
digital-brain
|
|
346
|
-
digital-brain
|
|
347
|
-
digital-brain
|
|
348
|
-
digital-brain
|
|
578
|
+
digital-brain tutorial
|
|
579
|
+
digital-brain sync-whatsapp --days 30
|
|
580
|
+
digital-brain sync-imessage --days 30
|
|
581
|
+
digital-brain import-slack --input slack-export.zip
|
|
582
|
+
digital-brain import-linkedin --input linkedin-archive.zip
|
|
583
|
+
digital-brain extract --days 30
|
|
584
|
+
digital-brain interpret --days 30
|
|
585
|
+
digital-brain send-whatsapp --to "Name" --message "Text" [--yes]
|
|
349
586
|
`);
|
|
350
587
|
}
|
package/docs/AUTOMATIONS.md
CHANGED
|
@@ -10,6 +10,14 @@ Tools/digital-brain-refresh.sh
|
|
|
10
10
|
|
|
11
11
|
That script runs sync, extract, and interpret using the install-time config.
|
|
12
12
|
|
|
13
|
+
After setup, the normal manual command is:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
digital-brain run
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`init` saves your default vault in `~/.digital-brain/config.json`, so `run` does not need a vault path.
|
|
20
|
+
|
|
13
21
|
## Configurable At Install
|
|
14
22
|
|
|
15
23
|
`digital-brain init` asks:
|
|
@@ -17,6 +25,7 @@ That script runs sync, extract, and interpret using the install-time config.
|
|
|
17
25
|
- vault path
|
|
18
26
|
- your name
|
|
19
27
|
- history window in days
|
|
28
|
+
- setup mode: guided or auto mode
|
|
20
29
|
- primary focus: relationship memory, reply help, work context
|
|
21
30
|
- refresh cadence: manual, daily, hourly, every 30 minutes, or always-on
|
|
22
31
|
- refresh interval in minutes for always-on mode, clamped to a minimum of 1
|
|
@@ -44,15 +53,15 @@ The answers are saved in:
|
|
|
44
53
|
digital-brain.config.json
|
|
45
54
|
```
|
|
46
55
|
|
|
47
|
-
##
|
|
56
|
+
## Auto Mode
|
|
48
57
|
|
|
49
|
-
|
|
58
|
+
Choose `Auto mode` during `digital-brain init`, or use:
|
|
50
59
|
|
|
51
60
|
```bash
|
|
52
61
|
digital-brain init --full-auto
|
|
53
62
|
```
|
|
54
63
|
|
|
55
|
-
|
|
64
|
+
Auto mode configures local always-on refreshes with a 5 minute default interval. It still uses the local watch script, so it only runs while the machine and runner are awake.
|
|
56
65
|
|
|
57
66
|
During the guided quiz, always-on and send-with-confirmation require an explicit responsibility check. Pressing Enter does not approve that check. If it is skipped, Digital Brain falls back to manual refresh and draft-only outbound.
|
|
58
67
|
|
|
@@ -63,7 +72,7 @@ Use a Codex cron automation pointed at the generated vault.
|
|
|
63
72
|
Prompt:
|
|
64
73
|
|
|
65
74
|
```text
|
|
66
|
-
Run `
|
|
75
|
+
Run `digital-brain run`. Verify completion and report only counts plus output paths. Do not print private message contents.
|
|
67
76
|
```
|
|
68
77
|
|
|
69
78
|
Example schedule ideas:
|