create-walle 0.1.0 → 0.2.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 (29) hide show
  1. package/bin/create-walle.js +276 -61
  2. package/package.json +1 -1
  3. package/template/CHANGELOG.md +44 -0
  4. package/template/bin/github-polish.sh +18 -0
  5. package/template/bin/install-service.sh +72 -0
  6. package/template/claude-task-manager/public/css/walle.css +9 -8
  7. package/template/claude-task-manager/public/index.html +1 -0
  8. package/template/claude-task-manager/public/setup.html +38 -1
  9. package/template/claude-task-manager/server.js +59 -0
  10. package/template/docs/site/astro.config.mjs +28 -0
  11. package/template/docs/site/package.json +19 -0
  12. package/template/docs/site/src/content/docs/api.md +189 -0
  13. package/template/docs/site/src/content/docs/guides/claude-code.md +62 -0
  14. package/template/docs/site/src/content/docs/guides/configuration.md +100 -0
  15. package/template/docs/site/src/content/docs/guides/quickstart.md +162 -0
  16. package/template/docs/site/src/content/docs/index.mdx +32 -0
  17. package/template/docs/site/src/content/docs/skills.md +137 -0
  18. package/template/docs/site/src/content.config.ts +7 -0
  19. package/template/package.json +11 -0
  20. package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +2 -2
  21. package/template/wall-e/docs/specs/SKILL-FORMAT.md +1 -1
  22. package/template/wall-e/skills/_bundled/email-digest/SKILL.md +1 -1
  23. package/template/wall-e/skills/_bundled/email-sync/SKILL.md +1 -1
  24. package/template/wall-e/skills/_bundled/google-calendar/SKILL.md +1 -1
  25. package/template/wall-e/skills/_bundled/memory-search/SKILL.md +1 -1
  26. package/template/wall-e/skills/_bundled/morning-briefing/SKILL.md +1 -1
  27. package/template/wall-e/skills/_bundled/morning-briefing/run.js +110 -56
  28. package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +1 -1
  29. package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +1 -1
@@ -1,134 +1,349 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- const { execFileSync } = require('child_process');
4
+ const { execFileSync, execFile, spawn } = require('child_process');
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
 
8
8
  const BOLD = '\x1b[1m';
9
9
  const DIM = '\x1b[2m';
10
10
  const GREEN = '\x1b[32m';
11
+ const RED = '\x1b[31m';
11
12
  const CYAN = '\x1b[36m';
12
13
  const RESET = '\x1b[0m';
13
14
 
14
- // Template directory bundled inside this npm package
15
15
  const TEMPLATE_DIR = path.join(__dirname, '..', 'template');
16
+ const LABEL = 'com.walle.server';
16
17
 
17
- function detectName() {
18
- try { return execFileSync('git', ['config', 'user.name'], { encoding: 'utf8' }).trim(); } catch {}
19
- try { return execFileSync('id', ['-F'], { encoding: 'utf8' }).trim(); } catch {} // macOS full name
20
- return process.env.USER || 'Owner';
21
- }
18
+ // ── CLI Router ──
22
19
 
