digital-brain 0.1.0 → 0.1.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 +32 -2
- package/bin/digital-brain.js +152 -11
- package/docs/AUTOMATIONS.md +46 -1
- package/docs/PRIVACY.md +3 -1
- package/examples/sample-vault/04 People/Interpreted Relationships/Close Friend.md +15 -1
- package/examples/sample-vault/04 People/Interpreted Relationships/Mom.md +15 -1
- package/examples/sample-vault/04 People/Interpreted Relationships/Project Team.md +15 -1
- package/examples/sample-vault/06 AI Memory/Interpreted Relationship Memory.md +3 -3
- package/examples/sample-vault/08 Sources/WhatsApp/Analysis/Interpreted/Close Friend.md +15 -1
- package/examples/sample-vault/08 Sources/WhatsApp/Analysis/Interpreted/Mom.md +15 -1
- package/examples/sample-vault/08 Sources/WhatsApp/Analysis/Interpreted/Project Team.md +15 -1
- package/examples/sample-vault/08 Sources/WhatsApp/Analysis/Relationship Map.md +3 -0
- package/examples/sample-vault/08 Sources/WhatsApp/Analysis/interpreted_relationship_models.json +48 -0
- package/examples/sample-vault/08 Sources/WhatsApp/Analysis/relationship_profiles.json +39 -3
- package/package.json +2 -1
- package/scripts/digital_brain_relationship_extractor.py +81 -0
- package/scripts/digital_brain_relationship_interpreter.py +47 -1
package/README.md
CHANGED
|
@@ -24,7 +24,27 @@ 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, active time window, outbound mode, and AI adapter setup.
|
|
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.
|
|
28
|
+
|
|
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
|
+
|
|
31
|
+
```text
|
|
32
|
+
./Digital Brain Vault
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
For a non-interactive setup:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx digital-brain init --yes
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
For local always-on setup:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx digital-brain init --full-auto
|
|
45
|
+
```
|
|
46
|
+
|
|
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.
|
|
28
48
|
|
|
29
49
|
For local development:
|
|
30
50
|
|
|
@@ -41,12 +61,14 @@ node ./bin/digital-brain.js init ./Digital Brain\ Vault
|
|
|
41
61
|
- Relationship extraction and interpretation models.
|
|
42
62
|
- Optional WhatsApp Web outbound sender.
|
|
43
63
|
- A refresh script based on your install-time answers.
|
|
64
|
+
- An optional always-on watch script that can pull every N minutes.
|
|
44
65
|
|
|
45
66
|
## What It Can Do
|
|
46
67
|
|
|
47
68
|
- Import recent WhatsApp history from the local macOS WhatsApp database.
|
|
48
69
|
- Build relationship profiles from message patterns.
|
|
49
70
|
- Infer provisional roles like parent, family group, work collaborator, close personal contact, or unlabeled contact.
|
|
71
|
+
- Extract relationship-specific typing style: casing, message length, punctuation, emoji, and slang.
|
|
50
72
|
- Generate "how to continue this relationship" notes.
|
|
51
73
|
- Create AI-readable memory files for future prompts.
|
|
52
74
|
- Draft WhatsApp sends by default, and only send with explicit `--yes`.
|
|
@@ -74,7 +96,13 @@ Each vault gets:
|
|
|
74
96
|
Tools/digital-brain-refresh.sh
|
|
75
97
|
```
|
|
76
98
|
|
|
77
|
-
|
|
99
|
+
For 24/7 local polling:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
Tools/digital-brain-watch.sh
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Use these with Codex automations, local cron, launchd, or another scheduler. See [docs/AUTOMATIONS.md](docs/AUTOMATIONS.md).
|
|
78
106
|
|
|
79
107
|
## Example
|
|
80
108
|
|
|
@@ -106,6 +134,8 @@ WhatsApp support reads the local macOS WhatsApp database when available. This is
|
|
|
106
134
|
|
|
107
135
|
Relationship labels are working notes, not truth. You can edit them with `relationship_overrides.json`.
|
|
108
136
|
|
|
137
|
+
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.
|
|
138
|
+
|
|
109
139
|
## Status
|
|
110
140
|
|
|
111
141
|
Alpha. Expect rough edges.
|
package/bin/digital-brain.js
CHANGED
|
@@ -30,26 +30,56 @@ async function main() {
|
|
|
30
30
|
async function init(argv, args) {
|
|
31
31
|
const positional = argv.filter((arg) => !arg.startsWith("--"));
|
|
32
32
|
const defaultVault = path.resolve(process.cwd(), "Digital Brain Vault");
|
|
33
|
+
const fullAuto = toBoolean(args["full-auto"]);
|
|
33
34
|
let vault = positional[0] ? path.resolve(positional[0]) : args.yes ? defaultVault : "";
|
|
34
35
|
let selfName = args["self-name"] || "";
|
|
35
36
|
let connectAi = toBoolean(args["connect-ai"]);
|
|
36
37
|
let dataWindowDays = Number(args["data-window-days"] || 30);
|
|
37
38
|
let focus = args.focus || "";
|
|
38
|
-
let schedule = args.schedule || "manual";
|
|
39
|
+
let schedule = args.schedule || (fullAuto ? "always-on" : "manual");
|
|
40
|
+
let refreshIntervalMinutes = clampInterval(args["refresh-interval-minutes"] || 5);
|
|
39
41
|
let activeWindow = args["active-window"] || "08:00-12:00";
|
|
40
42
|
let timezone = args.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || "local";
|
|
41
43
|
let outboundMode = args["outbound-mode"] || "draft";
|
|
44
|
+
let responsibilityAccepted = fullAuto || schedule === "always-on";
|
|
42
45
|
|
|
43
46
|
if (!args.yes) {
|
|
44
47
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
printSetupHeader(defaultVault);
|
|
49
|
+
vault ||= path.resolve(await ask(rl, "📁 Vault path", defaultVault, "Enter creates this folder if it does not exist."));
|
|
50
|
+
selfName ||= await ask(rl, "👤 Your name", "Me");
|
|
51
|
+
dataWindowDays = await askNumber(rl, "🕰️ History to import", dataWindowDays, { suffix: "days", min: 1 });
|
|
52
|
+
focus ||= await select(rl, "Primary focus", [
|
|
53
|
+
["relationship-memory", "Relationship memory", "Map people, tone, and recurring patterns.", "🧠"],
|
|
54
|
+
["reply-help", "Reply help", "Prioritize drafting guidance and typing-style matching.", "💬"],
|
|
55
|
+
["work-context", "Work context", "Prioritize collaborators, projects, and operational notes.", "💼"],
|
|
56
|
+
], "relationship-memory");
|
|
57
|
+
schedule = await select(rl, "Refresh cadence", [
|
|
58
|
+
["manual", "Manual", "Only runs when you run a command.", "🖐️"],
|
|
59
|
+
["daily", "Daily", "Good for a low-maintenance personal vault.", "🌅"],
|
|
60
|
+
["hourly", "Hourly", "Keeps memory warm without running constantly.", "⏱️"],
|
|
61
|
+
["every-30-min", "Every 30 minutes", "Useful for morning or work-window guidance.", "🔁"],
|
|
62
|
+
["always-on", "Always-on local loop", "Runs repeatedly while your computer is awake.", "⚡"],
|
|
63
|
+
], schedule);
|
|
64
|
+
if (schedule === "always-on") {
|
|
65
|
+
refreshIntervalMinutes = await askNumber(rl, "⏳ Always-on pull interval", refreshIntervalMinutes, {
|
|
66
|
+
suffix: "minutes",
|
|
67
|
+
min: 1,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
activeWindow = await ask(rl, "🪟 Active window for frequent refreshes", activeWindow);
|
|
71
|
+
outboundMode = await select(rl, "WhatsApp outbound mode", [
|
|
72
|
+
["disabled", "Disabled", "Never prepares WhatsApp sends.", "🔒"],
|
|
73
|
+
["draft", "Draft only", "Prepares text and requires you to send it.", "✍️"],
|
|
74
|
+
["send-with-confirmation", "Send with confirmation", "Can send only after explicit command confirmation.", "✅"],
|
|
75
|
+
], outboundMode);
|
|
76
|
+
connectAi = await confirm(rl, "🔗 Add global AI pointers for Codex/Claude/Gemini?", true);
|
|
77
|
+
responsibilityAccepted = await responsibilityGate(rl, { schedule, outboundMode });
|
|
78
|
+
if (!responsibilityAccepted && (schedule === "always-on" || outboundMode === "send-with-confirmation")) {
|
|
79
|
+
console.log("Full-auto/outbound confirmation was not accepted. Using manual refresh and draft-only outbound.");
|
|
80
|
+
schedule = "manual";
|
|
81
|
+
outboundMode = "draft";
|
|
82
|
+
}
|
|
53
83
|
rl.close();
|
|
54
84
|
}
|
|
55
85
|
|
|
@@ -60,9 +90,18 @@ async function init(argv, args) {
|
|
|
60
90
|
dataWindowDays,
|
|
61
91
|
focus: focus || "relationship-memory",
|
|
62
92
|
schedule,
|
|
93
|
+
refreshIntervalMinutes,
|
|
63
94
|
activeWindow,
|
|
64
95
|
timezone,
|
|
65
96
|
outboundMode,
|
|
97
|
+
setupMode: fullAuto ? "full-auto" : "guided",
|
|
98
|
+
responsibilityAccepted,
|
|
99
|
+
defaults: {
|
|
100
|
+
enterUsesDefault: true,
|
|
101
|
+
defaultVault,
|
|
102
|
+
skippedVaultCreates: defaultVault,
|
|
103
|
+
minimumRefreshIntervalMinutes: 1,
|
|
104
|
+
},
|
|
66
105
|
disclosureRule: {
|
|
67
106
|
enabled: true,
|
|
68
107
|
discloseAfterAiAssistedSends: 2,
|
|
@@ -71,6 +110,7 @@ async function init(argv, args) {
|
|
|
71
110
|
};
|
|
72
111
|
writeConfig(vault, config);
|
|
73
112
|
writeRefreshScript(vault, config);
|
|
113
|
+
writeWatchScript(vault, config);
|
|
74
114
|
|
|
75
115
|
if (connectAi) {
|
|
76
116
|
addGlobalPointer(path.join(os.homedir(), ".codex", "AGENTS.md"), vault, "Codex");
|
|
@@ -81,10 +121,12 @@ async function init(argv, args) {
|
|
|
81
121
|
console.log(`Digital Brain vault created: ${vault}`);
|
|
82
122
|
console.log(`Config: ${path.join(vault, "digital-brain.config.json")}`);
|
|
83
123
|
console.log(`Refresh script: ${path.join(vault, "Tools", "digital-brain-refresh.sh")}`);
|
|
124
|
+
console.log(`Always-on script: ${path.join(vault, "Tools", "digital-brain-watch.sh")}`);
|
|
84
125
|
console.log("Next:");
|
|
85
126
|
console.log(` digital-brain sync-whatsapp --vault "${vault}" --days ${dataWindowDays}`);
|
|
86
127
|
console.log(` digital-brain extract --vault "${vault}" --days ${dataWindowDays}`);
|
|
87
128
|
console.log(` digital-brain interpret --vault "${vault}" --days ${dataWindowDays}`);
|
|
129
|
+
if (schedule === "always-on") console.log(` "${path.join(vault, "Tools", "digital-brain-watch.sh")}"`);
|
|
88
130
|
}
|
|
89
131
|
|
|
90
132
|
function doctor() {
|
|
@@ -142,6 +184,29 @@ echo "Digital Brain refresh complete for $VAULT"
|
|
|
142
184
|
fs.chmodSync(scriptPath, 0o755);
|
|
143
185
|
}
|
|
144
186
|
|
|
187
|
+
function writeWatchScript(vault, config) {
|
|
188
|
+
const toolsDir = path.join(vault, "Tools");
|
|
189
|
+
ensureDir(toolsDir);
|
|
190
|
+
const interval = clampInterval(config.refreshIntervalMinutes || 5);
|
|
191
|
+
const content = `#!/usr/bin/env bash
|
|
192
|
+
set -euo pipefail
|
|
193
|
+
|
|
194
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
195
|
+
INTERVAL_MINUTES="${interval}"
|
|
196
|
+
|
|
197
|
+
echo "Digital Brain watch loop started. Interval: $INTERVAL_MINUTES minute(s)."
|
|
198
|
+
echo "Press Ctrl+C to stop."
|
|
199
|
+
|
|
200
|
+
while true; do
|
|
201
|
+
"$SCRIPT_DIR/digital-brain-refresh.sh"
|
|
202
|
+
sleep "$((INTERVAL_MINUTES * 60))"
|
|
203
|
+
done
|
|
204
|
+
`;
|
|
205
|
+
const scriptPath = path.join(toolsDir, "digital-brain-watch.sh");
|
|
206
|
+
fs.writeFileSync(scriptPath, content);
|
|
207
|
+
fs.chmodSync(scriptPath, 0o755);
|
|
208
|
+
}
|
|
209
|
+
|
|
145
210
|
function addGlobalPointer(file, vault, label) {
|
|
146
211
|
ensureDir(path.dirname(file));
|
|
147
212
|
const block = `
|
|
@@ -159,11 +224,81 @@ Use it as local personal context when the user asks about preferences, relations
|
|
|
159
224
|
console.log(`${label} pointer: ${file}`);
|
|
160
225
|
}
|
|
161
226
|
|
|
162
|
-
|
|
163
|
-
|
|
227
|
+
function printSetupHeader(defaultVault) {
|
|
228
|
+
console.log("");
|
|
229
|
+
console.log("╭────────────────────────────────────────╮");
|
|
230
|
+
console.log("│ 🧠 Digital Brain setup │");
|
|
231
|
+
console.log("╰────────────────────────────────────────╯");
|
|
232
|
+
console.log("Pick with A/B/C, 1/2/3, exact value, or press Enter for the default.");
|
|
233
|
+
console.log(`Skipping the vault path creates: ${defaultVault}`);
|
|
234
|
+
console.log("");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function ask(rl, label, fallback, helpText = "") {
|
|
238
|
+
if (helpText) console.log(` ${helpText}`);
|
|
239
|
+
const answer = await rl.question(`${label} [${fallback}]: `);
|
|
164
240
|
return answer.trim() || fallback;
|
|
165
241
|
}
|
|
166
242
|
|
|
243
|
+
async function askNumber(rl, label, fallback, options = {}) {
|
|
244
|
+
const suffix = options.suffix ? ` ${options.suffix}` : "";
|
|
245
|
+
const answer = await ask(rl, `${label}${suffix}`, String(fallback));
|
|
246
|
+
const parsed = Number(answer);
|
|
247
|
+
if (!Number.isFinite(parsed)) return Number(fallback);
|
|
248
|
+
if (Number.isFinite(options.min) && parsed < options.min) return options.min;
|
|
249
|
+
return Math.floor(parsed);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function select(rl, label, options, fallback) {
|
|
253
|
+
const defaultIndex = Math.max(0, options.findIndex(([value]) => value === fallback));
|
|
254
|
+
console.log("");
|
|
255
|
+
console.log(`◇ ${label}`);
|
|
256
|
+
options.forEach(([, title, description, icon = "•"], index) => {
|
|
257
|
+
const marker = index === defaultIndex ? " ← default" : "";
|
|
258
|
+
const letter = letterFor(index);
|
|
259
|
+
console.log(` ${letter}) ${icon} ${title}${marker}`);
|
|
260
|
+
console.log(` ${description}`);
|
|
261
|
+
});
|
|
262
|
+
const answer = await rl.question(`Choose ${letterFor(defaultIndex)}/${defaultIndex + 1} [${letterFor(defaultIndex)}]: `);
|
|
263
|
+
const trimmed = answer.trim();
|
|
264
|
+
if (!trimmed) return options[defaultIndex][0];
|
|
265
|
+
const letterIndex = indexFromLetter(trimmed);
|
|
266
|
+
if (letterIndex >= 0 && letterIndex < options.length) return options[letterIndex][0];
|
|
267
|
+
const selected = Number(trimmed);
|
|
268
|
+
if (Number.isInteger(selected) && selected >= 1 && selected <= options.length) return options[selected - 1][0];
|
|
269
|
+
const lower = trimmed.toLowerCase();
|
|
270
|
+
const exact = options.find(([value, title]) => value.toLowerCase() === lower || title.toLowerCase() === lower);
|
|
271
|
+
return exact ? exact[0] : options[defaultIndex][0];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function confirm(rl, label, fallback) {
|
|
275
|
+
const hint = fallback ? "Y/n" : "y/N";
|
|
276
|
+
const answer = (await rl.question(`${label} [${hint}]: `)).trim().toLowerCase();
|
|
277
|
+
if (!answer) return fallback;
|
|
278
|
+
return ["y", "yes", "true", "1"].includes(answer);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function responsibilityGate(rl, { schedule, outboundMode }) {
|
|
282
|
+
const needsGate = schedule === "always-on" || outboundMode === "send-with-confirmation";
|
|
283
|
+
if (!needsGate) return true;
|
|
284
|
+
console.log("");
|
|
285
|
+
console.log("⚠️ Responsibility check:");
|
|
286
|
+
console.log(" Digital Brain may use local databases, WhatsApp Web, and black-box third-party app behavior.");
|
|
287
|
+
console.log(" You are responsible for consent, privacy, message content, and any sends triggered from this machine.");
|
|
288
|
+
console.log(" Enter does not approve this mode.");
|
|
289
|
+
return confirm(rl, "I understand and want this mode enabled", false);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function letterFor(index) {
|
|
293
|
+
return String.fromCharCode(65 + index);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function indexFromLetter(value) {
|
|
297
|
+
const normalized = value.trim().toUpperCase();
|
|
298
|
+
if (!/^[A-Z]$/.test(normalized)) return -1;
|
|
299
|
+
return normalized.charCodeAt(0) - 65;
|
|
300
|
+
}
|
|
301
|
+
|
|
167
302
|
function shell(command, args, optional = false) {
|
|
168
303
|
const result = spawnSync(command, args, { encoding: "utf8" });
|
|
169
304
|
if (result.error && optional) return "";
|
|
@@ -195,6 +330,12 @@ function toBoolean(value) {
|
|
|
195
330
|
return !["false", "0", "no", "off"].includes(String(value).toLowerCase());
|
|
196
331
|
}
|
|
197
332
|
|
|
333
|
+
function clampInterval(value) {
|
|
334
|
+
const parsed = Number(value);
|
|
335
|
+
if (!Number.isFinite(parsed) || parsed < 1) return 1;
|
|
336
|
+
return Math.floor(parsed);
|
|
337
|
+
}
|
|
338
|
+
|
|
198
339
|
function help() {
|
|
199
340
|
console.log(`Digital Brain
|
|
200
341
|
|
package/docs/AUTOMATIONS.md
CHANGED
|
@@ -18,17 +18,44 @@ That script runs sync, extract, and interpret using the install-time config.
|
|
|
18
18
|
- your name
|
|
19
19
|
- history window in days
|
|
20
20
|
- primary focus: relationship memory, reply help, work context
|
|
21
|
-
- refresh cadence
|
|
21
|
+
- refresh cadence: manual, daily, hourly, every 30 minutes, or always-on
|
|
22
|
+
- refresh interval in minutes for always-on mode, clamped to a minimum of 1
|
|
22
23
|
- active time window
|
|
23
24
|
- WhatsApp outbound mode
|
|
24
25
|
- whether to add AI adapter pointers
|
|
25
26
|
|
|
27
|
+
Most questions are multiple choice. Pick with `A/B/C`, `1/2/3`, the exact value, or press Enter to use the displayed default.
|
|
28
|
+
|
|
29
|
+
Important defaults:
|
|
30
|
+
|
|
31
|
+
- skipped vault path creates `./Digital Brain Vault` in the current directory
|
|
32
|
+
- skipped name uses `Me`
|
|
33
|
+
- skipped history window uses 30 days
|
|
34
|
+
- skipped focus uses relationship memory
|
|
35
|
+
- skipped schedule uses manual refresh
|
|
36
|
+
- skipped always-on interval uses 5 minutes, with a hard minimum of 1 minute
|
|
37
|
+
- skipped active window uses `08:00-12:00`
|
|
38
|
+
- skipped outbound mode uses draft-only
|
|
39
|
+
- skipped AI pointers are added during the guided quiz
|
|
40
|
+
|
|
26
41
|
The answers are saved in:
|
|
27
42
|
|
|
28
43
|
```bash
|
|
29
44
|
digital-brain.config.json
|
|
30
45
|
```
|
|
31
46
|
|
|
47
|
+
## Full Auto
|
|
48
|
+
|
|
49
|
+
Use:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
digital-brain init --full-auto
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Full-auto 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
|
+
|
|
57
|
+
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
|
+
|
|
32
59
|
## Codex App
|
|
33
60
|
|
|
34
61
|
Use a Codex cron automation pointed at the generated vault.
|
|
@@ -45,6 +72,18 @@ Example schedule ideas:
|
|
|
45
72
|
- Every 30 minutes from 8-12: `FREQ=DAILY;BYHOUR=8,9,10,11;BYMINUTE=0,30;BYSECOND=0`
|
|
46
73
|
- Weekly: `FREQ=WEEKLY;BYDAY=MO;BYHOUR=9;BYMINUTE=0;BYSECOND=0`
|
|
47
74
|
|
|
75
|
+
If Codex supports minute-based schedules in your environment, point it at `Tools/digital-brain-refresh.sh`. Otherwise, use the generated local watch script below.
|
|
76
|
+
|
|
77
|
+
## Always-On Local Watch
|
|
78
|
+
|
|
79
|
+
For 24/7 local polling, run:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
/path/to/vault/Tools/digital-brain-watch.sh
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The generated script loops forever and sleeps for `refreshIntervalMinutes`. The minimum supported interval is 1 minute. A practical default is 5 minutes.
|
|
86
|
+
|
|
48
87
|
## Local Cron
|
|
49
88
|
|
|
50
89
|
Run every 30 minutes from 8-12:
|
|
@@ -53,6 +92,12 @@ Run every 30 minutes from 8-12:
|
|
|
53
92
|
0,30 8-11 * * * /path/to/vault/Tools/digital-brain-refresh.sh >> /path/to/vault/08\ Sources/WhatsApp/.sync-state/cron.log 2>&1
|
|
54
93
|
```
|
|
55
94
|
|
|
95
|
+
Run every 5 minutes all day:
|
|
96
|
+
|
|
97
|
+
```cron
|
|
98
|
+
*/5 * * * * /path/to/vault/Tools/digital-brain-refresh.sh >> /path/to/vault/08\ Sources/WhatsApp/.sync-state/cron.log 2>&1
|
|
99
|
+
```
|
|
100
|
+
|
|
56
101
|
## macOS launchd
|
|
57
102
|
|
|
58
103
|
Use launchd if you want a native background job. Be aware macOS privacy permissions may block background access to app databases unless Terminal or the runner has Full Disk Access.
|
package/docs/PRIVACY.md
CHANGED
|
@@ -13,5 +13,7 @@ Things to be careful about:
|
|
|
13
13
|
- Do not commit a generated vault.
|
|
14
14
|
- Do not paste private message exports into GitHub issues.
|
|
15
15
|
- Do not enable outbound sending without understanding the risk.
|
|
16
|
+
- Treat WhatsApp database access and WhatsApp Web automation as unofficial, changeable integrations.
|
|
17
|
+
- Always-on mode runs on your machine and inherits your local permissions.
|
|
18
|
+
- You are responsible for consent, privacy, message content, and sends made from your machine.
|
|
16
19
|
- Treat relationship labels as editable working notes, not truth.
|
|
17
|
-
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# Close Friend
|
|
2
2
|
|
|
3
|
-
Generated: 2026-06-09T22:
|
|
3
|
+
Generated: 2026-06-09T22:26:26.131803+00:00
|
|
4
4
|
Role: operational contact
|
|
5
5
|
Role confidence: low
|
|
6
6
|
Closeness: medium
|
|
7
7
|
Conversation difficulty: low
|
|
8
|
+
Typing style: short
|
|
8
9
|
|
|
9
10
|
These are private working notes. Edit them where wrong.
|
|
10
11
|
|
|
@@ -21,9 +22,22 @@ These are private working notes. Edit them where wrong.
|
|
|
21
22
|
- Messages: 2 (1 inbound, 1 outbound).
|
|
22
23
|
- Tags: direct-chat, light, warm, logistics-heavy.
|
|
23
24
|
|
|
25
|
+
## Typing Style To Match
|
|
26
|
+
- Signature: short.
|
|
27
|
+
- Average length: 5.0 words / 30.0 chars.
|
|
28
|
+
- Lowercase share: 0.0.
|
|
29
|
+
- Question share: 0.0.
|
|
30
|
+
- Exclamation share: 0.0.
|
|
31
|
+
- Emoji share: 0.0.
|
|
32
|
+
- Slang: none detected.
|
|
33
|
+
|
|
24
34
|
## How To Continue This Relationship
|
|
25
35
|
- Keep tone neutral until the user labels this relationship.
|
|
26
36
|
- Track open loops, plans, dates, and commitments.
|
|
27
37
|
|
|
38
|
+
## Reply Guidance
|
|
39
|
+
- Keep replies very short unless context demands detail.
|
|
40
|
+
- Match style without exaggerating or parodying the person.
|
|
41
|
+
|
|
28
42
|
## What Not To Assume
|
|
29
43
|
- Do not expose private summaries unless asked.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# Mom
|
|
2
2
|
|
|
3
|
-
Generated: 2026-06-09T22:
|
|
3
|
+
Generated: 2026-06-09T22:26:26.131148+00:00
|
|
4
4
|
Role: mother
|
|
5
5
|
Role confidence: high
|
|
6
6
|
Closeness: medium
|
|
7
7
|
Conversation difficulty: low
|
|
8
|
+
Typing style: very short
|
|
8
9
|
|
|
9
10
|
These are private working notes. Edit them where wrong.
|
|
10
11
|
|
|
@@ -21,10 +22,23 @@ These are private working notes. Edit them where wrong.
|
|
|
21
22
|
- Messages: 2 (1 inbound, 1 outbound).
|
|
22
23
|
- Tags: direct-chat, light, warm, logistics-heavy.
|
|
23
24
|
|
|
25
|
+
## Typing Style To Match
|
|
26
|
+
- Signature: very short.
|
|
27
|
+
- Average length: 4.0 words / 24.0 chars.
|
|
28
|
+
- Lowercase share: 0.0.
|
|
29
|
+
- Question share: 0.0.
|
|
30
|
+
- Exclamation share: 0.0.
|
|
31
|
+
- Emoji share: 0.0.
|
|
32
|
+
- Slang: none detected.
|
|
33
|
+
|
|
24
34
|
## How To Continue This Relationship
|
|
25
35
|
- Use warmer language than a work chat.
|
|
26
36
|
- Do not make the interaction purely transactional.
|
|
27
37
|
- Track open loops, plans, dates, and commitments.
|
|
28
38
|
|
|
39
|
+
## Reply Guidance
|
|
40
|
+
- Keep replies very short unless context demands detail.
|
|
41
|
+
- Match style without exaggerating or parodying the person.
|
|
42
|
+
|
|
29
43
|
## What Not To Assume
|
|
30
44
|
- Do not expose private summaries unless asked.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# Project Team
|
|
2
2
|
|
|
3
|
-
Generated: 2026-06-09T22:
|
|
3
|
+
Generated: 2026-06-09T22:26:26.131626+00:00
|
|
4
4
|
Role: work collaborator
|
|
5
5
|
Role confidence: medium
|
|
6
6
|
Closeness: low/unclear
|
|
7
7
|
Conversation difficulty: practical/low-emotional
|
|
8
|
+
Typing style: short
|
|
8
9
|
|
|
9
10
|
These are private working notes. Edit them where wrong.
|
|
10
11
|
|
|
@@ -21,10 +22,23 @@ These are private working notes. Edit them where wrong.
|
|
|
21
22
|
- Messages: 2 (1 inbound, 1 outbound).
|
|
22
23
|
- Tags: group-chat, light, logistics-heavy, question-heavy.
|
|
23
24
|
|
|
25
|
+
## Typing Style To Match
|
|
26
|
+
- Signature: short.
|
|
27
|
+
- Average length: 6.0 words / 33.0 chars.
|
|
28
|
+
- Lowercase share: 0.0.
|
|
29
|
+
- Question share: 0.0.
|
|
30
|
+
- Exclamation share: 0.0.
|
|
31
|
+
- Emoji share: 0.0.
|
|
32
|
+
- Slang: none detected.
|
|
33
|
+
|
|
24
34
|
## How To Continue This Relationship
|
|
25
35
|
- Lead with context, next steps, and clear asks.
|
|
26
36
|
- Keep emotional interpretation light.
|
|
27
37
|
- Track open loops, plans, dates, and commitments.
|
|
28
38
|
|
|
39
|
+
## Reply Guidance
|
|
40
|
+
- Use concise replies with one clear point.
|
|
41
|
+
- Match style without exaggerating or parodying the person.
|
|
42
|
+
|
|
29
43
|
## What Not To Assume
|
|
30
44
|
- Do not expose private summaries unless asked.
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Generated working notes. Treat as editable, not truth.
|
|
4
4
|
|
|
5
|
-
- [[Mom]]: mother (high), closeness medium, difficulty low
|
|
6
|
-
- [[Project Team]]: work collaborator (medium), closeness low/unclear, difficulty practical/low-emotional
|
|
7
|
-
- [[Close Friend]]: operational contact (low), closeness medium, difficulty low
|
|
5
|
+
- [[Mom]]: mother (high), closeness medium, difficulty low, style very short
|
|
6
|
+
- [[Project Team]]: work collaborator (medium), closeness low/unclear, difficulty practical/low-emotional, style short
|
|
7
|
+
- [[Close Friend]]: operational contact (low), closeness medium, difficulty low, style short
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# Close Friend
|
|
2
2
|
|
|
3
|
-
Generated: 2026-06-09T22:
|
|
3
|
+
Generated: 2026-06-09T22:26:26.131803+00:00
|
|
4
4
|
Role: operational contact
|
|
5
5
|
Role confidence: low
|
|
6
6
|
Closeness: medium
|
|
7
7
|
Conversation difficulty: low
|
|
8
|
+
Typing style: short
|
|
8
9
|
|
|
9
10
|
These are private working notes. Edit them where wrong.
|
|
10
11
|
|
|
@@ -21,9 +22,22 @@ These are private working notes. Edit them where wrong.
|
|
|
21
22
|
- Messages: 2 (1 inbound, 1 outbound).
|
|
22
23
|
- Tags: direct-chat, light, warm, logistics-heavy.
|
|
23
24
|
|
|
25
|
+
## Typing Style To Match
|
|
26
|
+
- Signature: short.
|
|
27
|
+
- Average length: 5.0 words / 30.0 chars.
|
|
28
|
+
- Lowercase share: 0.0.
|
|
29
|
+
- Question share: 0.0.
|
|
30
|
+
- Exclamation share: 0.0.
|
|
31
|
+
- Emoji share: 0.0.
|
|
32
|
+
- Slang: none detected.
|
|
33
|
+
|
|
24
34
|
## How To Continue This Relationship
|
|
25
35
|
- Keep tone neutral until the user labels this relationship.
|
|
26
36
|
- Track open loops, plans, dates, and commitments.
|
|
27
37
|
|
|
38
|
+
## Reply Guidance
|
|
39
|
+
- Keep replies very short unless context demands detail.
|
|
40
|
+
- Match style without exaggerating or parodying the person.
|
|
41
|
+
|
|
28
42
|
## What Not To Assume
|
|
29
43
|
- Do not expose private summaries unless asked.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# Mom
|
|
2
2
|
|
|
3
|
-
Generated: 2026-06-09T22:
|
|
3
|
+
Generated: 2026-06-09T22:26:26.131148+00:00
|
|
4
4
|
Role: mother
|
|
5
5
|
Role confidence: high
|
|
6
6
|
Closeness: medium
|
|
7
7
|
Conversation difficulty: low
|
|
8
|
+
Typing style: very short
|
|
8
9
|
|
|
9
10
|
These are private working notes. Edit them where wrong.
|
|
10
11
|
|
|
@@ -21,10 +22,23 @@ These are private working notes. Edit them where wrong.
|
|
|
21
22
|
- Messages: 2 (1 inbound, 1 outbound).
|
|
22
23
|
- Tags: direct-chat, light, warm, logistics-heavy.
|
|
23
24
|
|
|
25
|
+
## Typing Style To Match
|
|
26
|
+
- Signature: very short.
|
|
27
|
+
- Average length: 4.0 words / 24.0 chars.
|
|
28
|
+
- Lowercase share: 0.0.
|
|
29
|
+
- Question share: 0.0.
|
|
30
|
+
- Exclamation share: 0.0.
|
|
31
|
+
- Emoji share: 0.0.
|
|
32
|
+
- Slang: none detected.
|
|
33
|
+
|
|
24
34
|
## How To Continue This Relationship
|
|
25
35
|
- Use warmer language than a work chat.
|
|
26
36
|
- Do not make the interaction purely transactional.
|
|
27
37
|
- Track open loops, plans, dates, and commitments.
|
|
28
38
|
|
|
39
|
+
## Reply Guidance
|
|
40
|
+
- Keep replies very short unless context demands detail.
|
|
41
|
+
- Match style without exaggerating or parodying the person.
|
|
42
|
+
|
|
29
43
|
## What Not To Assume
|
|
30
44
|
- Do not expose private summaries unless asked.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# Project Team
|
|
2
2
|
|
|
3
|
-
Generated: 2026-06-09T22:
|
|
3
|
+
Generated: 2026-06-09T22:26:26.131626+00:00
|
|
4
4
|
Role: work collaborator
|
|
5
5
|
Role confidence: medium
|
|
6
6
|
Closeness: low/unclear
|
|
7
7
|
Conversation difficulty: practical/low-emotional
|
|
8
|
+
Typing style: short
|
|
8
9
|
|
|
9
10
|
These are private working notes. Edit them where wrong.
|
|
10
11
|
|
|
@@ -21,10 +22,23 @@ These are private working notes. Edit them where wrong.
|
|
|
21
22
|
- Messages: 2 (1 inbound, 1 outbound).
|
|
22
23
|
- Tags: group-chat, light, logistics-heavy, question-heavy.
|
|
23
24
|
|
|
25
|
+
## Typing Style To Match
|
|
26
|
+
- Signature: short.
|
|
27
|
+
- Average length: 6.0 words / 33.0 chars.
|
|
28
|
+
- Lowercase share: 0.0.
|
|
29
|
+
- Question share: 0.0.
|
|
30
|
+
- Exclamation share: 0.0.
|
|
31
|
+
- Emoji share: 0.0.
|
|
32
|
+
- Slang: none detected.
|
|
33
|
+
|
|
24
34
|
## How To Continue This Relationship
|
|
25
35
|
- Lead with context, next steps, and clear asks.
|
|
26
36
|
- Keep emotional interpretation light.
|
|
27
37
|
- Track open loops, plans, dates, and commitments.
|
|
28
38
|
|
|
39
|
+
## Reply Guidance
|
|
40
|
+
- Use concise replies with one clear point.
|
|
41
|
+
- Match style without exaggerating or parodying the person.
|
|
42
|
+
|
|
29
43
|
## What Not To Assume
|
|
30
44
|
- Do not expose private summaries unless asked.
|
|
@@ -11,6 +11,7 @@ Generated signals. Treat as editable working notes.
|
|
|
11
11
|
- Dates: 2026-01-01 to 2026-01-01
|
|
12
12
|
- Scores: sentiment 0.159, warmth 0.5, friction 0.0, operational 1.5
|
|
13
13
|
- Tags: direct-chat, light, warm, logistics-heavy
|
|
14
|
+
- Typing style: very short; avg 4.0 words; lowercase 0.0; emoji 0.0; slang none
|
|
14
15
|
|
|
15
16
|
## Project Team
|
|
16
17
|
|
|
@@ -19,6 +20,7 @@ Generated signals. Treat as editable working notes.
|
|
|
19
20
|
- Dates: 2026-01-01 to 2026-01-01
|
|
20
21
|
- Scores: sentiment 0, warmth 0.0, friction 0.0, operational 3.0
|
|
21
22
|
- Tags: group-chat, light, logistics-heavy, question-heavy
|
|
23
|
+
- Typing style: short; avg 6.0 words; lowercase 0.0; emoji 0.0; slang none
|
|
22
24
|
|
|
23
25
|
## Close Friend
|
|
24
26
|
|
|
@@ -27,3 +29,4 @@ Generated signals. Treat as editable working notes.
|
|
|
27
29
|
- Dates: 2026-01-01 to 2026-01-01
|
|
28
30
|
- Scores: sentiment 0.159, warmth 1.0, friction 0.0, operational 0.5
|
|
29
31
|
- Tags: direct-chat, light, warm, logistics-heavy
|
|
32
|
+
- Typing style: short; avg 5.0 words; lowercase 0.0; emoji 0.0; slang none
|
package/examples/sample-vault/08 Sources/WhatsApp/Analysis/interpreted_relationship_models.json
CHANGED
|
@@ -20,6 +20,18 @@
|
|
|
20
20
|
"warm",
|
|
21
21
|
"logistics-heavy"
|
|
22
22
|
],
|
|
23
|
+
"typingStyle": {
|
|
24
|
+
"sampleSize": 1,
|
|
25
|
+
"avgChars": 24.0,
|
|
26
|
+
"avgWords": 4.0,
|
|
27
|
+
"lowercaseShare": 0.0,
|
|
28
|
+
"uppercaseShare": 0.0,
|
|
29
|
+
"questionShare": 0.0,
|
|
30
|
+
"exclamationShare": 0.0,
|
|
31
|
+
"emojiShare": 0.0,
|
|
32
|
+
"slang": [],
|
|
33
|
+
"signature": "very short"
|
|
34
|
+
},
|
|
23
35
|
"role": "mother",
|
|
24
36
|
"roleConfidence": "high",
|
|
25
37
|
"roleReason": "manual override",
|
|
@@ -33,6 +45,10 @@
|
|
|
33
45
|
],
|
|
34
46
|
"boundaries": [
|
|
35
47
|
"Do not expose private summaries unless asked."
|
|
48
|
+
],
|
|
49
|
+
"replyStyle": [
|
|
50
|
+
"Keep replies very short unless context demands detail.",
|
|
51
|
+
"Match style without exaggerating or parodying the person."
|
|
36
52
|
]
|
|
37
53
|
},
|
|
38
54
|
{
|
|
@@ -56,6 +72,18 @@
|
|
|
56
72
|
"logistics-heavy",
|
|
57
73
|
"question-heavy"
|
|
58
74
|
],
|
|
75
|
+
"typingStyle": {
|
|
76
|
+
"sampleSize": 1,
|
|
77
|
+
"avgChars": 33.0,
|
|
78
|
+
"avgWords": 6.0,
|
|
79
|
+
"lowercaseShare": 0.0,
|
|
80
|
+
"uppercaseShare": 0.0,
|
|
81
|
+
"questionShare": 0.0,
|
|
82
|
+
"exclamationShare": 0.0,
|
|
83
|
+
"emojiShare": 0.0,
|
|
84
|
+
"slang": [],
|
|
85
|
+
"signature": "short"
|
|
86
|
+
},
|
|
59
87
|
"role": "work collaborator",
|
|
60
88
|
"roleConfidence": "medium",
|
|
61
89
|
"roleReason": "matched chat name",
|
|
@@ -69,6 +97,10 @@
|
|
|
69
97
|
],
|
|
70
98
|
"boundaries": [
|
|
71
99
|
"Do not expose private summaries unless asked."
|
|
100
|
+
],
|
|
101
|
+
"replyStyle": [
|
|
102
|
+
"Use concise replies with one clear point.",
|
|
103
|
+
"Match style without exaggerating or parodying the person."
|
|
72
104
|
]
|
|
73
105
|
},
|
|
74
106
|
{
|
|
@@ -92,6 +124,18 @@
|
|
|
92
124
|
"warm",
|
|
93
125
|
"logistics-heavy"
|
|
94
126
|
],
|
|
127
|
+
"typingStyle": {
|
|
128
|
+
"sampleSize": 1,
|
|
129
|
+
"avgChars": 30.0,
|
|
130
|
+
"avgWords": 5.0,
|
|
131
|
+
"lowercaseShare": 0.0,
|
|
132
|
+
"uppercaseShare": 0.0,
|
|
133
|
+
"questionShare": 0.0,
|
|
134
|
+
"exclamationShare": 0.0,
|
|
135
|
+
"emojiShare": 0.0,
|
|
136
|
+
"slang": [],
|
|
137
|
+
"signature": "short"
|
|
138
|
+
},
|
|
95
139
|
"role": "operational contact",
|
|
96
140
|
"roleConfidence": "low",
|
|
97
141
|
"roleReason": "logistics-heavy communication",
|
|
@@ -104,6 +148,10 @@
|
|
|
104
148
|
],
|
|
105
149
|
"boundaries": [
|
|
106
150
|
"Do not expose private summaries unless asked."
|
|
151
|
+
],
|
|
152
|
+
"replyStyle": [
|
|
153
|
+
"Keep replies very short unless context demands detail.",
|
|
154
|
+
"Match style without exaggerating or parodying the person."
|
|
107
155
|
]
|
|
108
156
|
}
|
|
109
157
|
]
|
|
@@ -19,7 +19,19 @@
|
|
|
19
19
|
"light",
|
|
20
20
|
"warm",
|
|
21
21
|
"logistics-heavy"
|
|
22
|
-
]
|
|
22
|
+
],
|
|
23
|
+
"typingStyle": {
|
|
24
|
+
"sampleSize": 1,
|
|
25
|
+
"avgChars": 24.0,
|
|
26
|
+
"avgWords": 4.0,
|
|
27
|
+
"lowercaseShare": 0.0,
|
|
28
|
+
"uppercaseShare": 0.0,
|
|
29
|
+
"questionShare": 0.0,
|
|
30
|
+
"exclamationShare": 0.0,
|
|
31
|
+
"emojiShare": 0.0,
|
|
32
|
+
"slang": [],
|
|
33
|
+
"signature": "very short"
|
|
34
|
+
}
|
|
23
35
|
},
|
|
24
36
|
{
|
|
25
37
|
"chatName": "Project Team",
|
|
@@ -41,7 +53,19 @@
|
|
|
41
53
|
"light",
|
|
42
54
|
"logistics-heavy",
|
|
43
55
|
"question-heavy"
|
|
44
|
-
]
|
|
56
|
+
],
|
|
57
|
+
"typingStyle": {
|
|
58
|
+
"sampleSize": 1,
|
|
59
|
+
"avgChars": 33.0,
|
|
60
|
+
"avgWords": 6.0,
|
|
61
|
+
"lowercaseShare": 0.0,
|
|
62
|
+
"uppercaseShare": 0.0,
|
|
63
|
+
"questionShare": 0.0,
|
|
64
|
+
"exclamationShare": 0.0,
|
|
65
|
+
"emojiShare": 0.0,
|
|
66
|
+
"slang": [],
|
|
67
|
+
"signature": "short"
|
|
68
|
+
}
|
|
45
69
|
},
|
|
46
70
|
{
|
|
47
71
|
"chatName": "Close Friend",
|
|
@@ -63,6 +87,18 @@
|
|
|
63
87
|
"light",
|
|
64
88
|
"warm",
|
|
65
89
|
"logistics-heavy"
|
|
66
|
-
]
|
|
90
|
+
],
|
|
91
|
+
"typingStyle": {
|
|
92
|
+
"sampleSize": 1,
|
|
93
|
+
"avgChars": 30.0,
|
|
94
|
+
"avgWords": 5.0,
|
|
95
|
+
"lowercaseShare": 0.0,
|
|
96
|
+
"uppercaseShare": 0.0,
|
|
97
|
+
"questionShare": 0.0,
|
|
98
|
+
"exclamationShare": 0.0,
|
|
99
|
+
"emojiShare": 0.0,
|
|
100
|
+
"slang": [],
|
|
101
|
+
"signature": "short"
|
|
102
|
+
}
|
|
67
103
|
}
|
|
68
104
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "digital-brain",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Your private digital imprint for AI assistants.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
21
|
"test:sample": "node ./bin/digital-brain.js extract --vault ./examples/sample-vault --days 365 --min-messages 1 && node ./bin/digital-brain.js interpret --vault ./examples/sample-vault --days 365",
|
|
22
|
+
"test": "node --test tests/*.test.mjs",
|
|
22
23
|
"check": "node --check bin/digital-brain.js && node --check lib/*.js && node --check whatsapp-web/*.mjs && python3 -m py_compile scripts/*.py",
|
|
23
24
|
"start": "node ./bin/digital-brain.js"
|
|
24
25
|
},
|
|
@@ -11,6 +11,7 @@ POSITIVE = {"love", "thanks", "thank", "amazing", "great", "good", "nice", "perf
|
|
|
11
11
|
NEGATIVE = {"angry", "annoyed", "upset", "sad", "bad", "hate", "sorry", "fight", "problem", "issue", "wrong", "stress", "fuck", "shit", "worried", "pain", "hurt", "confused"}
|
|
12
12
|
LOGISTICS = {"when", "where", "time", "today", "tomorrow", "meeting", "call", "send", "sent", "come", "coming", "reach", "book", "plan", "schedule"}
|
|
13
13
|
WORK = {"pr", "repo", "client", "customer", "meeting", "deck", "code", "ship", "product", "founder", "startup", "work", "office", "investor", "sales", "demo", "launch"}
|
|
14
|
+
SLANG = {"lol", "lmao", "haha", "hahaha", "bro", "bruh", "wtf", "omg", "ngl", "idk", "rn", "btw", "bc", "pls", "plz", "ya", "yeah", "yep", "nah", "fuck", "shit"}
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def main():
|
|
@@ -59,6 +60,7 @@ def profile_chat(chat_name, messages):
|
|
|
59
60
|
inbound = count - outbound
|
|
60
61
|
dates = [m["_dt"] for m in messages]
|
|
61
62
|
text = "\n".join(m.get("body") or "" for m in messages)
|
|
63
|
+
outbound_messages = [m for m in messages if m.get("fromMe") and (m.get("body") or "").strip()]
|
|
62
64
|
words = Counter(re.findall(r"[a-zA-Z']+", text.lower()))
|
|
63
65
|
positive = score(words, POSITIVE)
|
|
64
66
|
negative = score(words, NEGATIVE)
|
|
@@ -86,6 +88,7 @@ def profile_chat(chat_name, messages):
|
|
|
86
88
|
"questionCount": text.count("?"),
|
|
87
89
|
"relationshipGuess": guess,
|
|
88
90
|
"tags": tags,
|
|
91
|
+
"typingStyle": typing_style(outbound_messages),
|
|
89
92
|
}
|
|
90
93
|
|
|
91
94
|
|
|
@@ -138,6 +141,7 @@ def write_markdown(path, profiles, days):
|
|
|
138
141
|
f"- Dates: {profile['firstSeen']} to {profile['lastSeen']}",
|
|
139
142
|
f"- Scores: sentiment {profile['sentimentScore']}, warmth {profile['warmthScore']}, friction {profile['frictionScore']}, operational {profile['operationalScore']}",
|
|
140
143
|
f"- Tags: {', '.join(profile['tags'])}",
|
|
144
|
+
f"- Typing style: {typing_style_summary(profile['typingStyle'])}",
|
|
141
145
|
"",
|
|
142
146
|
])
|
|
143
147
|
path.write_text("\n".join(lines), encoding="utf-8")
|
|
@@ -147,6 +151,83 @@ def score(words, lexicon):
|
|
|
147
151
|
return sum(words[word] for word in lexicon)
|
|
148
152
|
|
|
149
153
|
|
|
154
|
+
def typing_style(messages):
|
|
155
|
+
bodies = [(m.get("body") or "").strip() for m in messages if (m.get("body") or "").strip()]
|
|
156
|
+
count = len(bodies)
|
|
157
|
+
if not count:
|
|
158
|
+
return {
|
|
159
|
+
"sampleSize": 0,
|
|
160
|
+
"avgChars": 0,
|
|
161
|
+
"avgWords": 0,
|
|
162
|
+
"lowercaseShare": 0,
|
|
163
|
+
"uppercaseShare": 0,
|
|
164
|
+
"questionShare": 0,
|
|
165
|
+
"exclamationShare": 0,
|
|
166
|
+
"emojiShare": 0,
|
|
167
|
+
"slang": [],
|
|
168
|
+
"signature": "no outbound sample",
|
|
169
|
+
}
|
|
170
|
+
word_lists = [re.findall(r"[A-Za-z']+", body) for body in bodies]
|
|
171
|
+
all_words = [word.lower() for words in word_lists for word in words]
|
|
172
|
+
slang = Counter(word for word in all_words if word in SLANG)
|
|
173
|
+
avg_chars = sum(len(body) for body in bodies) / count
|
|
174
|
+
avg_words = sum(len(words) for words in word_lists) / count
|
|
175
|
+
lowercase = sum(1 for body in bodies if has_letters(body) and body == body.lower()) / count
|
|
176
|
+
uppercase = sum(1 for body in bodies if has_letters(body) and body == body.upper()) / count
|
|
177
|
+
questions = sum(1 for body in bodies if "?" in body) / count
|
|
178
|
+
exclaims = sum(1 for body in bodies if "!" in body) / count
|
|
179
|
+
emojis = sum(1 for body in bodies if has_emoji(body)) / count
|
|
180
|
+
return {
|
|
181
|
+
"sampleSize": count,
|
|
182
|
+
"avgChars": round(avg_chars, 1),
|
|
183
|
+
"avgWords": round(avg_words, 1),
|
|
184
|
+
"lowercaseShare": round(lowercase, 2),
|
|
185
|
+
"uppercaseShare": round(uppercase, 2),
|
|
186
|
+
"questionShare": round(questions, 2),
|
|
187
|
+
"exclamationShare": round(exclaims, 2),
|
|
188
|
+
"emojiShare": round(emojis, 2),
|
|
189
|
+
"slang": [word for word, _ in slang.most_common(8)],
|
|
190
|
+
"signature": infer_typing_signature(avg_words, lowercase, questions, exclaims, emojis, slang),
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def typing_style_summary(style):
|
|
195
|
+
slang = ", ".join(style.get("slang", [])) or "none"
|
|
196
|
+
return (
|
|
197
|
+
f"{style.get('signature', 'unknown')}; avg {style.get('avgWords', 0)} words; "
|
|
198
|
+
f"lowercase {style.get('lowercaseShare', 0)}; emoji {style.get('emojiShare', 0)}; slang {slang}"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def infer_typing_signature(avg_words, lowercase, questions, exclaims, emojis, slang):
|
|
203
|
+
parts = []
|
|
204
|
+
if avg_words <= 4:
|
|
205
|
+
parts.append("very short")
|
|
206
|
+
elif avg_words <= 10:
|
|
207
|
+
parts.append("short")
|
|
208
|
+
else:
|
|
209
|
+
parts.append("longer-form")
|
|
210
|
+
if lowercase > 0.55:
|
|
211
|
+
parts.append("lowercase-heavy")
|
|
212
|
+
if questions > 0.25:
|
|
213
|
+
parts.append("question-heavy")
|
|
214
|
+
if exclaims > 0.2:
|
|
215
|
+
parts.append("expressive")
|
|
216
|
+
if emojis > 0.2:
|
|
217
|
+
parts.append("emoji-friendly")
|
|
218
|
+
if slang:
|
|
219
|
+
parts.append("slangy")
|
|
220
|
+
return ", ".join(parts)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def has_letters(text):
|
|
224
|
+
return any(char.isalpha() for char in text)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def has_emoji(text):
|
|
228
|
+
return any(ord(char) > 10000 for char in text)
|
|
229
|
+
|
|
230
|
+
|
|
150
231
|
def normalized_sentiment(positive, negative, count):
|
|
151
232
|
if positive + negative == 0:
|
|
152
233
|
return 0
|
|
@@ -54,6 +54,7 @@ def build_model(profile, override):
|
|
|
54
54
|
"reciprocity": infer_reciprocity(profile),
|
|
55
55
|
"howToContinue": infer_continuity(profile, role),
|
|
56
56
|
"boundaries": infer_boundaries(role, infer_difficulty(profile)),
|
|
57
|
+
"replyStyle": infer_reply_style(profile),
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
|
|
@@ -136,6 +137,7 @@ Role: {model['role']}
|
|
|
136
137
|
Role confidence: {model['roleConfidence']}
|
|
137
138
|
Closeness: {model['closeness']}
|
|
138
139
|
Conversation difficulty: {model['conversationDifficulty']}
|
|
140
|
+
Typing style: {model['typingStyle'].get('signature', 'unknown')}
|
|
139
141
|
|
|
140
142
|
These are private working notes. Edit them where wrong.
|
|
141
143
|
|
|
@@ -152,9 +154,15 @@ These are private working notes. Edit them where wrong.
|
|
|
152
154
|
- Messages: {model['messageCount']} ({model['inbound']} inbound, {model['outbound']} outbound).
|
|
153
155
|
- Tags: {', '.join(model['tags'])}.
|
|
154
156
|
|
|
157
|
+
## Typing Style To Match
|
|
158
|
+
{render_typing_style(model)}
|
|
159
|
+
|
|
155
160
|
## How To Continue This Relationship
|
|
156
161
|
{bullets(model['howToContinue'])}
|
|
157
162
|
|
|
163
|
+
## Reply Guidance
|
|
164
|
+
{bullets(model['replyStyle'])}
|
|
165
|
+
|
|
158
166
|
## What Not To Assume
|
|
159
167
|
{bullets(model['boundaries'])}
|
|
160
168
|
"""
|
|
@@ -164,7 +172,8 @@ def write_index(path, models):
|
|
|
164
172
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
165
173
|
lines = ["# Interpreted Relationship Memory", "", "Generated working notes. Treat as editable, not truth.", ""]
|
|
166
174
|
for model in models:
|
|
167
|
-
|
|
175
|
+
style = model.get("typingStyle", {}).get("signature", "unknown style")
|
|
176
|
+
lines.append(f"- [[{safe_filename(model['chatName'])}]]: {model['role']} ({model['roleConfidence']}), closeness {model['closeness']}, difficulty {model['conversationDifficulty']}, style {style}")
|
|
168
177
|
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
169
178
|
|
|
170
179
|
|
|
@@ -172,6 +181,43 @@ def bullets(items):
|
|
|
172
181
|
return "\n".join(f"- {item}" for item in items)
|
|
173
182
|
|
|
174
183
|
|
|
184
|
+
def render_typing_style(model):
|
|
185
|
+
style = model.get("typingStyle", {})
|
|
186
|
+
slang = ", ".join(style.get("slang", [])) or "none detected"
|
|
187
|
+
lines = [
|
|
188
|
+
f"- Signature: {style.get('signature', 'unknown')}.",
|
|
189
|
+
f"- Average length: {style.get('avgWords', 0)} words / {style.get('avgChars', 0)} chars.",
|
|
190
|
+
f"- Lowercase share: {style.get('lowercaseShare', 0)}.",
|
|
191
|
+
f"- Question share: {style.get('questionShare', 0)}.",
|
|
192
|
+
f"- Exclamation share: {style.get('exclamationShare', 0)}.",
|
|
193
|
+
f"- Emoji share: {style.get('emojiShare', 0)}.",
|
|
194
|
+
f"- Slang: {slang}.",
|
|
195
|
+
]
|
|
196
|
+
return "\n".join(lines)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def infer_reply_style(profile):
|
|
200
|
+
style = profile.get("typingStyle", {})
|
|
201
|
+
guidance = []
|
|
202
|
+
avg_words = style.get("avgWords", 0)
|
|
203
|
+
if avg_words and avg_words <= 5:
|
|
204
|
+
guidance.append("Keep replies very short unless context demands detail.")
|
|
205
|
+
elif avg_words <= 12:
|
|
206
|
+
guidance.append("Use concise replies with one clear point.")
|
|
207
|
+
else:
|
|
208
|
+
guidance.append("Longer replies are acceptable in this relationship.")
|
|
209
|
+
if style.get("lowercaseShare", 0) > 0.55:
|
|
210
|
+
guidance.append("Lowercase is acceptable; avoid making it too formal.")
|
|
211
|
+
if style.get("emojiShare", 0) > 0.2:
|
|
212
|
+
guidance.append("A light emoji can fit this relationship.")
|
|
213
|
+
if style.get("questionShare", 0) > 0.25:
|
|
214
|
+
guidance.append("Asking a direct question fits the existing rhythm.")
|
|
215
|
+
if style.get("slang"):
|
|
216
|
+
guidance.append(f"Some familiar slang appears here: {', '.join(style['slang'][:4])}.")
|
|
217
|
+
guidance.append("Match style without exaggerating or parodying the person.")
|
|
218
|
+
return guidance
|
|
219
|
+
|
|
220
|
+
|
|
175
221
|
def safe_filename(value):
|
|
176
222
|
cleaned = "".join("-" if char in '/:\\?%*"<>|' else char for char in value)
|
|
177
223
|
return (" ".join(cleaned.split()).strip() or "Unknown Chat")[:120]
|