create-walle 0.9.3 → 0.9.4

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 (75) hide show
  1. package/README.md +2 -1
  2. package/package.json +1 -1
  3. package/template/claude-task-manager/db.js +5 -1
  4. package/template/claude-task-manager/public/css/walle.css +317 -0
  5. package/template/claude-task-manager/public/index.html +404 -101
  6. package/template/claude-task-manager/public/js/walle.js +1256 -86
  7. package/template/claude-task-manager/server.js +189 -14
  8. package/template/docs/site/api/README.md +146 -0
  9. package/template/docs/site/skills/README.md +99 -5
  10. package/template/package.json +1 -1
  11. package/template/wall-e/agent.js +54 -0
  12. package/template/wall-e/api-walle.js +452 -3
  13. package/template/wall-e/brain.js +45 -1
  14. package/template/wall-e/channels/telegram-channel.js +96 -0
  15. package/template/wall-e/chat.js +61 -2
  16. package/template/wall-e/coding-context.js +252 -0
  17. package/template/wall-e/coding-orchestrator.js +625 -0
  18. package/template/wall-e/coding-review.js +189 -0
  19. package/template/wall-e/core-tasks.js +12 -3
  20. package/template/wall-e/deploy.sh +4 -4
  21. package/template/wall-e/fly.toml +2 -2
  22. package/template/wall-e/package.json +4 -1
  23. package/template/wall-e/skills/_bundled/coding-agent/SKILL.md +17 -0
  24. package/template/wall-e/skills/_bundled/coding-agent/run.js +142 -0
  25. package/template/wall-e/skills/_bundled/email-sync/SKILL.md +12 -7
  26. package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +76 -46
  27. package/template/wall-e/skills/_bundled/email-sync/run.js +42 -2
  28. package/template/wall-e/skills/_bundled/glean-team-sync/SKILL.md +57 -0
  29. package/template/wall-e/skills/_bundled/glean-team-sync/run.js +254 -0
  30. package/template/wall-e/skills/_bundled/slack-mentions/SKILL.md +1 -1
  31. package/template/wall-e/skills/_bundled/slack-mentions/run.js +268 -121
  32. package/template/wall-e/skills/_templates/data-fetcher.md +27 -0
  33. package/template/wall-e/skills/_templates/manual-action.md +19 -0
  34. package/template/wall-e/skills/_templates/periodic-checker.md +29 -0
  35. package/template/wall-e/skills/_templates/script-runner.md +21 -0
  36. package/template/wall-e/skills/claude-code-reader.js +16 -4
  37. package/template/wall-e/skills/skill-executor.js +23 -1
  38. package/template/wall-e/skills/skill-validator.js +73 -0
  39. package/template/wall-e/tests/brain.test.js +3 -3
  40. package/template/wall-e/tests/coding-agent-integration.test.js +240 -0
  41. package/template/wall-e/tests/coding-context.test.js +212 -0
  42. package/template/wall-e/tests/coding-orchestrator.test.js +303 -0
  43. package/template/wall-e/tests/coding-review.test.js +141 -0
  44. package/template/claude-task-manager/package-lock.json +0 -1607
  45. package/template/claude-task-manager/tests/test-ai-search.js +0 -61
  46. package/template/claude-task-manager/tests/test-editor-ux.js +0 -76
  47. package/template/claude-task-manager/tests/test-editor-ux2.js +0 -51
  48. package/template/claude-task-manager/tests/test-features-v2.js +0 -127
  49. package/template/claude-task-manager/tests/test-insights-cached.js +0 -78
  50. package/template/claude-task-manager/tests/test-insights.js +0 -124
  51. package/template/claude-task-manager/tests/test-permissions-v2.js +0 -127
  52. package/template/claude-task-manager/tests/test-permissions.js +0 -122
  53. package/template/claude-task-manager/tests/test-pin.js +0 -51
  54. package/template/claude-task-manager/tests/test-prompts.js +0 -164
  55. package/template/claude-task-manager/tests/test-recent-sessions.js +0 -96
  56. package/template/claude-task-manager/tests/test-review.js +0 -104
  57. package/template/claude-task-manager/tests/test-send-dropdown.js +0 -76
  58. package/template/claude-task-manager/tests/test-send-final.js +0 -30
  59. package/template/claude-task-manager/tests/test-send-fixes.js +0 -76
  60. package/template/claude-task-manager/tests/test-send-integration.js +0 -107
  61. package/template/claude-task-manager/tests/test-send-visual.js +0 -34
  62. package/template/claude-task-manager/tests/test-session-create.js +0 -147
  63. package/template/claude-task-manager/tests/test-sidebar-ux.js +0 -83
  64. package/template/claude-task-manager/tests/test-url-hash.js +0 -68
  65. package/template/claude-task-manager/tests/test-ux-crop.js +0 -34
  66. package/template/claude-task-manager/tests/test-ux-review.js +0 -130
  67. package/template/claude-task-manager/tests/test-zoom-card.js +0 -76
  68. package/template/claude-task-manager/tests/test-zoom.js +0 -92
  69. package/template/claude-task-manager/tests/test-zoom2.js +0 -67
  70. package/template/docs/openclaw-vs-walle-comparison.md +0 -103
  71. package/template/docs/ux-improvement-plan.md +0 -84
  72. package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +0 -112
  73. package/template/wall-e/docs/specs/SKILL-FORMAT.md +0 -326
  74. package/template/wall-e/package-lock.json +0 -533
  75. package/template/wall-e/skills/_bundled/slack-mentions/.watermark.json +0 -4