23
- function detectTimezone() {
24
- try { return Intl.DateTimeFormat().resolvedOptions().timeZone; } catch {}
25
- return 'UTC';
20
+ const command = process.argv[2] || '';
21
+
22
+ if (command === 'install') {
23
+ install(process.argv[3]);
24
+ } else if (command === 'start') {
25
+ start();
26
+ } else if (command === 'stop') {
27
+ stop();
28
+ } else if (command === 'status') {
29
+ status();
30
+ } else if (command === 'logs') {
31
+ logs();
32
+ } else if (command === 'uninstall') {
33
+ uninstall();
34
+ } else if (command === '--help' || command === '-h' || command === 'help') {
35
+ usage();
36
+ } else if (command && !command.startsWith('-')) {
37
+ // Backwards compat: bare directory name = install
38
+ install(command);
39
+ } else {
40
+ usage();
26
41
  }
27
42
 
28
- function copyRecursive(src, dest) {
29
- const stat = fs.statSync(src);
30
- if (stat.isDirectory()) {
31
- fs.mkdirSync(dest, { recursive: true });
32
- for (const entry of fs.readdirSync(src)) {
33
- if (entry === 'node_modules' || entry === '.git') continue;
34
- copyRecursive(path.join(src, entry), path.join(dest, entry));
35
- }
36
- } else {
37
- fs.copyFileSync(src, dest);
38
- }
43
+ // ── Commands ──
44
+
45
+ function usage() {
46
+ console.log(`
47
+ ${BOLD}${CYAN} Wall-E${RESET} Your personal digital twin
48
+
49
+ ${BOLD}Usage:${RESET}
50
+
51
+ ${BOLD}npx create-walle install <directory>${RESET} Install Wall-E and start the service
52
+ ${BOLD}npx create-walle start${RESET} Start Wall-E (auto-restarts on reboot)
53
+ ${BOLD}npx create-walle stop${RESET} Stop Wall-E
54
+ ${BOLD}npx create-walle status${RESET} Check if Wall-E is running
55
+ ${BOLD}npx create-walle logs${RESET} Tail the service logs
56
+ ${BOLD}npx create-walle uninstall${RESET} Remove the auto-start service
57
+
58
+ ${DIM}Run 'start' and 'stop' from inside your Wall-E directory,
59
+ or set WALLE_DIR to point to it.${RESET}
60
+ `);
39
61
  }
40
62
 
41
- async function main() {
42
- const targetDir = process.argv[2] || 'walle';
63
+ function install(targetDir) {
64
+ if (!targetDir) {
65
+ console.log(`\n ${RED}Missing directory name.${RESET} Usage: npx create-walle install ./my-agent\n`);
66
+ process.exit(1);
67
+ }
43
68
 
44
- console.log(`\n${BOLD}${CYAN} Wall-E${RESET} — Your personal digital twin\n`);
69
+ console.log(`\n${BOLD}${CYAN} Wall-E${RESET} — Installing...\n`);
45
70
 
46
71
  if (fs.existsSync(targetDir)) {
47
- console.log(` Directory "${targetDir}" already exists. Choose a different name:\n npx create-walle my-agent\n`);
72
+ console.log(` ${RED}Directory "${targetDir}" already exists.${RESET}\n`);
48
73
  process.exit(1);
49
74
  }
50
-
51
75
  if (!fs.existsSync(TEMPLATE_DIR)) {
52
- console.error(' Template directory not found. Try: npm cache clean --force && npx create-walle@latest');
76
+ console.error(' Template not found. Try: npm cache clean --force && npx create-walle@latest');
53
77
  process.exit(1);
54
78
  }
55
79
 
56
- // Auto-detect owner info (strip chars that would break .env)
80
+ // Auto-detect
57
81
  const ownerName = detectName().replace(/[\r\n=]/g, '').trim().slice(0, 200);
58
82
  const timezone = detectTimezone();
59
83
  const nameParts = ownerName.split(/\s+/);
84
+ const port = process.env.WALLE_PORT || '4567';
60
85
 
61
86
  console.log(` ${DIM}Owner: ${ownerName}${RESET}`);
62
87
  console.log(` ${DIM}Timezone: ${timezone}${RESET}`);
88
+ console.log(` ${DIM}Port: ${port}${RESET}`);
63
89
 
64
90
  // Copy template
65
91
  console.log(`\n Copying files...`);
66
92
  copyRecursive(TEMPLATE_DIR, targetDir);
67
93
 
68
- // Install dependencies
94
+ // Install deps
69
95
  console.log(` Installing dependencies...\n`);
70
96
  try {
71
97
  execFileSync('npm', ['install', '--loglevel=warn'], { cwd: path.join(targetDir, 'claude-task-manager'), stdio: 'inherit' });
72
98
  execFileSync('npm', ['install', '--loglevel=warn'], { cwd: path.join(targetDir, 'wall-e'), stdio: 'inherit' });
73
- } catch (e) {
74
- console.error(`\n Failed to install dependencies. Make sure npm is installed and try:\n cd ${targetDir}/claude-task-manager && npm install\n cd ${targetDir}/wall-e && npm install\n`);
99
+ } catch {
100
+ console.error(`\n ${RED}npm install failed.${RESET} Try manually:\n cd ${targetDir}/claude-task-manager && npm install\n cd ${targetDir}/wall-e && npm install\n`);
75
101
  process.exit(1);
76
102
  }
77
103
 
78
- // Create .env (API key left as placeholder — configured via web UI)
104
+ // .env
79
105
  const envLines = [
80
106
  `# Wall-E configuration — generated ${new Date().toISOString().split('T')[0]}`,
81
- `# Add your API key below, or configure it at http://localhost:3456/setup`,
107
+ `# Finish setup at http://localhost:${port}/setup.html`,
82
108
  '',
83
109
  `WALLE_OWNER_NAME=${ownerName}`,
84
110
  '# ANTHROPIC_API_KEY=sk-ant-...',
85
111
  '',
86
- '# Slack (connect via http://localhost:3456/#walle → Settings)',
112
+ `CTM_PORT=${port}`,
113
+ '',
87
114
  '# SLACK_TOKEN=',
88
115
  '# SLACK_OWNER_USER_ID=',
89
116
  '# SLACK_OWNER_HANDLE=',
90
117
  ];
91
118
  fs.writeFileSync(path.join(targetDir, '.env'), envLines.join('\n') + '\n', { mode: 0o600 });
92
119
 
93
- // Create wall-e-config.json
94
- const walleConfig = {
95
- owner: {
96
- name: ownerName,
97
- first_name: nameParts[0],
98
- last_name: nameParts.slice(1).join(' '),
99
- timezone,
100
- },
101
- adapters: { ctm: { enabled: true }, slack: { enabled: false } },
102
- intervals: { ingest_ms: 60000, think_ms: 120000 },
103
- };
120
+ // wall-e-config.json
104
121
  fs.writeFileSync(
105
122
  path.join(targetDir, 'wall-e', 'wall-e-config.json'),
106
- JSON.stringify(walleConfig, null, 2) + '\n',
123
+ JSON.stringify({
124
+ owner: { name: ownerName, first_name: nameParts[0], last_name: nameParts.slice(1).join(' '), timezone },
125
+ adapters: { ctm: { enabled: true }, slack: { enabled: false } },
126
+ intervals: { ingest_ms: 60000, think_ms: 120000 },
127
+ }, null, 2) + '\n',
107
128
  { mode: 0o600 }
108
129
  );
109
130
 
110
- // Create data directory
111
- const dataDir = path.join(process.env.HOME, '.walle', 'data');
112
- fs.mkdirSync(dataDir, { recursive: true });
131
+ // Data dir
132
+ fs.mkdirSync(path.join(process.env.HOME, '.walle', 'data'), { recursive: true });
113
133
 
114
- // Init git repo
115
- execFileSync('git', ['init', '-q'], { cwd: targetDir, stdio: 'ignore' });
134
+ // Git init
135
+ try { execFileSync('git', ['init', '-q'], { cwd: targetDir, stdio: 'ignore' }); } catch {}
136
+
137
+ // Save install path for start/stop
138
+ saveWalleDir(path.resolve(targetDir));
139
+
140
+ // Install and start the service
141
+ console.log(`\n Starting Wall-E service...`);
142
+ installService(path.resolve(targetDir), port);
116
143
 
117
144
  console.log(`
118
- ${GREEN}${BOLD}Done!${RESET}
145
+ ${GREEN}${BOLD}Wall-E is running!${RESET}
119
146
 
120
- ${BOLD}1. Start:${RESET}
121
- cd ${targetDir} && node claude-task-manager/server.js
147
+ ${BOLD}Open:${RESET} http://localhost:${port}
122
148
 
123
- ${BOLD}2. Open:${RESET}
124
- http://localhost:3456
149
+ ${BOLD}Manage:${RESET}
150
+ npx create-walle stop ${DIM}Stop the service${RESET}
151
+ npx create-walle start ${DIM}Start / restart${RESET}
152
+ npx create-walle status ${DIM}Check status${RESET}
153
+ npx create-walle logs ${DIM}View logs${RESET}
125
154
 
126
- ${BOLD}3. Finish setup in the browser:${RESET}
127
- Add your Anthropic API key and connect integrations.
155
+ ${BOLD}Change port:${RESET}
156
+ Edit ${targetDir}/.env CTM_PORT=8080
157
+ npx create-walle start
128
158
  `);
129
159
  }
130
160
 
131
- main().catch(err => {
132
- console.error(` Error: ${err.message}`);
161
+ function start() {
162
+ const dir = findWalleDir();
163
+ const port = readPort(dir);
164
+
165
+ console.log(`\n Starting Wall-E from ${DIM}${dir}${RESET} on port ${port}...`);
166
+ installService(dir, port);
167
+ console.log(` ${GREEN}Running!${RESET} http://localhost:${port}\n`);
168
+ }
169
+
170
+ function stop() {
171
+ const plist = path.join(process.env.HOME, 'Library', 'LaunchAgents', `${LABEL}.plist`);
172
+ if (process.platform === 'darwin' && fs.existsSync(plist)) {
173
+ try { execFileSync('launchctl', ['unload', plist], { stdio: 'ignore' }); } catch {}
174
+ console.log(`\n ${GREEN}Wall-E stopped.${RESET}`);
175
+ console.log(` Run ${BOLD}npx create-walle start${RESET} to restart.\n`);
176
+ } else {
177
+ // Fallback: try killing the process
178
+ try {
179
+ const dir = findWalleDir();
180
+ const port = readPort(dir);
181
+ execFileSync('curl', ['-sX', 'POST', `http://localhost:${port}/api/stop/walle`], { timeout: 3000 });
182
+ } catch {}
183
+ console.log(`\n Stopped (or was not running).\n`);
184
+ }
185
+ }
186
+
187
+ function status() {
188
+ const dir = findWalleDir();
189
+ const port = readPort(dir);
190
+ try {
191
+ const result = execFileSync('curl', ['-s', `http://localhost:${port}/api/services/status`], { encoding: 'utf8', timeout: 3000 });
192
+ const data = JSON.parse(result);
193
+ console.log(`\n ${GREEN}${BOLD}Wall-E is running${RESET} on port ${port}`);
194
+ console.log(` ${DIM}CTM: PID ${data.ctm?.pid || '?'}, uptime ${Math.round((data.ctm?.uptime || 0) / 60)}m${RESET}`);
195
+ console.log(` ${DIM}Wall-E: PID ${data.walle?.pid || '?'}${RESET}`);
196
+ console.log(` ${DIM}Dir: ${dir}${RESET}\n`);
197
+ } catch {
198
+ console.log(`\n ${RED}Wall-E is not running${RESET} (port ${port})`);
199
+ console.log(` Run ${BOLD}npx create-walle start${RESET} to start.\n`);
200
+ }
201
+ }
202
+
203
+ function logs() {
204
+ const logFile = path.join(process.env.HOME, '.walle', 'logs', 'walle.log');
205
+ if (!fs.existsSync(logFile)) {
206
+ console.log(`\n No logs yet at ${logFile}\n`);
207
+ return;
208
+ }
209
+ const child = spawn('tail', ['-f', logFile], { stdio: 'inherit' });
210
+ child.on('error', () => console.log(` Could not tail ${logFile}`));
211
+ }
212
+
213
+ function uninstall() {
214
+ const plist = path.join(process.env.HOME, 'Library', 'LaunchAgents', `${LABEL}.plist`);
215
+ try { execFileSync('launchctl', ['unload', plist], { stdio: 'ignore' }); } catch {}
216
+ try { fs.unlinkSync(plist); } catch {}
217
+ try { fs.unlinkSync(path.join(process.env.HOME, '.walle', 'install-path')); } catch {}
218
+ console.log(`\n ${GREEN}Service uninstalled.${RESET} Your data in ~/.walle/data is preserved.\n`);
219
+ }
220
+
221
+ // ── Service Management ──
222
+
223
+ function installService(walleDir, port) {
224
+ const logDir = path.join(process.env.HOME, '.walle', 'logs');
225
+ fs.mkdirSync(logDir, { recursive: true });
226
+
227
+ if (process.platform === 'darwin') {
228
+ const nodePath = process.execPath;
229
+ const plistDir = path.join(process.env.HOME, 'Library', 'LaunchAgents');
230
+ fs.mkdirSync(plistDir, { recursive: true });
231
+ const plistPath = path.join(plistDir, `${LABEL}.plist`);
232
+
233
+ // Build env vars from .env file
234
+ let envDict = ` <key>PATH</key>\n <string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>\n`;
235
+ envDict += ` <key>HOME</key>\n <string>${process.env.HOME}</string>\n`;
236
+ envDict += ` <key>CTM_PORT</key>\n <string>${port}</string>\n`;
237
+ try {
238
+ const envFile = fs.readFileSync(path.join(walleDir, '.env'), 'utf8');
239
+ for (const line of envFile.split('\n')) {
240
+ const m = line.match(/^\s*([^#=\s]+)\s*=\s*(.+?)\s*$/);
241
+ if (m && m[1] !== 'CTM_PORT' && m[1] !== 'PATH' && m[1] !== 'HOME') {
242
+ envDict += ` <key>${m[1]}</key>\n <string>${m[2]}</string>\n`;
243
+ }
244
+ }
245
+ } catch {}
246
+
247
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
248
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
249
+ <plist version="1.0">
250
+ <dict>
251
+ <key>Label</key>
252
+ <string>${LABEL}</string>
253
+ <key>ProgramArguments</key>
254
+ <array>
255
+ <string>${nodePath}</string>
256
+ <string>${walleDir}/claude-task-manager/server.js</string>
257
+ </array>
258
+ <key>WorkingDirectory</key>
259
+ <string>${walleDir}</string>
260
+ <key>EnvironmentVariables</key>
261
+ <dict>
262
+ ${envDict} </dict>
263
+ <key>RunAtLoad</key>
264
+ <true/>
265
+ <key>KeepAlive</key>
266
+ <true/>
267
+ <key>StandardOutPath</key>
268
+ <string>${logDir}/walle.log</string>
269
+ <key>StandardErrorPath</key>
270
+ <string>${logDir}/walle.err</string>
271
+ </dict>
272
+ </plist>`;
273
+
274
+ fs.writeFileSync(plistPath, plist);
275
+ try { execFileSync('launchctl', ['unload', plistPath], { stdio: 'ignore' }); } catch {}
276
+ execFileSync('launchctl', ['load', plistPath]);
277
+ } else {
278
+ // Linux/other: just start directly in background
279
+ const child = spawn('node', ['claude-task-manager/server.js'], {
280
+ cwd: walleDir,
281
+ detached: true,
282
+ stdio: ['ignore', fs.openSync(path.join(logDir, 'walle.log'), 'a'), fs.openSync(path.join(logDir, 'walle.err'), 'a')],
283
+ env: { ...process.env, CTM_PORT: port },
284
+ });
285
+ child.unref();
286
+ fs.writeFileSync(path.join(logDir, 'walle.pid'), String(child.pid));
287
+ }
288
+ }
289
+
290
+ // ── Helpers ──
291
+
292
+ function saveWalleDir(dir) {
293
+ const metaDir = path.join(process.env.HOME, '.walle');
294
+ fs.mkdirSync(metaDir, { recursive: true });
295
+ fs.writeFileSync(path.join(metaDir, 'install-path'), dir + '\n');
296
+ }
297
+
298
+ function findWalleDir() {
299
+ // 1. WALLE_DIR env
300
+ if (process.env.WALLE_DIR && fs.existsSync(path.join(process.env.WALLE_DIR, 'claude-task-manager'))) {
301
+ return process.env.WALLE_DIR;
302
+ }
303
+ // 2. Current directory
304
+ if (fs.existsSync(path.join(process.cwd(), 'claude-task-manager', 'server.js'))) {
305
+ return process.cwd();
306
+ }
307
+ // 3. Saved install path
308
+ const saved = path.join(process.env.HOME, '.walle', 'install-path');
309
+ if (fs.existsSync(saved)) {
310
+ const dir = fs.readFileSync(saved, 'utf8').trim();
311
+ if (fs.existsSync(path.join(dir, 'claude-task-manager', 'server.js'))) return dir;
312
+ }
313
+ console.error(`\n ${RED}Can't find Wall-E installation.${RESET}`);
314
+ console.error(` Run from inside the Wall-E directory, or set WALLE_DIR.\n`);
133
315
  process.exit(1);
134
- });
316
+ }
317
+
318
+ function readPort(dir) {
319
+ try {
320
+ const env = fs.readFileSync(path.join(dir, '.env'), 'utf8');
321
+ const m = env.match(/^CTM_PORT=(\d+)/m);
322
+ if (m) return m[1];
323
+ } catch {}
324
+ return '4567';
325
+ }
326
+
327
+ function detectName() {
328
+ try { return execFileSync('git', ['config', 'user.name'], { encoding: 'utf8' }).trim(); } catch {}
329
+ try { return execFileSync('id', ['-F'], { encoding: 'utf8' }).trim(); } catch {}
330
+ return process.env.USER || 'Owner';
331
+ }
332
+
333
+ function detectTimezone() {
334
+ try { return Intl.DateTimeFormat().resolvedOptions().timeZone; } catch {}
335
+ return 'UTC';
336
+ }
337
+
338
+ function copyRecursive(src, dest) {
339
+ const stat = fs.statSync(src);
340
+ if (stat.isDirectory()) {
341
+ fs.mkdirSync(dest, { recursive: true });
342
+ for (const entry of fs.readdirSync(src)) {
343
+ if (entry === 'node_modules' || entry === '.git') continue;
344
+ copyRecursive(path.join(src, entry), path.join(dest, entry));
345
+ }
346
+ } else {
347
+ fs.copyFileSync(src, dest);
348
+ }
349
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-walle",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Set up Wall-E — your personal digital twin",
5
5
  "bin": {
6
6
  "create-walle": "bin/create-walle.js"
@@ -0,0 +1,44 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1 (2026-04-02)
4
+
5
+ ### Fixed
6
+ - Remove personal name from SKILL.md author fields and spec docs
7
+ - Deprecate npm 0.1.0 (contained personal data in author fields)
8
+
9
+ ## 0.1.0 (2026-04-02)
10
+
11
+ ### Added
12
+ - **Browser-based setup wizard** — auto-detects owner name from `git config` and timezone from system; first visit redirects to `/setup.html` for API key and Slack OAuth
13
+ - **`npx create-walle` scaffolder** — zero-prompt install with bundled template
14
+ - **Documentation site** — quickstart, configuration, Claude Code integration, skill catalog, API reference
15
+ - **README.md** — architecture diagram, quickstart, config table, API overview
16
+ - **Claude Code skill registry** — `claude-code-skill.md` for MCP server integration
17
+ - **Security hardening** — input validation on setup save, .env injection protection, body size limits, API key format checks, cached `needsSetup()`
18
+
19
+ ### Changed
20
+ - Replaced all hardcoded personal data with env vars (`SLACK_OWNER_USER_ID`, `SLACK_OWNER_HANDLE`, `WALLE_OWNER_NAME`)
21
+ - Default data directory changed from `~/Dropbox/...` to `~/.walle/data`
22
+ - Genericized test data (personal names replaced with `Test Owner`/`TestUser`)
23
+ - Git history scrubbed of sensitive strings via `filter-branch`
24
+
25
+ ## Pre-release (2026-01 to 2026-04)
26
+
27
+ ### Wall-E Agent
28
+ - **Phase 5** — Cloud deployment (Dockerfile, Fly.io, HTTP server)
29
+ - **Phase 4** — iMessage channel, Slack DM, agent-to-agent protocol
30
+ - **Phase 3** — Chat interface with tool-use, confidence scoring, actions UI
31
+ - **Phase 2** — Contradiction detection, reflect loop, knowledge extraction
32
+ - **Phase 1** — Brain DB (SQLite), adapters, ingest + think loops
33
+ - **Slack integration** — 20,933 messages ingested (2022-2026), backfill + incremental sync
34
+ - **Skills system** — 7 bundled skills (calendar, Slack, email, morning briefing, memory search)
35
+ - **MCP client** — Call Slack, SendGrid, and any MCP server from skills
36
+ - **Chat persistence** — Conversation history stored in brain DB
37
+
38
+ ### Claude Task Manager (CTM)
39
+ - Terminal multiplexer for Claude Code sessions
40
+ - Rich-text prompt editor with versioning, folders, composite prompts
41
+ - Code review module with diff viewer and inline comments
42
+ - AI-powered session search and analysis
43
+ - Permission manager with natural language rules
44
+ - Task dashboard with recurring tasks and live logs
@@ -0,0 +1,18 @@
1
+ #!/bin/bash
2
+ # Run this after authenticating gh as the ShanniLi account:
3
+ # gh auth login
4
+ # Or manually set these in GitHub Settings > General
5
+
6
+ gh repo edit ShanniLi/tools \
7
+ --description "Wall-E — your personal digital twin. AI agent that learns from Slack, email & calendar. Includes CTM dashboard." \
8
+ --homepage "https://walle.sh" \
9
+ --add-topic "ai" \
10
+ --add-topic "digital-twin" \
11
+ --add-topic "claude" \
12
+ --add-topic "personal-assistant" \
13
+ --add-topic "slack" \
14
+ --add-topic "sqlite" \
15
+ --add-topic "nodejs"
16
+
17
+ echo "Done. Set social preview image manually at:"
18
+ echo " https://github.com/ShanniLi/tools/settings"
@@ -0,0 +1,72 @@
1
+ #!/bin/bash
2
+ # Install Wall-E as a macOS Launch Agent so it starts automatically on login.
3
+ # Usage: bash bin/install-service.sh [--uninstall]
4
+
5
+ set -e
6
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
7
+ ROOT="$(dirname "$SCRIPT_DIR")"
8
+ LABEL="com.walle.server"
9
+ PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
10
+ PORT="${CTM_PORT:-4567}"
11
+
12
+ if [[ "$1" == "--uninstall" ]]; then
13
+ launchctl unload "$PLIST" 2>/dev/null || true
14
+ rm -f "$PLIST"
15
+ echo "Wall-E service uninstalled."
16
+ exit 0
17
+ fi
18
+
19
+ # Source .env to get the port
20
+ if [[ -f "$ROOT/.env" ]]; then
21
+ export $(grep -v '^#' "$ROOT/.env" | grep '=' | xargs)
22
+ PORT="${CTM_PORT:-4567}"
23
+ fi
24
+
25
+ NODE=$(which node)
26
+ LOG_DIR="$HOME/.walle/logs"
27
+ mkdir -p "$LOG_DIR"
28
+
29
+ cat > "$PLIST" << EOF
30
+ <?xml version="1.0" encoding="UTF-8"?>
31
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
32
+ <plist version="1.0">
33
+ <dict>
34
+ <key>Label</key>
35
+ <string>$LABEL</string>
36
+ <key>ProgramArguments</key>
37
+ <array>
38
+ <string>$NODE</string>
39
+ <string>$ROOT/claude-task-manager/server.js</string>
40
+ </array>
41
+ <key>WorkingDirectory</key>
42
+ <string>$ROOT</string>
43
+ <key>EnvironmentVariables</key>
44
+ <dict>
45
+ <key>PATH</key>
46
+ <string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
47
+ <key>HOME</key>
48
+ <string>$HOME</string>
49
+ <key>CTM_PORT</key>
50
+ <string>$PORT</string>
51
+ </dict>
52
+ <key>RunAtLoad</key>
53
+ <true/>
54
+ <key>KeepAlive</key>
55
+ <true/>
56
+ <key>StandardOutPath</key>
57
+ <string>$LOG_DIR/walle.log</string>
58
+ <key>StandardErrorPath</key>
59
+ <string>$LOG_DIR/walle.err</string>
60
+ </dict>
61
+ </plist>
62
+ EOF
63
+
64
+ launchctl unload "$PLIST" 2>/dev/null || true
65
+ launchctl load "$PLIST"
66
+
67
+ echo "Wall-E installed as a macOS service."
68
+ echo ""
69
+ echo " Status: launchctl list | grep walle"
70
+ echo " Logs: tail -f ~/.walle/logs/walle.log"
71
+ echo " Dashboard: http://localhost:$PORT"
72
+ echo " Uninstall: bash bin/install-service.sh --uninstall"
@@ -408,23 +408,24 @@
408
408
 
409
409
  /* Markdown rendered output */
410
410
  .we-task-output-md {
411
- padding: 10px 14px;
411
+ padding: 12px 16px;
412
412
  overflow-y: auto;
413
- max-height: 500px;
413
+ max-height: 600px;
414
414
  }
415
415
  .we-task-md {
416
416
  font-size: 13px; line-height: 1.6; color: #ccc;
417
417
  }
418
418
  .we-task-md h1, .we-task-md h2, .we-task-md h3 {
419
- color: #e2e8f0; margin: 12px 0 6px; font-weight: 600;
419
+ color: #e2e8f0; font-weight: 600;
420
420
  }
421
- .we-task-md h1 { font-size: 16px; border-bottom: 1px solid rgba(255,255,255,0.08); padding-bottom: 4px; }
422
- .we-task-md h2 { font-size: 14px; }
423
- .we-task-md h3 { font-size: 13px; color: #94a3b8; }
421
+ .we-task-md h1 { font-size: 17px; border-bottom: 1px solid rgba(255,255,255,0.08); padding-bottom: 6px; margin: 16px 0 10px; }
422
+ .we-task-md h2 { font-size: 14px; margin: 18px 0 8px; color: #93c5fd; }
423
+ .we-task-md h3 { font-size: 13px; color: #94a3b8; margin: 12px 0 6px; }
424
424
  .we-task-md p { margin: 6px 0; }
425
425
  .we-task-md ul, .we-task-md ol { margin: 6px 0; padding-left: 20px; }
426
- .we-task-md li { margin: 3px 0; }
427
- .we-task-md li::marker { color: #555; }
426
+ .we-task-md li { margin: 4px 0; line-height: 1.55; }
427
+ .we-task-md li::marker { color: #444; }
428
+ .we-task-md li strong { color: #93c5fd; }
428
429
  .we-task-md code {
429
430
  font-family: 'SF Mono', 'Menlo', 'Monaco', monospace;
430
431
  font-size: 12px; background: rgba(255,255,255,0.06);
@@ -2239,6 +2239,7 @@
2239
2239
  <button class="nav-pill" data-nav="codereview" onclick="navTo('codereview')" title="Code Review">Review</button>
2240
2240
  <button class="nav-pill" data-nav="walle" onclick="navTo('walle')" title="WALL-E Agent">WALL-E</button>
2241
2241
  <button class="nav-pill" data-nav="rules" onclick="navTo('rules')" title="Edit CLAUDE.md rules">Rules</button>
2242
+ <a href="/setup.html" class="nav-pill" title="Setup & Settings" style="text-decoration:none;margin-left:auto;font-size:16px;padding:4px 8px;">⚙</a>
2242
2243
  </nav>
2243
2244
  <div class="topbar-right">
2244
2245
  <span id="topbar-context-btns"></span>
@@ -74,6 +74,7 @@
74
74
  </div>
75
75
  <div class="btn-row">
76
76
  <button class="btn btn-primary" id="save-btn" onclick="saveConfig()">Save & Continue</button>
77
+ <button class="btn btn-secondary" id="detect-btn" onclick="detectKey()" title="Check your shell environment for an existing API key">Detect from environment</button>
77
78
  <span class="success-msg" id="save-ok">Saved!</span>
78
79
  <span class="error-msg" id="save-err"></span>
79
80
  </div>
@@ -120,7 +121,8 @@
120
121
 
121
122
  <!-- Done -->
122
123
  <div class="done-section">
123
- <a href="/" id="done-link">Go to Dashboard →</a>
124
+ <a href="/index.html" id="done-link">Go to Dashboard →</a>
125
+ <div style="margin-top:8px;font-size:12px;color:var(--dim)">You can return to this page anytime from the settings icon in the nav bar.</div>
124
126
  </div>
125
127
  </div>
126
128
 
@@ -210,6 +212,41 @@
210
212
  }
211
213
  }
212
214
 
215
+ async function detectKey() {
216
+ const btn = document.getElementById('detect-btn');
217
+ const errMsg = document.getElementById('save-err');
218
+ const okMsg = document.getElementById('save-ok');
219
+ errMsg.style.display = 'none';
220
+ okMsg.style.display = 'none';
221
+ btn.disabled = true;
222
+ btn.textContent = 'Checking...';
223
+ try {
224
+ const r = await fetch('/api/setup/detect-key');
225
+ const d = await r.json();
226
+ if (d.found) {
227
+ document.getElementById('api-key').value = '';
228
+ document.getElementById('api-key').placeholder = '••••••••••••••• (from ' + (d.source || 'environment') + ')';
229
+ document.getElementById('api-dot').className = 'status-dot ok';
230
+ okMsg.textContent = 'API key detected from ' + (d.source || 'environment') + '!';
231
+ okMsg.style.display = 'inline';
232
+ const ownerVal = document.getElementById('owner-name').value.trim();
233
+ await fetch('/api/setup/save', {
234
+ method: 'POST',
235
+ headers: { 'Content-Type': 'application/json' },
236
+ body: JSON.stringify({ owner_name: ownerVal, api_key: d.key }),
237
+ });
238
+ } else {
239
+ errMsg.textContent = d.hint || 'No API key found. Enter one manually.';
240
+ errMsg.style.display = 'inline';
241
+ }
242
+ } catch (e) {
243
+ errMsg.textContent = e.message;
244
+ errMsg.style.display = 'inline';
245
+ }
246
+ btn.textContent = 'Detect from environment';
247
+ btn.disabled = false;
248
+ }
249
+
213
250
  loadStatus();
214
251
  </script>
215
252
  </body>