create-walle 0.1.0
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/bin/create-walle.js +134 -0
- package/package.json +18 -0
- package/template/.env.example +40 -0
- package/template/CLAUDE.md +12 -0
- package/template/LICENSE +21 -0
- package/template/README.md +167 -0
- package/template/bin/setup.js +100 -0
- package/template/claude-code-skill.md +60 -0
- package/template/claude-task-manager/api-prompts.js +1841 -0
- package/template/claude-task-manager/api-reviews.js +275 -0
- package/template/claude-task-manager/approval-agent.js +454 -0
- package/template/claude-task-manager/bin/restart-ctm.sh +16 -0
- package/template/claude-task-manager/db.js +1721 -0
- package/template/claude-task-manager/docs/PROMPT-MANAGEMENT-DESIGN.md +631 -0
- package/template/claude-task-manager/git-utils.js +214 -0
- package/template/claude-task-manager/package-lock.json +1607 -0
- package/template/claude-task-manager/package.json +31 -0
- package/template/claude-task-manager/prompt-harvest.js +1148 -0
- package/template/claude-task-manager/public/css/prompts.css +880 -0
- package/template/claude-task-manager/public/css/reviews.css +430 -0
- package/template/claude-task-manager/public/css/walle.css +732 -0
- package/template/claude-task-manager/public/favicon.ico +0 -0
- package/template/claude-task-manager/public/icon.svg +37 -0
- package/template/claude-task-manager/public/index.html +8346 -0
- package/template/claude-task-manager/public/js/prompts.js +3159 -0
- package/template/claude-task-manager/public/js/reviews.js +1292 -0
- package/template/claude-task-manager/public/js/walle.js +3081 -0
- package/template/claude-task-manager/public/manifest.json +13 -0
- package/template/claude-task-manager/public/prompts.html +4353 -0
- package/template/claude-task-manager/public/setup.html +216 -0
- package/template/claude-task-manager/queue-engine.js +404 -0
- package/template/claude-task-manager/server-state.js +5 -0
- package/template/claude-task-manager/server.js +2254 -0
- package/template/claude-task-manager/session-utils.js +124 -0
- package/template/claude-task-manager/start.sh +17 -0
- package/template/claude-task-manager/tests/test-ai-search.js +61 -0
- package/template/claude-task-manager/tests/test-editor-ux.js +76 -0
- package/template/claude-task-manager/tests/test-editor-ux2.js +51 -0
- package/template/claude-task-manager/tests/test-features-v2.js +127 -0
- package/template/claude-task-manager/tests/test-insights-cached.js +78 -0
- package/template/claude-task-manager/tests/test-insights.js +124 -0
- package/template/claude-task-manager/tests/test-permissions-v2.js +127 -0
- package/template/claude-task-manager/tests/test-permissions.js +122 -0
- package/template/claude-task-manager/tests/test-pin.js +51 -0
- package/template/claude-task-manager/tests/test-prompts.js +164 -0
- package/template/claude-task-manager/tests/test-recent-sessions.js +96 -0
- package/template/claude-task-manager/tests/test-review.js +104 -0
- package/template/claude-task-manager/tests/test-send-dropdown.js +76 -0
- package/template/claude-task-manager/tests/test-send-final.js +30 -0
- package/template/claude-task-manager/tests/test-send-fixes.js +76 -0
- package/template/claude-task-manager/tests/test-send-integration.js +107 -0
- package/template/claude-task-manager/tests/test-send-visual.js +34 -0
- package/template/claude-task-manager/tests/test-session-create.js +147 -0
- package/template/claude-task-manager/tests/test-sidebar-ux.js +83 -0
- package/template/claude-task-manager/tests/test-url-hash.js +68 -0
- package/template/claude-task-manager/tests/test-ux-crop.js +34 -0
- package/template/claude-task-manager/tests/test-ux-review.js +130 -0
- package/template/claude-task-manager/tests/test-zoom-card.js +76 -0
- package/template/claude-task-manager/tests/test-zoom.js +92 -0
- package/template/claude-task-manager/tests/test-zoom2.js +67 -0
- package/template/docs/site/api/README.md +187 -0
- package/template/docs/site/guides/claude-code.md +58 -0
- package/template/docs/site/guides/configuration.md +96 -0
- package/template/docs/site/guides/quickstart.md +158 -0
- package/template/docs/site/index.md +14 -0
- package/template/docs/site/skills/README.md +135 -0
- package/template/wall-e/.dockerignore +11 -0
- package/template/wall-e/Dockerfile +25 -0
- package/template/wall-e/adapters/adapter-base.js +37 -0
- package/template/wall-e/adapters/ctm.js +193 -0
- package/template/wall-e/adapters/slack.js +56 -0
- package/template/wall-e/agent.js +319 -0
- package/template/wall-e/api-walle.js +1073 -0
- package/template/wall-e/brain.js +1235 -0
- package/template/wall-e/channels/agent-api.js +172 -0
- package/template/wall-e/channels/channel-base.js +14 -0
- package/template/wall-e/channels/imessage-channel.js +113 -0
- package/template/wall-e/channels/slack-channel.js +118 -0
- package/template/wall-e/chat.js +778 -0
- package/template/wall-e/decision/confidence.js +93 -0
- package/template/wall-e/deploy.sh +35 -0
- package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +112 -0
- package/template/wall-e/docs/specs/SKILL-FORMAT.md +326 -0
- package/template/wall-e/extraction/contradiction.js +168 -0
- package/template/wall-e/extraction/knowledge-extractor.js +190 -0
- package/template/wall-e/fly.toml +24 -0
- package/template/wall-e/loops/ingest.js +34 -0
- package/template/wall-e/loops/reflect.js +63 -0
- package/template/wall-e/loops/tasks.js +487 -0
- package/template/wall-e/loops/think.js +125 -0
- package/template/wall-e/package-lock.json +533 -0
- package/template/wall-e/package.json +18 -0
- package/template/wall-e/scripts/ingest-slack-search.js +85 -0
- package/template/wall-e/scripts/pull-slack-via-claude.js +98 -0
- package/template/wall-e/scripts/slack-backfill.js +295 -0
- package/template/wall-e/scripts/slack-channel-history.js +454 -0
- package/template/wall-e/server.js +93 -0
- package/template/wall-e/skills/_bundled/email-digest/SKILL.md +95 -0
- package/template/wall-e/skills/_bundled/email-sync/SKILL.md +65 -0
- package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +104 -0
- package/template/wall-e/skills/_bundled/email-sync/run.js +213 -0
- package/template/wall-e/skills/_bundled/google-calendar/SKILL.md +73 -0
- package/template/wall-e/skills/_bundled/google-calendar/cal-reader.swift +81 -0
- package/template/wall-e/skills/_bundled/google-calendar/run.js +181 -0
- package/template/wall-e/skills/_bundled/memory-search/SKILL.md +92 -0
- package/template/wall-e/skills/_bundled/morning-briefing/SKILL.md +131 -0
- package/template/wall-e/skills/_bundled/morning-briefing/run.js +264 -0
- package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +60 -0
- package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +55 -0
- package/template/wall-e/skills/claude-code-reader.js +144 -0
- package/template/wall-e/skills/mcp-client.js +407 -0
- package/template/wall-e/skills/skill-executor.js +163 -0
- package/template/wall-e/skills/skill-loader.js +410 -0
- package/template/wall-e/skills/skill-planner.js +88 -0
- package/template/wall-e/skills/slack-ingest.js +329 -0
- package/template/wall-e/skills/slack-pull-live.js +270 -0
- package/template/wall-e/skills/tool-executor.js +188 -0
- package/template/wall-e/tests/adapter-base.test.js +20 -0
- package/template/wall-e/tests/adapter-ctm.test.js +122 -0
- package/template/wall-e/tests/adapter-slack.test.js +98 -0
- package/template/wall-e/tests/agent-api.test.js +256 -0
- package/template/wall-e/tests/api-walle.test.js +222 -0
- package/template/wall-e/tests/brain.test.js +602 -0
- package/template/wall-e/tests/channels.test.js +104 -0
- package/template/wall-e/tests/chat.test.js +103 -0
- package/template/wall-e/tests/confidence.test.js +134 -0
- package/template/wall-e/tests/contradiction.test.js +217 -0
- package/template/wall-e/tests/ingest.test.js +113 -0
- package/template/wall-e/tests/mcp-client.test.js +71 -0
- package/template/wall-e/tests/reflect.test.js +103 -0
- package/template/wall-e/tests/server.test.js +111 -0
- package/template/wall-e/tests/skills.test.js +198 -0
- package/template/wall-e/tests/slack-ingest.test.js +103 -0
- package/template/wall-e/tests/think.test.js +435 -0
- package/template/wall-e/tools/local-tools.js +697 -0
- package/template/wall-e/tools/slack-mcp.js +290 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: email-sync
|
|
3
|
+
description: >
|
|
4
|
+
Sync sent emails from macOS Mail into WALL-E's brain so it can learn
|
|
5
|
+
the owner's communication style. Always syncs all sent mail with body
|
|
6
|
+
content. Inbox sync is optional and filters to only emails addressed
|
|
7
|
+
directly to the owner (on To/Cc line). Uses JXA to read Mail.app.
|
|
8
|
+
version: 1.1.0
|
|
9
|
+
author: juncao
|
|
10
|
+
execution: script
|
|
11
|
+
entry: run.js
|
|
12
|
+
args: ["--days-back", "3"]
|
|
13
|
+
trigger:
|
|
14
|
+
type: manual
|
|
15
|
+
config:
|
|
16
|
+
days_back:
|
|
17
|
+
type: number
|
|
18
|
+
default: 3
|
|
19
|
+
description: "How many days back to sync"
|
|
20
|
+
sync_inbox:
|
|
21
|
+
type: boolean
|
|
22
|
+
default: false
|
|
23
|
+
description: "Also sync inbox emails addressed directly to owner"
|
|
24
|
+
tags: [email, gmail, mail, sent, sync, learning]
|
|
25
|
+
permissions:
|
|
26
|
+
- mail:read
|
|
27
|
+
- brain:write
|
|
28
|
+
---
|
|
29
|
+
# Email Sync
|
|
30
|
+
|
|
31
|
+
## What This Skill Does
|
|
32
|
+
|
|
33
|
+
Syncs sent emails from macOS Mail into WALL-E's brain so it can learn the
|
|
34
|
+
owner's writing style, communication patterns, and topics of interest.
|
|
35
|
+
Optionally syncs inbox emails that are addressed directly to the owner.
|
|
36
|
+
|
|
37
|
+
## How It Works
|
|
38
|
+
|
|
39
|
+
1. Run JXA script via `osascript` to read sent messages from all accounts
|
|
40
|
+
2. Sent mail always includes body content (first 2000 chars) for learning
|
|
41
|
+
3. Inbox is opt-in (`--sync-inbox`), filtered to emails where owner is on To/Cc
|
|
42
|
+
4. Dedup by `source_id` = `mail:{messageId}` — skip unchanged, update modified
|
|
43
|
+
5. Store as memories with `source: 'email'`, `source_channel: <account name>`
|
|
44
|
+
|
|
45
|
+
## Memory Format
|
|
46
|
+
|
|
47
|
+
Each email is stored as a memory:
|
|
48
|
+
- **source**: `email`
|
|
49
|
+
- **source_channel**: account name (e.g., `owner@example.com`)
|
|
50
|
+
- **memory_type**: `email_sent` or `email_received`
|
|
51
|
+
- **subject**: email subject
|
|
52
|
+
- **content**: human-readable summary with recipients + body for sent mail
|
|
53
|
+
- **metadata**: JSON with messageId, sender, recipients, dates
|
|
54
|
+
- **timestamp**: date sent/received (ISO8601)
|
|
55
|
+
- **importance**: 0.6 for sent (learning), 0.4 for received
|
|
56
|
+
|
|
57
|
+
## Prerequisites
|
|
58
|
+
|
|
59
|
+
macOS Mail.app must be configured with your email accounts and running.
|
|
60
|
+
The terminal must have Automation access for Mail.app:
|
|
61
|
+
**System Settings > Privacy & Security > Automation > [terminal] > Mail**
|
|
62
|
+
|
|
63
|
+
## Output
|
|
64
|
+
|
|
65
|
+
Returns: `{ inserted, updated, skipped, filtered, errors, total }`
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// mail-reader.jxa — Read emails from macOS Mail via JXA
|
|
2
|
+
// Usage: osascript -l JavaScript mail-reader.jxa [--days-back N] [--inbox]
|
|
3
|
+
//
|
|
4
|
+
// Always reads Sent with content. Inbox is opt-in via --inbox flag.
|
|
5
|
+
// Outputs JSON: { accounts: [...], inbox: [...], sent: [...] }
|
|
6
|
+
|
|
7
|
+
function run(argv) {
|
|
8
|
+
var daysBack = 3;
|
|
9
|
+
var readInbox = false;
|
|
10
|
+
|
|
11
|
+
for (var i = 0; i < argv.length; i++) {
|
|
12
|
+
if (argv[i] === '--days-back' && argv[i + 1]) daysBack = parseInt(argv[i + 1], 10);
|
|
13
|
+
if (argv[i] === '--inbox') readInbox = true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
var mail = Application('Mail');
|
|
17
|
+
var cutoff = new Date();
|
|
18
|
+
cutoff.setDate(cutoff.getDate() - daysBack);
|
|
19
|
+
|
|
20
|
+
var results = { accounts: [], inbox: [], sent: [] };
|
|
21
|
+
var accounts = mail.accounts();
|
|
22
|
+
|
|
23
|
+
for (var a = 0; a < accounts.length; a++) {
|
|
24
|
+
var acct = accounts[a];
|
|
25
|
+
var acctName = acct.name();
|
|
26
|
+
|
|
27
|
+
// Collect account email addresses for inbox filtering
|
|
28
|
+
var acctEmails = [];
|
|
29
|
+
try { acctEmails = acct.emailAddresses(); } catch(e) {}
|
|
30
|
+
results.accounts.push({ name: acctName, emails: acctEmails });
|
|
31
|
+
|
|
32
|
+
// Read inbox (opt-in only)
|
|
33
|
+
if (readInbox) {
|
|
34
|
+
try {
|
|
35
|
+
var inbox = acct.mailboxes.byName('INBOX');
|
|
36
|
+
var inboxMsgs = inbox.messages.whose({ dateReceived: { _greaterThan: cutoff } })();
|
|
37
|
+
for (var m = 0; m < inboxMsgs.length; m++) {
|
|
38
|
+
try {
|
|
39
|
+
var msg = inboxMsgs[m];
|
|
40
|
+
var toRecips = [];
|
|
41
|
+
try { toRecips = msg.toRecipients().map(function(r) { return { name: r.name(), email: r.address() }; }); } catch(e) {}
|
|
42
|
+
var ccRecips = [];
|
|
43
|
+
try { ccRecips = msg.ccRecipients().map(function(r) { return { name: r.name(), email: r.address() }; }); } catch(e) {}
|
|
44
|
+
|
|
45
|
+
results.inbox.push({
|
|
46
|
+
messageId: msg.messageId(),
|
|
47
|
+
subject: msg.subject() || '(no subject)',
|
|
48
|
+
sender: msg.sender(),
|
|
49
|
+
to: toRecips,
|
|
50
|
+
cc: ccRecips,
|
|
51
|
+
date: msg.dateReceived().toISOString(),
|
|
52
|
+
account: acctName,
|
|
53
|
+
mailbox: 'INBOX',
|
|
54
|
+
readStatus: msg.readStatus()
|
|
55
|
+
});
|
|
56
|
+
} catch(e) { /* skip individual message errors */ }
|
|
57
|
+
}
|
|
58
|
+
} catch(e) { /* account may not have INBOX */ }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Always read sent — try common mailbox names
|
|
62
|
+
var sentNames = ['[Gmail]/Sent Mail', 'Sent Messages', 'Sent', 'Sent Items'];
|
|
63
|
+
for (var s = 0; s < sentNames.length; s++) {
|
|
64
|
+
try {
|
|
65
|
+
var sentBox = acct.mailboxes.byName(sentNames[s]);
|
|
66
|
+
var sentMsgs = sentBox.messages.whose({ dateSent: { _greaterThan: cutoff } })();
|
|
67
|
+
for (var m = 0; m < sentMsgs.length; m++) {
|
|
68
|
+
try {
|
|
69
|
+
var msg = sentMsgs[m];
|
|
70
|
+
var toRecips = [];
|
|
71
|
+
try { toRecips = msg.toRecipients().map(function(r) { return { name: r.name(), email: r.address() }; }); } catch(e) {}
|
|
72
|
+
var ccRecips = [];
|
|
73
|
+
try { ccRecips = msg.ccRecipients().map(function(r) { return { name: r.name(), email: r.address() }; }); } catch(e) {}
|
|
74
|
+
|
|
75
|
+
var entry = {
|
|
76
|
+
messageId: msg.messageId(),
|
|
77
|
+
subject: msg.subject() || '(no subject)',
|
|
78
|
+
sender: msg.sender(),
|
|
79
|
+
to: toRecips,
|
|
80
|
+
cc: ccRecips,
|
|
81
|
+
date: msg.dateSent().toISOString(),
|
|
82
|
+
account: acctName,
|
|
83
|
+
mailbox: sentNames[s],
|
|
84
|
+
readStatus: true
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Always fetch content for sent messages
|
|
88
|
+
try {
|
|
89
|
+
var body = msg.content();
|
|
90
|
+
if (body && body.length > 0) {
|
|
91
|
+
entry.content = body.substring(0, 2000);
|
|
92
|
+
}
|
|
93
|
+
} catch(e) { /* content extraction failed */ }
|
|
94
|
+
|
|
95
|
+
results.sent.push(entry);
|
|
96
|
+
} catch(e) { /* skip individual message errors */ }
|
|
97
|
+
}
|
|
98
|
+
break; // found the sent mailbox for this account
|
|
99
|
+
} catch(e) { /* try next sent mailbox name */ }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return JSON.stringify(results);
|
|
104
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/**
|
|
4
|
+
* email-sync skill — reads sent emails from macOS Mail (which syncs with
|
|
5
|
+
* Gmail/Google Workspace) via JXA, then stores them as memories in
|
|
6
|
+
* WALL-E's brain so it can learn the owner's communication style.
|
|
7
|
+
*
|
|
8
|
+
* By default, only SENT emails are synced (with body content).
|
|
9
|
+
* Inbox sync is opt-in via --sync-inbox, and when enabled only emails
|
|
10
|
+
* where the owner is on the To/Cc line are included.
|
|
11
|
+
*
|
|
12
|
+
* Usage: node run.js [--days-back N] [--sync-inbox]
|
|
13
|
+
*
|
|
14
|
+
* Requires: Mail.app running with accounts configured. Terminal needs
|
|
15
|
+
* Automation access for Mail in System Settings > Privacy & Security.
|
|
16
|
+
*/
|
|
17
|
+
const { execFileSync } = require('child_process');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
|
|
21
|
+
const SKILL_DIR = __dirname;
|
|
22
|
+
const JXA_SCRIPT = path.join(SKILL_DIR, 'mail-reader.jxa');
|
|
23
|
+
|
|
24
|
+
// Parse CLI args
|
|
25
|
+
const args = process.argv.slice(2);
|
|
26
|
+
let daysBack = 3;
|
|
27
|
+
let syncInbox = false;
|
|
28
|
+
for (let i = 0; i < args.length; i++) {
|
|
29
|
+
if (args[i] === '--days-back' && args[i + 1]) daysBack = parseInt(args[i + 1], 10);
|
|
30
|
+
if (args[i] === '--sync-inbox') syncInbox = true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Task-level config overrides (passed via WALL_E_SKILL_CONFIG env var)
|
|
34
|
+
try {
|
|
35
|
+
const envConfig = process.env.WALL_E_SKILL_CONFIG
|
|
36
|
+
? JSON.parse(process.env.WALL_E_SKILL_CONFIG) : {};
|
|
37
|
+
if (envConfig.days_back != null) daysBack = envConfig.days_back;
|
|
38
|
+
if (envConfig.sync_inbox != null) syncInbox = !!envConfig.sync_inbox;
|
|
39
|
+
} catch { /* ignore parse errors */ }
|
|
40
|
+
|
|
41
|
+
// ── Step 1: Read emails via JXA ──────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
function readEmails() {
|
|
44
|
+
const jxaArgs = ['-l', 'JavaScript', JXA_SCRIPT, '--days-back', String(daysBack)];
|
|
45
|
+
if (syncInbox) jxaArgs.push('--inbox');
|
|
46
|
+
|
|
47
|
+
const result = execFileSync('osascript', jxaArgs, {
|
|
48
|
+
timeout: 300_000, // 5 min for content fetching
|
|
49
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const data = JSON.parse(result.toString('utf8'));
|
|
53
|
+
console.error(`[email-sync] Read ${data.inbox.length} inbox + ${data.sent.length} sent (${daysBack}d back)`);
|
|
54
|
+
return data;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Step 2: Open Automation privacy settings on permission error ─────
|
|
58
|
+
|
|
59
|
+
function openAutomationSettings() {
|
|
60
|
+
try {
|
|
61
|
+
execFileSync('open', ['x-apple.systempreferences:com.apple.preference.security?Privacy_Automation'], {
|
|
62
|
+
timeout: 5_000,
|
|
63
|
+
});
|
|
64
|
+
console.error('[email-sync] Opened System Settings > Privacy & Security > Automation');
|
|
65
|
+
} catch {
|
|
66
|
+
console.error('[email-sync] Please open: System Settings > Privacy & Security > Automation');
|
|
67
|
+
console.error('[email-sync] Enable Mail access for your terminal app.');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Step 3: Store in brain ───────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
function storeToBrain(data) {
|
|
74
|
+
const brainPath = path.resolve(SKILL_DIR, '..', '..', '..', 'brain.js');
|
|
75
|
+
if (!fs.existsSync(brainPath)) {
|
|
76
|
+
console.error(`[email-sync] brain.js not found at ${brainPath}`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
const brain = require(brainPath);
|
|
80
|
+
brain.initDb();
|
|
81
|
+
|
|
82
|
+
let inserted = 0;
|
|
83
|
+
let skipped = 0;
|
|
84
|
+
let updated = 0;
|
|
85
|
+
let filtered = 0;
|
|
86
|
+
let errors = 0;
|
|
87
|
+
|
|
88
|
+
// Collect owner's email addresses from all accounts
|
|
89
|
+
const ownerEmails = new Set();
|
|
90
|
+
for (const acct of (data.accounts || [])) {
|
|
91
|
+
for (const email of (acct.emails || [])) {
|
|
92
|
+
ownerEmails.add(email.toLowerCase());
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
console.error(`[email-sync] Owner emails: ${[...ownerEmails].join(', ')}`);
|
|
96
|
+
|
|
97
|
+
// Filter inbox: only emails where owner is on To or Cc line
|
|
98
|
+
const inboxFiltered = data.inbox.filter(msg => {
|
|
99
|
+
const allRecips = [...(msg.to || []), ...(msg.cc || [])];
|
|
100
|
+
return allRecips.some(r => r.email && ownerEmails.has(r.email.toLowerCase()));
|
|
101
|
+
});
|
|
102
|
+
filtered = data.inbox.length - inboxFiltered.length;
|
|
103
|
+
if (data.inbox.length > 0) {
|
|
104
|
+
console.error(`[email-sync] Inbox: ${inboxFiltered.length} addressed to owner, ${filtered} filtered out (mailing lists, BCC, etc.)`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const allMessages = [
|
|
108
|
+
...inboxFiltered.map(m => ({ ...m, type: 'email_received' })),
|
|
109
|
+
...data.sent.map(m => ({ ...m, type: 'email_sent' })),
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
for (const msg of allMessages) {
|
|
113
|
+
try {
|
|
114
|
+
// Build readable content
|
|
115
|
+
const parts = [msg.subject];
|
|
116
|
+
parts.push(`From: ${msg.sender}`);
|
|
117
|
+
if (msg.to && msg.to.length > 0) {
|
|
118
|
+
const names = msg.to.map(r => r.name || r.email).filter(Boolean);
|
|
119
|
+
if (names.length > 0) parts.push(`To: ${names.join(', ')}`);
|
|
120
|
+
}
|
|
121
|
+
if (msg.cc && msg.cc.length > 0) {
|
|
122
|
+
const names = msg.cc.map(r => r.name || r.email).filter(Boolean);
|
|
123
|
+
if (names.length > 0) parts.push(`Cc: ${names.join(', ')}`);
|
|
124
|
+
}
|
|
125
|
+
if (msg.content) {
|
|
126
|
+
parts.push(`\n${msg.content}`);
|
|
127
|
+
}
|
|
128
|
+
const content = parts.join('\n');
|
|
129
|
+
|
|
130
|
+
const sourceId = `mail:${msg.messageId}`;
|
|
131
|
+
|
|
132
|
+
// Check if exists
|
|
133
|
+
const existing = brain.getDb().prepare(
|
|
134
|
+
'SELECT id, content FROM memories WHERE source = ? AND source_id = ?'
|
|
135
|
+
).get('email', sourceId);
|
|
136
|
+
|
|
137
|
+
if (existing) {
|
|
138
|
+
if (existing.content !== content) {
|
|
139
|
+
brain.getDb().prepare(
|
|
140
|
+
'UPDATE memories SET content = ?, metadata = ?, timestamp = ? WHERE id = ?'
|
|
141
|
+
).run(content, JSON.stringify(msg), msg.date, existing.id);
|
|
142
|
+
updated++;
|
|
143
|
+
} else {
|
|
144
|
+
skipped++;
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Insert new
|
|
150
|
+
const mem = {
|
|
151
|
+
source: 'email',
|
|
152
|
+
source_id: sourceId,
|
|
153
|
+
source_channel: msg.account,
|
|
154
|
+
memory_type: msg.type,
|
|
155
|
+
subject: msg.subject,
|
|
156
|
+
content,
|
|
157
|
+
metadata: JSON.stringify(msg),
|
|
158
|
+
importance: msg.type === 'email_sent' ? 0.6 : 0.4,
|
|
159
|
+
timestamp: msg.date,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const result = brain.insertMemory(mem);
|
|
163
|
+
if (result) inserted++;
|
|
164
|
+
else skipped++;
|
|
165
|
+
} catch (err) {
|
|
166
|
+
errors++;
|
|
167
|
+
console.error(`[email-sync] Error processing message: ${err.message}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
brain.closeDb();
|
|
172
|
+
return { inserted, updated, skipped, filtered, errors, total: allMessages.length };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── Main ─────────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
function sleep(ms) {
|
|
178
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function main() {
|
|
182
|
+
const MAX_RETRIES = 2;
|
|
183
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
184
|
+
try {
|
|
185
|
+
const data = readEmails();
|
|
186
|
+
const stats = storeToBrain(data);
|
|
187
|
+
console.log(JSON.stringify(stats));
|
|
188
|
+
console.error(`[email-sync] Done: ${stats.inserted} new, ${stats.updated} updated, ${stats.skipped} unchanged, ${stats.filtered} filtered, ${stats.errors} errors`);
|
|
189
|
+
return;
|
|
190
|
+
} catch (err) {
|
|
191
|
+
const errMsg = (err.stderr || err.message || '').toString();
|
|
192
|
+
const isPermDenied = errMsg.includes('not allowed') || errMsg.includes('assistive access')
|
|
193
|
+
|| errMsg.includes('-1743') || errMsg.includes('-1744') || errMsg.includes('AppleEvent');
|
|
194
|
+
|
|
195
|
+
if (isPermDenied && attempt < MAX_RETRIES) {
|
|
196
|
+
console.error(`[email-sync] Mail access denied (attempt ${attempt + 1}/${MAX_RETRIES + 1}).`);
|
|
197
|
+
console.error('[email-sync] Opening System Settings — please grant Automation access for Mail to your terminal app...');
|
|
198
|
+
openAutomationSettings();
|
|
199
|
+
await sleep(15_000);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (isPermDenied) {
|
|
203
|
+
console.error('[email-sync] Mail access still denied after retries.');
|
|
204
|
+
console.error('[email-sync] Grant Automation > Mail access to your terminal in System Settings > Privacy & Security.');
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
console.error(`[email-sync] Error: ${err.message}`);
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
main();
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: google-calendar
|
|
3
|
+
description: >
|
|
4
|
+
Sync Google Calendar events into WALL-E's brain via macOS Calendar.
|
|
5
|
+
Reads events using EventKit (fast native API), stores as memories
|
|
6
|
+
with attendees, location, notes. Supports incremental sync with
|
|
7
|
+
update detection. Use for schedule awareness, meeting prep, calendar context.
|
|
8
|
+
version: 1.1.0
|
|
9
|
+
author: juncao
|
|
10
|
+
execution: script
|
|
11
|
+
entry: run.js
|
|
12
|
+
args: ["--days-back", "1", "--days-ahead", "14"]
|
|
13
|
+
trigger:
|
|
14
|
+
type: interval
|
|
15
|
+
schedule: "every 30m"
|
|
16
|
+
config:
|
|
17
|
+
days_back:
|
|
18
|
+
type: number
|
|
19
|
+
default: 1
|
|
20
|
+
description: "How many days back to sync"
|
|
21
|
+
days_ahead:
|
|
22
|
+
type: number
|
|
23
|
+
default: 14
|
|
24
|
+
description: "How many days ahead to sync"
|
|
25
|
+
tags: [calendar, google, schedule, meetings, events, sync]
|
|
26
|
+
permissions:
|
|
27
|
+
- calendar:read
|
|
28
|
+
- brain:write
|
|
29
|
+
---
|
|
30
|
+
# Google Calendar Sync
|
|
31
|
+
|
|
32
|
+
## What This Skill Does
|
|
33
|
+
|
|
34
|
+
Syncs calendar events from macOS Calendar (which mirrors Google Calendar) into
|
|
35
|
+
WALL-E's brain as searchable memories. Uses Apple's EventKit framework via a
|
|
36
|
+
compiled Swift binary for fast, reliable access.
|
|
37
|
+
|
|
38
|
+
## How It Works
|
|
39
|
+
|
|
40
|
+
1. Compile `cal-reader.swift` to a native binary (cached, only recompiles on change)
|
|
41
|
+
2. Run binary to read events from ALL calendars (Google, Outlook, iCloud, etc.)
|
|
42
|
+
3. For each event, build a content string with title, attendees, location, notes
|
|
43
|
+
4. Dedup by `source_id` = `cal:{uid}:{start}` -- skip unchanged, update modified
|
|
44
|
+
5. Store as memories with `source: 'calendar'`, `source_channel: <calendar name>`
|
|
45
|
+
|
|
46
|
+
## Memory Format
|
|
47
|
+
|
|
48
|
+
Each event is stored as a memory:
|
|
49
|
+
- **source**: `calendar`
|
|
50
|
+
- **source_channel**: calendar name (e.g., `owner@example.com`, `Family`)
|
|
51
|
+
- **memory_type**: `calendar_event` or `calendar_allday`
|
|
52
|
+
- **subject**: event title
|
|
53
|
+
- **content**: human-readable summary with attendees, location, notes
|
|
54
|
+
- **metadata**: full JSON event data (uid, start, end, attendees with RSVP status)
|
|
55
|
+
- **timestamp**: event start time (ISO8601)
|
|
56
|
+
|
|
57
|
+
## Prerequisites
|
|
58
|
+
|
|
59
|
+
The terminal running WALL-E must have Calendar access granted:
|
|
60
|
+
**System Settings > Privacy & Security > Calendars > [your terminal app]**
|
|
61
|
+
|
|
62
|
+
The Swift compiler (`swiftc`) must be available (ships with Xcode or Command Line Tools).
|
|
63
|
+
|
|
64
|
+
## Output
|
|
65
|
+
|
|
66
|
+
Returns: `{ inserted, updated, skipped, total }`
|
|
67
|
+
|
|
68
|
+
## Common Queries After Sync
|
|
69
|
+
|
|
70
|
+
Once synced, calendar events can be found with brain searches like:
|
|
71
|
+
- `source:calendar` -- all calendar events
|
|
72
|
+
- `source_channel:owner@example.com` -- work calendar only
|
|
73
|
+
- Search by attendee name, meeting title, or location
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import EventKit
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
// Accept CLI args: --days-back N --days-ahead N
|
|
5
|
+
var daysBack = 1
|
|
6
|
+
var daysAhead = 14
|
|
7
|
+
var args = CommandLine.arguments
|
|
8
|
+
for i in 0..<args.count {
|
|
9
|
+
if args[i] == "--days-back", i + 1 < args.count, let n = Int(args[i + 1]) { daysBack = n }
|
|
10
|
+
if args[i] == "--days-ahead", i + 1 < args.count, let n = Int(args[i + 1]) { daysAhead = n }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let store = EKEventStore()
|
|
14
|
+
let status = EKEventStore.authorizationStatus(for: .event)
|
|
15
|
+
|
|
16
|
+
switch status {
|
|
17
|
+
case .fullAccess, .authorized:
|
|
18
|
+
break
|
|
19
|
+
case .notDetermined:
|
|
20
|
+
let sem = DispatchSemaphore(value: 0)
|
|
21
|
+
store.requestFullAccessToEvents(completion: { granted, error in
|
|
22
|
+
if !granted {
|
|
23
|
+
fputs("{\"error\":\"access_denied\",\"message\":\"Calendar access was denied. Grant access in System Settings > Privacy & Security > Calendars.\"}\n", stderr)
|
|
24
|
+
exit(1)
|
|
25
|
+
}
|
|
26
|
+
sem.signal()
|
|
27
|
+
})
|
|
28
|
+
sem.wait()
|
|
29
|
+
default:
|
|
30
|
+
fputs("{\"error\":\"access_denied\",\"message\":\"Calendar access denied (status=\(status.rawValue)). Grant access to your terminal in System Settings > Privacy & Security > Calendars.\"}\n", stderr)
|
|
31
|
+
exit(1)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let cal = Calendar.current
|
|
35
|
+
let start = cal.date(byAdding: .day, value: -daysBack, to: Date())!
|
|
36
|
+
let end = cal.date(byAdding: .day, value: daysAhead, to: Date())!
|
|
37
|
+
let pred = store.predicateForEvents(withStart: start, end: end, calendars: nil)
|
|
38
|
+
let events = store.events(matching: pred)
|
|
39
|
+
|
|
40
|
+
let fmt = ISO8601DateFormatter()
|
|
41
|
+
var arr: [[String: Any]] = []
|
|
42
|
+
|
|
43
|
+
for e in events {
|
|
44
|
+
var d: [String: Any] = [
|
|
45
|
+
"title": e.title ?? "(no title)",
|
|
46
|
+
"start": fmt.string(from: e.startDate),
|
|
47
|
+
"end": fmt.string(from: e.endDate),
|
|
48
|
+
"allDay": e.isAllDay,
|
|
49
|
+
"calendar": e.calendar.title,
|
|
50
|
+
"uid": e.calendarItemIdentifier,
|
|
51
|
+
"location": e.location ?? ""
|
|
52
|
+
]
|
|
53
|
+
if let notes = e.notes, !notes.isEmpty {
|
|
54
|
+
d["notes"] = String(notes.prefix(500))
|
|
55
|
+
}
|
|
56
|
+
if let attendees = e.attendees, !attendees.isEmpty {
|
|
57
|
+
d["attendees"] = attendees.compactMap { att -> [String: Any]? in
|
|
58
|
+
let name = att.name ?? ""
|
|
59
|
+
let email = att.url.absoluteString.replacingOccurrences(of: "mailto:", with: "")
|
|
60
|
+
let status: String
|
|
61
|
+
switch att.participantStatus {
|
|
62
|
+
case .accepted: status = "accepted"
|
|
63
|
+
case .declined: status = "declined"
|
|
64
|
+
case .tentative: status = "tentative"
|
|
65
|
+
case .pending: status = "pending"
|
|
66
|
+
default: status = "unknown"
|
|
67
|
+
}
|
|
68
|
+
let isOrganizer = att.isCurrentUser ? false : (att.participantRole == .chair)
|
|
69
|
+
return ["name": name, "email": email, "status": status, "organizer": isOrganizer]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if let org = e.organizer {
|
|
73
|
+
d["organizer"] = org.name ?? org.url.absoluteString.replacingOccurrences(of: "mailto:", with: "")
|
|
74
|
+
}
|
|
75
|
+
arr.append(d)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if let data = try? JSONSerialization.data(withJSONObject: arr, options: []),
|
|
79
|
+
let str = String(data: data, encoding: .utf8) {
|
|
80
|
+
print(str)
|
|
81
|
+
}
|