@@ -0,0 +1,189 @@
1
+ 'use strict';
2
+ const { spawn } = require('node:child_process');
3
+
4
+ /**
5
+ * Build a prompt for reviewing a single subtask's diff against its intent.
6
+ * @param {{ title: string, prompt: string }} subtask
7
+ * @param {string} diff
8
+ * @returns {string}
9
+ */
10
+ function buildSubtaskReviewPrompt(subtask, diff) {
11
+ return `You are a code reviewer. Review the following diff for a subtask and check whether it correctly implements the stated intent. Look for bugs, security issues, and missing pieces.
12
+
13
+ ## Subtask Title
14
+ ${subtask.title}
15
+
16
+ ## Subtask Intent
17
+ ${subtask.prompt}
18
+
19
+ ## Diff
20
+ \`\`\`
21
+ ${diff}
22
+ \`\`\`
23
+
24
+ Reply with JSON in exactly this format (no other text outside the JSON):
25
+ {"pass": true/false, "issues": ["description of each issue"], "severity": "minor|major|blocker"}
26
+
27
+ - "pass": true if the diff correctly implements the intent with no significant issues
28
+ - "issues": array of strings describing any problems found (empty array if none)
29
+ - "severity": "minor" for style/nitpicks, "major" for bugs or missing functionality, "blocker" for security issues or fundamentally broken logic`;
30
+ }
31
+
32
+ /**
33
+ * Build a prompt for final review of the entire diff against the original request.
34
+ * @param {string} request - The original user request
35
+ * @param {string} fullDiff - Combined diff of all changes
36
+ * @returns {string}
37
+ */
38
+ function buildFinalReviewPrompt(request, fullDiff) {
39
+ return `You are a senior code reviewer performing a final review of an entire changeset. Check that the diff fully implements the original request. Evaluate:
40
+
41
+ 1. Cross-file consistency - do imports, exports, and interfaces line up?
42
+ 2. Missing pieces - is anything from the request left unimplemented?
43
+ 3. Architectural issues - are there design problems that span multiple files?
44
+ 4. Bugs and security issues
45
+
46
+ ## Original Request
47
+ ${request}
48
+
49
+ ## Full Diff
50
+ \`\`\`
51
+ ${fullDiff}
52
+ \`\`\`
53
+
54
+ Reply with JSON in exactly this format (no other text outside the JSON):
55
+ {"pass": true/false, "issues": ["description of each issue"], "severity": "minor|major|blocker", "summary": "one-line summary of the review"}
56
+
57
+ - "pass": true if the changeset correctly and completely implements the request
58
+ - "issues": array of strings describing any problems found (empty array if none)
59
+ - "severity": "minor" for style/nitpicks, "major" for bugs or missing functionality, "blocker" for security issues or fundamentally broken logic
60
+ - "summary": a brief one-line summary of the overall review`;
61
+ }
62
+
63
+ /**
64
+ * Extract a JSON verdict from Claude's response text.
65
+ * Finds the first JSON object containing a "pass" field.
66
+ * @param {string} text
67
+ * @returns {{ pass: boolean, issues: string[], severity: string, summary: string }}
68
+ */
69
+ function parseVerdict(text) {
70
+ const failResult = {
71
+ pass: false,
72
+ issues: ['Failed to parse review verdict'],
73
+ severity: 'blocker',
74
+ summary: '',
75
+ };
76
+
77
+ if (!text) return failResult;
78
+
79
+ // Match all JSON-like objects in the text
80
+ const jsonPattern = /\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g;
81
+ let match;
82
+ while ((match = jsonPattern.exec(text)) !== null) {
83
+ try {
84
+ const parsed = JSON.parse(match[0]);
85
+ if ('pass' in parsed) {
86
+ return {
87
+ pass: Boolean(parsed.pass),
88
+ issues: Array.isArray(parsed.issues) ? parsed.issues : [],
89
+ severity: parsed.severity || 'minor',
90
+ summary: parsed.summary || '',
91
+ };
92
+ }
93
+ } catch {
94
+ // Not valid JSON, try next match
95
+ }
96
+ }
97
+
98
+ return failResult;
99
+ }
100
+
101
+ /**
102
+ * Spawn `claude` CLI to run a review prompt and return the parsed verdict.
103
+ * Uses spawn with argument array (no shell injection risk).
104
+ * @param {string} prompt
105
+ * @param {{ cwd?: string, timeoutMs?: number, budgetUsd?: number }} opts
106
+ * @returns {Promise<{ pass: boolean, issues: string[], severity: string, summary: string }>}
107
+ */
108
+ function runReview(prompt, opts = {}) {
109
+ const { cwd, timeoutMs = 120_000 } = opts;
110
+
111
+ return new Promise((resolve) => {
112
+ const blockerVerdict = {
113
+ pass: false,
114
+ issues: ['Review process failed'],
115
+ severity: 'blocker',
116
+ summary: '',
117
+ };
118
+
119
+ let proc;
120
+ try {
121
+ const args = ['-p', prompt, '--output-format', 'text', '--max-turns', '1'];
122
+ proc = spawn('claude', args, {
123
+ cwd: cwd || process.cwd(),
124
+ stdio: ['ignore', 'pipe', 'pipe'],
125
+ timeout: timeoutMs,
126
+ });
127
+ } catch (err) {
128
+ blockerVerdict.issues = [`Spawn error: ${err.message}`];
129
+ return resolve(blockerVerdict);
130
+ }
131
+
132
+ const MAX_OUTPUT = 256 * 1024; // 256KB cap
133
+ let stdout = '';
134
+ let stderr = '';
135
+
136
+ proc.stdout.on('data', (chunk) => {
137
+ if (stdout.length < MAX_OUTPUT) stdout += chunk;
138
+ });
139
+ proc.stderr.on('data', (chunk) => {
140
+ if (stderr.length < MAX_OUTPUT) stderr += chunk;
141
+ });
142
+
143
+ proc.on('error', (err) => {
144
+ blockerVerdict.issues = [`Spawn error: ${err.message}`];
145
+ resolve(blockerVerdict);
146
+ });
147
+
148
+ proc.on('close', (code) => {
149
+ if (code !== 0 && !stdout.trim()) {
150
+ blockerVerdict.issues = [`claude exited with code ${code}: ${stderr.trim() || 'no output'}`];
151
+ return resolve(blockerVerdict);
152
+ }
153
+ resolve(parseVerdict(stdout));
154
+ });
155
+ });
156
+ }
157
+
158
+ /**
159
+ * Review a single subtask's diff.
160
+ * @param {{ title: string, prompt: string }} subtask
161
+ * @param {string} diff
162
+ * @param {{ cwd?: string, timeoutMs?: number, budgetUsd?: number }} opts
163
+ * @returns {Promise<{ pass: boolean, issues: string[], severity: string, summary: string }>}
164
+ */
165
+ function reviewSubtask(subtask, diff, opts = {}) {
166
+ const prompt = buildSubtaskReviewPrompt(subtask, diff);
167
+ return runReview(prompt, opts);
168
+ }
169
+
170
+ /**
171
+ * Final review of the entire diff against the original request.
172
+ * @param {string} request
173
+ * @param {string} fullDiff
174
+ * @param {{ cwd?: string, timeoutMs?: number, budgetUsd?: number }} opts
175
+ * @returns {Promise<{ pass: boolean, issues: string[], severity: string, summary: string }>}
176
+ */
177
+ function reviewFinal(request, fullDiff, opts = {}) {
178
+ const prompt = buildFinalReviewPrompt(request, fullDiff);
179
+ return runReview(prompt, opts);
180
+ }
181
+
182
+ module.exports = {
183
+ buildSubtaskReviewPrompt,
184
+ buildFinalReviewPrompt,
185
+ parseVerdict,
186
+ runReview,
187
+ reviewSubtask,
188
+ reviewFinal,
189
+ };
@@ -44,10 +44,11 @@ module.exports = [
44
44
  },
