digital-brain 0.1.7 → 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 +20 -2
- package/bin/digital-brain.js +136 -20
- package/docs/INTEGRATIONS.md +72 -0
- package/docs/PRIVACY.md +3 -1
- package/docs/SETUP.md +33 -1
- 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 +7 -1
- 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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
import argparse
|
|
3
|
+
import hashlib
|
|
3
4
|
import json
|
|
4
5
|
import sqlite3
|
|
5
6
|
import time
|
|
@@ -60,7 +61,7 @@ def sync_once(args, seen, raw_dir, chats_dir, whats_app):
|
|
|
60
61
|
|
|
61
62
|
added = 0
|
|
62
63
|
for row in rows:
|
|
63
|
-
record = row_to_record(row, args.self_name)
|
|
64
|
+
record = row_to_record(row, args.self_name, args.privacy_mode)
|
|
64
65
|
if args.no_groups and record["isGroup"]:
|
|
65
66
|
continue
|
|
66
67
|
if args.chat and args.chat.lower() not in record["chatName"].lower():
|
|
@@ -74,13 +75,16 @@ def sync_once(args, seen, raw_dir, chats_dir, whats_app):
|
|
|
74
75
|
return added
|
|
75
76
|
|
|
76
77
|
|
|
77
|
-
def row_to_record(row, self_name):
|
|
78
|
+
def row_to_record(row, self_name, privacy_mode):
|
|
78
79
|
timestamp = datetime.fromtimestamp(float(row["message_date"]) + CORE_DATA_EPOCH_OFFSET, tz=timezone.utc).isoformat()
|
|
79
80
|
chat_name = row["chat_name"] or row["chat_jid"] or "Unknown Chat"
|
|
80
81
|
from_me = bool(row["is_from_me"])
|
|
82
|
+
body = row["text"] or ""
|
|
83
|
+
record_id = compound_id(row, timestamp)
|
|
81
84
|
return {
|
|
82
|
-
"id":
|
|
85
|
+
"id": record_id,
|
|
83
86
|
"source": "WhatsApp Mac app ChatStorage.sqlite",
|
|
87
|
+
"sourceSystem": "WhatsApp",
|
|
84
88
|
"timestamp": timestamp,
|
|
85
89
|
"chatPk": row["chat_pk"],
|
|
86
90
|
"chatName": chat_name,
|
|
@@ -91,10 +95,23 @@ def row_to_record(row, self_name):
|
|
|
91
95
|
"fromJid": row["from_jid"],
|
|
92
96
|
"toJid": row["to_jid"],
|
|
93
97
|
"messageType": row["message_type"],
|
|
94
|
-
"body":
|
|
98
|
+
"body": "" if privacy_mode == "metadata-only" else body,
|
|
99
|
+
"bodyHash": hashlib.sha256(body.encode("utf-8")).hexdigest() if privacy_mode == "metadata-only" else "",
|
|
100
|
+
"bodyCharCount": len(body),
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
|
|
104
|
+
def compound_id(row, timestamp):
|
|
105
|
+
parts = [
|
|
106
|
+
"whatsapp",
|
|
107
|
+
str(row["chat_pk"] or row["chat_jid"] or "unknown-chat"),
|
|
108
|
+
str(row["stanza_id"] or "no-stanza"),
|
|
109
|
+
str(row["message_pk"] or "no-pk"),
|
|
110
|
+
timestamp,
|
|
111
|
+
]
|
|
112
|
+
return "::".join(parts)
|
|
113
|
+
|
|
114
|
+
|
|
98
115
|
def append_jsonl(raw_dir, record):
|
|
99
116
|
with (raw_dir / f"{record['timestamp'][:10]}.jsonl").open("a", encoding="utf-8") as f:
|
|
100
117
|
f.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
@@ -110,9 +127,9 @@ def append_markdown(chats_dir, whats_app, record, mode):
|
|
|
110
127
|
else:
|
|
111
128
|
file_path = chats_dir / f"{safe_filename(record['chatName'])}.md"
|
|
112
129
|
if not file_path.exists():
|
|
113
|
-
file_path
|
|
114
|
-
speaker = record["author"]
|
|
115
|
-
body = " ".join(record["body"].split())
|
|
130
|
+
write_text_atomic(file_path, f"# {escape_markdown(record['chatName'])}\n\nSynced from WhatsApp Mac app.\n\n")
|
|
131
|
+
speaker = escape_markdown(record["author"])
|
|
132
|
+
body = escape_markdown(" ".join(record["body"].split()))
|
|
116
133
|
with file_path.open("a", encoding="utf-8") as f:
|
|
117
134
|
f.write(f"- {record['timestamp']} | {speaker}: {body}\n")
|
|
118
135
|
|
|
@@ -127,7 +144,7 @@ def load_seen(path):
|
|
|
127
144
|
|
|
128
145
|
|
|
129
146
|
def save_seen(path, seen):
|
|
130
|
-
path
|
|
147
|
+
write_text_atomic(path, json.dumps(sorted(seen), indent=2))
|
|
131
148
|
|
|
132
149
|
|
|
133
150
|
def safe_filename(value):
|
|
@@ -135,6 +152,17 @@ def safe_filename(value):
|
|
|
135
152
|
return (" ".join(cleaned.split()).strip() or "Unknown Chat")[:120]
|
|
136
153
|
|
|
137
154
|
|
|
155
|
+
def escape_markdown(value):
|
|
156
|
+
text = str(value).replace("\n", " ").replace("\r", " ")
|
|
157
|
+
return text.replace("\\", "\\\\").replace("[", "\\[").replace("]", "\\]").replace("|", "\\|")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def write_text_atomic(path, content):
|
|
161
|
+
temp = path.with_name(f"{path.name}.{time.time_ns()}.tmp")
|
|
162
|
+
temp.write_text(content, encoding="utf-8")
|
|
163
|
+
temp.replace(path)
|
|
164
|
+
|
|
165
|
+
|
|
138
166
|
def parse_args():
|
|
139
167
|
parser = argparse.ArgumentParser()
|
|
140
168
|
parser.add_argument("--vault", type=Path, required=True)
|
|
@@ -146,6 +174,7 @@ def parse_args():
|
|
|
146
174
|
parser.add_argument("--db", type=Path, default=DEFAULT_DB)
|
|
147
175
|
parser.add_argument("--self-name", default="Me")
|
|
148
176
|
parser.add_argument("--markdown-mode", choices=["chat", "month", "none"], default="chat")
|
|
177
|
+
parser.add_argument("--privacy-mode", choices=["standard", "metadata-only"], default="standard")
|
|
149
178
|
return parser.parse_args()
|
|
150
179
|
|
|
151
180
|
|
|
@@ -7,5 +7,5 @@ Rules:
|
|
|
7
7
|
- Prefer local evidence over assumptions.
|
|
8
8
|
- Be clear when a claim is inferred.
|
|
9
9
|
- Treat generated relationship notes as provisional.
|
|
10
|
+
- When replying to a person, check `06 AI Memory/Person Reply Context.md` first.
|
|
10
11
|
- Do not send messages without explicit permission.
|
|
11
|
-
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Relationship Overrides
|
|
2
2
|
|
|
3
|
+
Keep human-edited relationship notes in this folder. Generated drafts are written to `06 AI Memory/Generated Relationship Drafts/` and may be overwritten.
|
|
4
|
+
|
|
3
5
|
Use `08 Sources/WhatsApp/relationship_overrides.json` to manually label important people.
|
|
4
6
|
|
|
5
7
|
Example:
|
|
@@ -13,4 +15,3 @@ Example:
|
|
|
13
15
|
}
|
|
14
16
|
}
|
|
15
17
|
```
|
|
16
|
-
|
|
@@ -5,13 +5,13 @@ Outbound sending is disabled unless you explicitly run the sender with `--yes`.
|
|
|
5
5
|
Draft:
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
digital-brain send-whatsapp --
|
|
8
|
+
digital-brain send-whatsapp --to "Name" --message "Text"
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
Send:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
digital-brain send-whatsapp --
|
|
14
|
+
digital-brain send-whatsapp --to "Name" --message "Text" --yes
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Disclosure rule:
|
|
@@ -8,6 +8,8 @@ Before answering personal-context questions, read:
|
|
|
8
8
|
- `00 Home/How AI Should Use This Vault.md`
|
|
9
9
|
- `01 Identity/Working Profile.md`
|
|
10
10
|
- `06 AI Memory/What AI Should Remember.md`
|
|
11
|
+
- `06 AI Memory/Person Context Index.md`
|
|
12
|
+
- `06 AI Memory/Person Reply Context.md`
|
|
11
13
|
- `06 AI Memory/Interpreted Relationship Memory.md`
|
|
12
14
|
|
|
13
15
|
Rules:
|
|
@@ -16,4 +18,6 @@ Rules:
|
|
|
16
18
|
- Separate facts from interpretation.
|
|
17
19
|
- Treat relationship labels as editable working notes.
|
|
18
20
|
- Do not expose private summaries unless asked.
|
|
19
|
-
|
|
21
|
+
- For reply help, prefer person-level context over source-specific notes when available.
|
|
22
|
+
- Do not read `08 Sources/` raw files unless the user explicitly asks you to inspect source data.
|
|
23
|
+
- Prefer `06 AI Memory/` summaries and `04 People/` human notes for normal context.
|
|
@@ -4,3 +4,6 @@ This is a Digital Brain vault. Use it as local personal context when relevant.
|
|
|
4
4
|
|
|
5
5
|
Start with `00 Home/Start Here.md`.
|
|
6
6
|
|
|
7
|
+
For reply help, prefer `06 AI Memory/Person Reply Context.md` when available.
|
|
8
|
+
|
|
9
|
+
Do not read `08 Sources/` raw files unless the user explicitly asks for source-level evidence.
|
|
@@ -3,3 +3,7 @@
|
|
|
3
3
|
This is a Digital Brain vault. Use it as local personal context when relevant.
|
|
4
4
|
|
|
5
5
|
Start with `00 Home/Start Here.md`.
|
|
6
|
+
|
|
7
|
+
For reply help, prefer `06 AI Memory/Person Reply Context.md` when available.
|
|
8
|
+
|
|
9
|
+
Do not read `08 Sources/` raw files unless the user explicitly asks for source-level evidence.
|
package/whatsapp-web/send.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import crypto from "node:crypto";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import qrcode from "qrcode-terminal";
|
|
4
5
|
import pkg from "whatsapp-web.js";
|
|
@@ -12,6 +13,8 @@ const vault = path.resolve(args.vault);
|
|
|
12
13
|
const whatsAppDir = path.join(vault, "08 Sources", "WhatsApp");
|
|
13
14
|
const outboundDir = path.join(whatsAppDir, "Outbound");
|
|
14
15
|
const sessionDir = path.join(whatsAppDir, ".session");
|
|
16
|
+
const config = readConfig(vault);
|
|
17
|
+
const outboundLogMode = args["log-mode"] || config.outboundLogMode || "metadata";
|
|
15
18
|
fs.mkdirSync(outboundDir, { recursive: true });
|
|
16
19
|
|
|
17
20
|
if (!args.yes) {
|
|
@@ -36,7 +39,11 @@ client.on("ready", async () => {
|
|
|
36
39
|
try {
|
|
37
40
|
const chat = await resolveChat(args.to);
|
|
38
41
|
const disclosure = disclosureStatus(chat);
|
|
39
|
-
|
|
42
|
+
const bypassDisclosure = args["skip-disclosure-check"] && process.env.DIGITAL_BRAIN_ALLOW_DISCLOSURE_BYPASS === "1";
|
|
43
|
+
if (args["skip-disclosure-check"] && !bypassDisclosure) {
|
|
44
|
+
throw new Error("Disclosure bypass requires DIGITAL_BRAIN_ALLOW_DISCLOSURE_BYPASS=1.");
|
|
45
|
+
}
|
|
46
|
+
if (disclosure.required && !containsDisclosure(args.message) && !bypassDisclosure) {
|
|
40
47
|
throw new Error(
|
|
41
48
|
'AI disclosure required before sending. Add a clear disclosure like: "Just flagging this is my AI assistant helping draft/send this."',
|
|
42
49
|
);
|
|
@@ -46,13 +53,19 @@ client.on("ready", async () => {
|
|
|
46
53
|
timestamp: new Date().toISOString(),
|
|
47
54
|
to: args.to,
|
|
48
55
|
resolvedChatName: chatName(chat),
|
|
49
|
-
message: args.message,
|
|
56
|
+
message: outboundLogMode === "full" ? args.message : undefined,
|
|
57
|
+
messageHash: hash(args.message),
|
|
58
|
+
messageCharCount: args.message.length,
|
|
50
59
|
messageId: sent.id?._serialized || null,
|
|
51
60
|
aiAssisted: !args.human,
|
|
52
61
|
disclosureIncluded: containsDisclosure(args.message),
|
|
62
|
+
disclosureBypassed: bypassDisclosure,
|
|
53
63
|
};
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
if (outboundLogMode !== "off") {
|
|
65
|
+
fs.appendFileSync(path.join(outboundDir, "sent.jsonl"), `${JSON.stringify(record)}\n`);
|
|
66
|
+
const visible = outboundLogMode === "full" ? args.message : `[metadata only, ${record.messageCharCount} chars, ${record.messageHash.slice(0, 12)}]`;
|
|
67
|
+
fs.appendFileSync(path.join(outboundDir, "Sent.md"), `- ${record.timestamp} | ${record.resolvedChatName}: ${visible}\n`);
|
|
68
|
+
}
|
|
56
69
|
console.log(`Sent to ${record.resolvedChatName}`);
|
|
57
70
|
await client.destroy();
|
|
58
71
|
process.exit(0);
|
|
@@ -104,7 +117,7 @@ function parseArgs(argv) {
|
|
|
104
117
|
}
|
|
105
118
|
|
|
106
119
|
function usage() {
|
|
107
|
-
console.error('Usage: digital-brain send-whatsapp --vault <path> --to "Name" --message "Text" [--yes]');
|
|
120
|
+
console.error('Usage: digital-brain send-whatsapp --vault <path> --to "Name" --message "Text" [--yes] [--log-mode metadata|full|off]');
|
|
108
121
|
process.exit(1);
|
|
109
122
|
}
|
|
110
123
|
|
|
@@ -137,3 +150,17 @@ function disclosureStatus(chat) {
|
|
|
137
150
|
function containsDisclosure(message) {
|
|
138
151
|
return /\b(ai|assistant|automated|bot)\b/i.test(message);
|
|
139
152
|
}
|
|
153
|
+
|
|
154
|
+
function readConfig(vaultPath) {
|
|
155
|
+
const file = path.join(vaultPath, "digital-brain.config.json");
|
|
156
|
+
if (!fs.existsSync(file)) return {};
|
|
157
|
+
try {
|
|
158
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
159
|
+
} catch {
|
|
160
|
+
return {};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function hash(value) {
|
|
165
|
+
return crypto.createHash("sha256").update(value).digest("hex");
|
|
166
|
+
}
|