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.
Files changed (136) hide show
  1. package/bin/create-walle.js +134 -0
  2. package/package.json +18 -0
  3. package/template/.env.example +40 -0
  4. package/template/CLAUDE.md +12 -0
  5. package/template/LICENSE +21 -0
  6. package/template/README.md +167 -0
  7. package/template/bin/setup.js +100 -0
  8. package/template/claude-code-skill.md +60 -0
  9. package/template/claude-task-manager/api-prompts.js +1841 -0
  10. package/template/claude-task-manager/api-reviews.js +275 -0
  11. package/template/claude-task-manager/approval-agent.js +454 -0
  12. package/template/claude-task-manager/bin/restart-ctm.sh +16 -0
  13. package/template/claude-task-manager/db.js +1721 -0
  14. package/template/claude-task-manager/docs/PROMPT-MANAGEMENT-DESIGN.md +631 -0
  15. package/template/claude-task-manager/git-utils.js +214 -0
  16. package/template/claude-task-manager/package-lock.json +1607 -0
  17. package/template/claude-task-manager/package.json +31 -0
  18. package/template/claude-task-manager/prompt-harvest.js +1148 -0
  19. package/template/claude-task-manager/public/css/prompts.css +880 -0
  20. package/template/claude-task-manager/public/css/reviews.css +430 -0
  21. package/template/claude-task-manager/public/css/walle.css +732 -0
  22. package/template/claude-task-manager/public/favicon.ico +0 -0
  23. package/template/claude-task-manager/public/icon.svg +37 -0
  24. package/template/claude-task-manager/public/index.html +8346 -0
  25. package/template/claude-task-manager/public/js/prompts.js +3159 -0
  26. package/template/claude-task-manager/public/js/reviews.js +1292 -0
  27. package/template/claude-task-manager/public/js/walle.js +3081 -0
  28. package/template/claude-task-manager/public/manifest.json +13 -0
  29. package/template/claude-task-manager/public/prompts.html +4353 -0
  30. package/template/claude-task-manager/public/setup.html +216 -0
  31. package/template/claude-task-manager/queue-engine.js +404 -0
  32. package/template/claude-task-manager/server-state.js +5 -0
  33. package/template/claude-task-manager/server.js +2254 -0
  34. package/template/claude-task-manager/session-utils.js +124 -0
  35. package/template/claude-task-manager/start.sh +17 -0
  36. package/template/claude-task-manager/tests/test-ai-search.js +61 -0
  37. package/template/claude-task-manager/tests/test-editor-ux.js +76 -0
  38. package/template/claude-task-manager/tests/test-editor-ux2.js +51 -0
  39. package/template/claude-task-manager/tests/test-features-v2.js +127 -0
  40. package/template/claude-task-manager/tests/test-insights-cached.js +78 -0
  41. package/template/claude-task-manager/tests/test-insights.js +124 -0
  42. package/template/claude-task-manager/tests/test-permissions-v2.js +127 -0
  43. package/template/claude-task-manager/tests/test-permissions.js +122 -0
  44. package/template/claude-task-manager/tests/test-pin.js +51 -0
  45. package/template/claude-task-manager/tests/test-prompts.js +164 -0
  46. package/template/claude-task-manager/tests/test-recent-sessions.js +96 -0
  47. package/template/claude-task-manager/tests/test-review.js +104 -0
  48. package/template/claude-task-manager/tests/test-send-dropdown.js +76 -0
  49. package/template/claude-task-manager/tests/test-send-final.js +30 -0
  50. package/template/claude-task-manager/tests/test-send-fixes.js +76 -0
  51. package/template/claude-task-manager/tests/test-send-integration.js +107 -0
  52. package/template/claude-task-manager/tests/test-send-visual.js +34 -0
  53. package/template/claude-task-manager/tests/test-session-create.js +147 -0
  54. package/template/claude-task-manager/tests/test-sidebar-ux.js +83 -0
  55. package/template/claude-task-manager/tests/test-url-hash.js +68 -0
  56. package/template/claude-task-manager/tests/test-ux-crop.js +34 -0
  57. package/template/claude-task-manager/tests/test-ux-review.js +130 -0
  58. package/template/claude-task-manager/tests/test-zoom-card.js +76 -0
  59. package/template/claude-task-manager/tests/test-zoom.js +92 -0
  60. package/template/claude-task-manager/tests/test-zoom2.js +67 -0
  61. package/template/docs/site/api/README.md +187 -0
  62. package/template/docs/site/guides/claude-code.md +58 -0
  63. package/template/docs/site/guides/configuration.md +96 -0
  64. package/template/docs/site/guides/quickstart.md +158 -0
  65. package/template/docs/site/index.md +14 -0
  66. package/template/docs/site/skills/README.md +135 -0
  67. package/template/wall-e/.dockerignore +11 -0
  68. package/template/wall-e/Dockerfile +25 -0
  69. package/template/wall-e/adapters/adapter-base.js +37 -0
  70. package/template/wall-e/adapters/ctm.js +193 -0
  71. package/template/wall-e/adapters/slack.js +56 -0
  72. package/template/wall-e/agent.js +319 -0
  73. package/template/wall-e/api-walle.js +1073 -0
  74. package/template/wall-e/brain.js +1235 -0
  75. package/template/wall-e/channels/agent-api.js +172 -0
  76. package/template/wall-e/channels/channel-base.js +14 -0
  77. package/template/wall-e/channels/imessage-channel.js +113 -0
  78. package/template/wall-e/channels/slack-channel.js +118 -0
  79. package/template/wall-e/chat.js +778 -0
  80. package/template/wall-e/decision/confidence.js +93 -0
  81. package/template/wall-e/deploy.sh +35 -0
  82. package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +112 -0
  83. package/template/wall-e/docs/specs/SKILL-FORMAT.md +326 -0
  84. package/template/wall-e/extraction/contradiction.js +168 -0
  85. package/template/wall-e/extraction/knowledge-extractor.js +190 -0
  86. package/template/wall-e/fly.toml +24 -0
  87. package/template/wall-e/loops/ingest.js +34 -0
  88. package/template/wall-e/loops/reflect.js +63 -0
  89. package/template/wall-e/loops/tasks.js +487 -0
  90. package/template/wall-e/loops/think.js +125 -0
  91. package/template/wall-e/package-lock.json +533 -0
  92. package/template/wall-e/package.json +18 -0
  93. package/template/wall-e/scripts/ingest-slack-search.js +85 -0
  94. package/template/wall-e/scripts/pull-slack-via-claude.js +98 -0
  95. package/template/wall-e/scripts/slack-backfill.js +295 -0
  96. package/template/wall-e/scripts/slack-channel-history.js +454 -0
  97. package/template/wall-e/server.js +93 -0
  98. package/template/wall-e/skills/_bundled/email-digest/SKILL.md +95 -0
  99. package/template/wall-e/skills/_bundled/email-sync/SKILL.md +65 -0
  100. package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +104 -0
  101. package/template/wall-e/skills/_bundled/email-sync/run.js +213 -0
  102. package/template/wall-e/skills/_bundled/google-calendar/SKILL.md +73 -0
  103. package/template/wall-e/skills/_bundled/google-calendar/cal-reader.swift +81 -0
  104. package/template/wall-e/skills/_bundled/google-calendar/run.js +181 -0
  105. package/template/wall-e/skills/_bundled/memory-search/SKILL.md +92 -0
  106. package/template/wall-e/skills/_bundled/morning-briefing/SKILL.md +131 -0
  107. package/template/wall-e/skills/_bundled/morning-briefing/run.js +264 -0
  108. package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +60 -0
  109. package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +55 -0
  110. package/template/wall-e/skills/claude-code-reader.js +144 -0
  111. package/template/wall-e/skills/mcp-client.js +407 -0
  112. package/template/wall-e/skills/skill-executor.js +163 -0
  113. package/template/wall-e/skills/skill-loader.js +410 -0
  114. package/template/wall-e/skills/skill-planner.js +88 -0
  115. package/template/wall-e/skills/slack-ingest.js +329 -0
  116. package/template/wall-e/skills/slack-pull-live.js +270 -0
  117. package/template/wall-e/skills/tool-executor.js +188 -0
  118. package/template/wall-e/tests/adapter-base.test.js +20 -0
  119. package/template/wall-e/tests/adapter-ctm.test.js +122 -0
  120. package/template/wall-e/tests/adapter-slack.test.js +98 -0
  121. package/template/wall-e/tests/agent-api.test.js +256 -0
  122. package/template/wall-e/tests/api-walle.test.js +222 -0
  123. package/template/wall-e/tests/brain.test.js +602 -0
  124. package/template/wall-e/tests/channels.test.js +104 -0
  125. package/template/wall-e/tests/chat.test.js +103 -0
  126. package/template/wall-e/tests/confidence.test.js +134 -0
  127. package/template/wall-e/tests/contradiction.test.js +217 -0
  128. package/template/wall-e/tests/ingest.test.js +113 -0
  129. package/template/wall-e/tests/mcp-client.test.js +71 -0
  130. package/template/wall-e/tests/reflect.test.js +103 -0
  131. package/template/wall-e/tests/server.test.js +111 -0
  132. package/template/wall-e/tests/skills.test.js +198 -0
  133. package/template/wall-e/tests/slack-ingest.test.js +103 -0
  134. package/template/wall-e/tests/think.test.js +435 -0
  135. package/template/wall-e/tools/local-tools.js +697 -0
  136. 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
+ }