45
45
  {
46
46
  title: 'Email: Full Send History Pull',
47
- description: 'One-time full sync of sent email history from macOS Mail. Run manually to backfill.',
48
- type: 'once',
47
+ description: 'Full sync of sent email history from macOS Mail. First run pulls 10 years; subsequent runs sync incrementally from last email.',
48
+ type: 'recurring',
49
+ schedule: 'every 12h',
49
50
  skill: 'email-sync',
50
- skill_config: JSON.stringify({ days_back: 3650, sync_inbox: true }),
51
+ skill_config: JSON.stringify({ days_back: 3650, sync_inbox: true, incremental: true }),
51
52
  priority: 'normal',
52
53
  },
53
54
  {
@@ -66,6 +67,14 @@ module.exports = [
66
67
  skill: 'slack-mentions',
67
68
  priority: 'normal',
68
69
  },
70
+ {
71
+ title: 'Glean: Team Structure Sync',
72
+ description: 'Sync team structure and contacts from Glean people directory into Wall-E brain.',
73
+ type: 'recurring',
74
+ schedule: 'weekly Monday at 6am',
75
+ skill: 'glean-team-sync',
76
+ priority: 'normal',
77
+ },
69
78
  {
70
79
  title: 'Weekly Reflection',
71
80
  description: 'Generate a weekly reflection analyzing patterns, decisions, and insights from the past 7 days.',
@@ -11,9 +11,9 @@ if ! command -v fly &> /dev/null; then
11
11
  fi
12
12
 
13
13
  # Check if app exists
14
- if ! fly apps list 2>/dev/null | grep -q wall-e-agent; then
14
+ if ! fly apps list 2>/dev/null | grep -q my-wall-e; then
15
15
  echo "Creating Fly.io app..."
16
- fly apps create wall-e-agent
16
+ fly apps create my-wall-e
17
17
  fly volumes create wall_e_data --region sjc --size 1
18
18
  fi
19
19
 
@@ -31,5 +31,5 @@ fly deploy
31
31
 
32
32
  echo ""
33
33
  echo "=== Deployment complete ==="
34
- echo "WALL-E is running at: https://wall-e-agent.fly.dev"
35
- echo "Health check: https://wall-e-agent.fly.dev/api/wall-e/health"
34
+ echo "WALL-E is running at: https://my-wall-e.fly.dev"
35
+ echo "Health check: https://my-wall-e.fly.dev/api/wall-e/health"
@@ -1,5 +1,5 @@
1
- app = "wall-e-agent"
2
- primary_region = "sjc"
1
+ app = "my-wall-e"
2
+ primary_region = "sjc" # Change to your nearest region: https://fly.io/docs/reference/regions/
3
3
 
4
4
  [build]
5
5
 
@@ -9,10 +9,13 @@
9
9
  "test": "node --test tests/*.test.js",
10
10
  "deploy": "./deploy.sh"
11
11
  },
12
- "engines": { "node": ">=18" },
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
13
15
  "dependencies": {
14
16
  "@anthropic-ai/sdk": "^0.78.0",
15
17
  "better-sqlite3": "^12.8.0",
18
+ "grammy": "^1.42.0",
16
19
  "uuid": "^11.1.0"
17
20
  }
18
21
  }
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: coding-agent
3
+ description: Autonomous coding agent — decomposes requests into subtasks, executes via Claude Code, verifies, and delivers
4
+ version: 1.0.0
5
+ execution: script
6
+ entry: run.js
7
+ trigger:
8
+ type: manual
9
+ permissions: [brain:read, brain:write, shell:exec, mcp:call]
10
+ tags: [coding, orchestration, automation]
11
+ ---
12
+
13
+ # Coding Agent
14
+
15
+ Accepts a coding request, breaks it into subtasks, executes each via headless Claude Code sessions, verifies with tests and code review, then commits and reports back.
16
+
17
+ Triggered manually via chat, Slack, API, or task UI.
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Coding Agent Skill — entry point.
5
+ *
6
+ * Receives config via WALL_E_SKILL_CONFIG env var.
7
+ * Resumes from WALL_E_CHECKPOINT if present.
8
+ * Outputs CHECKPOINT: lines for crash-safe resumption.
9
+ * Outputs final report to stdout.
10
+ */
11
+
12
+ const path = require('path');
13
+ const fs = require('fs');
14
+
15
+ async function main() {
16
+ // Parse config
17
+ const configStr = process.env.WALL_E_SKILL_CONFIG;
18
+ if (!configStr) {
19
+ console.error('Missing WALL_E_SKILL_CONFIG');
20
+ process.exit(1);
21
+ }
22
+
23
+ let config;
24
+ try {
25
+ config = JSON.parse(configStr);
26
+ } catch (e) {
27
+ console.error('Invalid WALL_E_SKILL_CONFIG:', e.message);
28
+ process.exit(1);
29
+ }
30
+
31
+ const request = config.request;
32
+ const cwd = config.cwd || process.cwd();
33
+ const options = config.options || {};
34
+
35
+ if (!request) {
36
+ console.error('Missing request in config');
37
+ process.exit(1);
38
+ }
39
+
40
+ // Load orchestrator (resolve relative to wall-e root, not skill dir)
41
+ const walleRoot = path.resolve(__dirname, '..', '..', '..');
42
+ const orchestrator = require(path.join(walleRoot, 'coding-orchestrator.js'));
43
+
44
+ // Load brain if available
45
+ let brain;
46
+ try {
47
+ brain = require(path.join(walleRoot, 'brain.js'));
48
+ } catch {}
49
+
50
+ // Check for checkpoint (resume case)
51
+ let resumeState = null;
52
+ const checkpoint = process.env.WALL_E_CHECKPOINT;
53
+ if (checkpoint && fs.existsSync(checkpoint)) {
54
+ try {
55
+ resumeState = orchestrator.readCheckpoint(checkpoint);
56
+ console.log(`Resuming from checkpoint: ${resumeState.completed.length} subtasks completed`);
57
+ } catch (e) {
58
+ console.error('Failed to read checkpoint:', e.message);
59
+ }
60
+ }
61
+
62
+ // Progress callback — logs to stdout
63
+ function onProgress(event) {
64
+ switch (event.type) {
65
+ case 'subtask_start':
66
+ console.log(`\n--- Subtask ${event.subtask.id}: ${event.subtask.title} ---`);
67
+ break;
68
+ case 'subtask_done':
69
+ console.log(` [OK] Subtask ${event.subtask.id} completed`);
70
+ break;
71
+ case 'subtask_retry':
72
+ console.log(` [RETRY ${event.attempt}] ${(event.error || '').slice(0, 200)}`);
73
+ break;
74
+ case 'review_concern':
75
+ console.log(` [CONCERN] ${(event.issues || []).join('; ')}`);
76
+ break;
77
+ case 'escalate':
78
+ console.log(` [ESCALATE] Stuck on subtask ${event.subtask.id}: ${(event.error || '').slice(0, 200)}`);
79
+ break;
80
+ case 'budget_exceeded':
81
+ console.log(` [BUDGET] Exceeded: ~$${event.spent.toFixed(2)}`);
82
+ break;
83
+ }
84
+ }
85
+
86
+ try {
87
+ // Plan (skip if resuming with existing plan)
88
+ let planData;
89
+ if (resumeState && resumeState.plan) {
90
+ console.log('Using plan from checkpoint');
91
+ const mergedConfig = { ...orchestrator.getDefaultConfig(), ...options };
92
+ const { assembleContext } = require(path.join(walleRoot, 'coding-context.js'));
93
+ const context = await assembleContext(request, cwd, { brain });
94
+ planData = { plan: resumeState.plan, context, config: mergedConfig };
95
+ } else {
96
+ console.log('Planning...');
97
+ planData = await orchestrator.plan(request, cwd, { ...options, brain });
98
+ console.log(`Plan: ${planData.plan.subtasks.length} subtasks, branch: ${planData.plan.branch_name}`);
99
+
100
+ // Guardrail check
101
+ if (planData.plan.subtasks.length > planData.config.guardrail_threshold) {
102
+ console.log(`\n[GUARDRAIL] Plan has ${planData.plan.subtasks.length} subtasks (threshold: ${planData.config.guardrail_threshold})`);
103
+ console.log('Subtasks:');
104
+ planData.plan.subtasks.forEach(s => console.log(` ${s.id}. ${s.title}`));
105
+ console.log('\nPausing for approval. Re-run to continue.');
106
+ const statePath = path.join(cwd, '.wall-e-coding-state.json');
107
+ orchestrator.writeCheckpoint(statePath, {
108
+ completed: [], plan: planData.plan,
109
+ cumulative_context: '', branch: planData.plan.branch_name, session_ids: {},
110
+ });
111
+ console.log(`CHECKPOINT: ${statePath}`);
112
+ return;
113
+ }
114
+ }
115
+
116
+ // Execute
117
+ console.log('\nExecuting...');
118
+ const executeResult = await orchestrator.execute(planData, {
119
+ cwd,
120
+ onProgress,
121
+ startFrom: resumeState,
122
+ });
123
+
124
+ if (!executeResult.success) {
125
+ console.error(`\nExecution failed: ${executeResult.error}`);
126
+ process.exit(1);
127
+ }
128
+
129
+ // Complete
130
+ console.log('\nCompleting...');
131
+ const report = await orchestrator.complete(request, planData, executeResult, {
132
+ cwd, brain, onProgress,
133
+ });
134
+
135
+ console.log('\n' + orchestrator.formatReport(report));
136
+ } catch (e) {
137
+ console.error(`Coding agent error: ${e.message}`);
138
+ process.exit(1);
139
+ }
140
+ }
141
+
142
+ main();
@@ -5,7 +5,7 @@ description: >
5
5
  the owner's communication style. Always syncs all sent mail with body
6
6
  content. Inbox sync is optional and filters to only emails addressed
7
7
  directly to the owner (on To/Cc line). Uses JXA to read Mail.app.
8
- version: 1.1.0
8
+ version: 1.2.0
9
9
  author: wall-e
10
10
  execution: script
11
11
  entry: run.js
@@ -16,11 +16,15 @@ config:
16
16
  days_back:
17
17
  type: number
18
18
  default: 3
19
- description: "How many days back to sync"
19
+ description: "How many days back to sync (used as fallback when incremental has no prior data)"
20
20
  sync_inbox:
21
21
  type: boolean
22
22
  default: false
23
23
  description: "Also sync inbox emails addressed directly to owner"
24
+ incremental:
25
+ type: boolean
26
+ default: false
27
+ description: "Query brain for latest email and only fetch since then (days_back used as fallback for first run)"
24
28
  tags: [email, gmail, mail, sent, sync, learning]
25
29
  permissions:
26
30
  - mail:read
@@ -36,11 +40,12 @@ Optionally syncs inbox emails that are addressed directly to the owner.
36
40
 
37
41
  ## How It Works
38
42
 
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>`
43
+ 1. **Incremental mode** (`incremental: true`): queries brain for the latest email timestamp, computes `days_back` dynamically (+1 day overlap). Falls back to configured `days_back` on first run (no prior data).
44
+ 2. Run JXA script via `osascript` to read sent messages from all accounts
45
+ 3. Sent mail always includes body content (first 2000 chars) for learning
46
+ 4. Inbox is opt-in (`--sync-inbox`), filtered to emails where owner is on To/Cc
47
+ 5. Dedup by `source_id` = `mail:{messageId}` skip unchanged, update modified
48
+ 6. Store as memories with `source: 'email'`, `source_channel: <account name>`
44
49
 
45
50
  ## Memory Format
46
51
 
@@ -17,6 +17,24 @@ function run(argv) {
17
17
  var cutoff = new Date();
18
18
  cutoff.setDate(cutoff.getDate() - daysBack);
19
19
 
20
+ // For large date ranges, chunk into 90-day windows to avoid AppleEvent timeout (-1712).
21
+ // The .whose() query is a single Apple Event and Mail can't respond in time for huge ranges.
22
+ var CHUNK_DAYS = 90;
23
+ var chunks = [];
24
+ if (daysBack > CHUNK_DAYS) {
25
+ var now = new Date();
26
+ var chunkEnd = new Date(now);
27
+ while (chunkEnd > cutoff) {
28
+ var chunkStart = new Date(chunkEnd);
29
+ chunkStart.setDate(chunkStart.getDate() - CHUNK_DAYS);
30
+ if (chunkStart < cutoff) chunkStart = cutoff;
31
+ chunks.push({ start: chunkStart, end: chunkEnd });
32
+ chunkEnd = new Date(chunkStart);
33
+ }
34
+ } else {
35
+ chunks.push({ start: cutoff, end: new Date() });
36
+ }
37
+
20
38
  var results = { accounts: [], inbox: [], sent: [] };
21
39
  var accounts = mail.accounts();
22
40
 
@@ -33,27 +51,33 @@ function run(argv) {
33
51
  if (readInbox) {
34
52
  try {
35
53
  var inbox = acct.mailboxes.byName('INBOX');
36
- var inboxMsgs = inbox.messages.whose({ dateReceived: { _greaterThan: cutoff } })();
37
- for (var m = 0; m < inboxMsgs.length; m++) {
54
+ for (var ci = 0; ci < chunks.length; ci++) {
38
55
  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) {}
56
+ var inboxMsgs = inbox.messages.whose({
57
+ _and: [{ dateReceived: { _greaterThan: chunks[ci].start } }, { dateReceived: { _lessThan: chunks[ci].end } }]
58
+ })();
59
+ for (var m = 0; m < inboxMsgs.length; m++) {
60
+ try {
61
+ var msg = inboxMsgs[m];
62
+ var toRecips = [];
63
+ try { toRecips = msg.toRecipients().map(function(r) { return { name: r.name(), email: r.address() }; }); } catch(e) {}
64
+ var ccRecips = [];
65
+ try { ccRecips = msg.ccRecipients().map(function(r) { return { name: r.name(), email: r.address() }; }); } catch(e) {}
44
66
 
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 */ }
67
+ results.inbox.push({
68
+ messageId: msg.messageId(),
69
+ subject: msg.subject() || '(no subject)',
70
+ sender: msg.sender(),
71
+ to: toRecips,
72
+ cc: ccRecips,
73
+ date: msg.dateReceived().toISOString(),
74
+ account: acctName,
75
+ mailbox: 'INBOX',
76
+ readStatus: msg.readStatus()
77
+ });
78
+ } catch(e) { /* skip individual message errors */ }
79
+ }
80
+ } catch(e) { /* chunk query failed, continue */ }
57
81
  }
58
82
  } catch(e) { /* account may not have INBOX */ }
59
83
  }
@@ -63,37 +87,43 @@ function run(argv) {
63
87
  for (var s = 0; s < sentNames.length; s++) {
64
88
  try {
65
89
  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++) {
90
+ for (var ci = 0; ci < chunks.length; ci++) {
68
91
  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) {}
92
+ var sentMsgs = sentBox.messages.whose({
93
+ _and: [{ dateSent: { _greaterThan: chunks[ci].start } }, { dateSent: { _lessThan: chunks[ci].end } }]
94
+ })();
95
+ for (var m = 0; m < sentMsgs.length; m++) {
96
+ try {
97
+ var msg = sentMsgs[m];
98
+ var toRecips = [];
99
+ try { toRecips = msg.toRecipients().map(function(r) { return { name: r.name(), email: r.address() }; }); } catch(e) {}
100
+ var ccRecips = [];
101
+ try { ccRecips = msg.ccRecipients().map(function(r) { return { name: r.name(), email: r.address() }; }); } catch(e) {}
74
102
 
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
- };
103
+ var entry = {
104
+ messageId: msg.messageId(),
105
+ subject: msg.subject() || '(no subject)',
106
+ sender: msg.sender(),
107
+ to: toRecips,
108
+ cc: ccRecips,
109
+ date: msg.dateSent().toISOString(),
110
+ account: acctName,
111
+ mailbox: sentNames[s],
112
+ readStatus: true
113
+ };
86
114
 
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 */ }
115
+ // Always fetch content for sent messages
116
+ try {
117
+ var body = msg.content();
118
+ if (body && body.length > 0) {
119
+ entry.content = body.substring(0, 2000);
120
+ }
121
+ } catch(e) { /* content extraction failed */ }
94
122
 
95
- results.sent.push(entry);
96
- } catch(e) { /* skip individual message errors */ }
123
+ results.sent.push(entry);
124
+ } catch(e) { /* skip individual message errors */ }
125
+ }
126
+ } catch(e) { /* chunk query failed, continue */ }
97
127
  }
98
128
  break; // found the sent mailbox for this account
99
129
  } catch(e) { /* try next sent mailbox name */ }