create-walle 0.4.6 → 0.6.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/README.md +78 -0
- package/bin/create-walle.js +31 -9
- package/package.json +4 -3
- package/template/claude-task-manager/api-prompts.js +12 -1
- package/template/claude-task-manager/db.js +20 -8
- package/template/claude-task-manager/public/css/walle.css +2 -0
- package/template/claude-task-manager/public/index.html +166 -4
- package/template/claude-task-manager/public/js/prompts.js +2 -1
- package/template/claude-task-manager/public/js/walle.js +77 -1
- package/template/claude-task-manager/public/setup.html +294 -106
- package/template/claude-task-manager/server.js +257 -23
- package/template/package.json +2 -2
- package/template/wall-e/api-walle.js +33 -0
- package/template/wall-e/brain.js +27 -9
- package/template/wall-e/chat.js +2 -2
- package/template/wall-e/extraction/contradiction.js +1 -1
- package/template/wall-e/extraction/knowledge-extractor.js +1 -1
- package/template/wall-e/skills/skill-executor.js +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# create-walle
|
|
2
|
+
|
|
3
|
+
Set up **Wall-E** — your personal digital twin. An AI agent that learns from your Slack, email, and calendar to build a searchable second brain.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx create-walle install ./my-agent
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This copies the project, installs dependencies, auto-detects your name and timezone, and starts the server. Open **http://localhost:3456** to finish setup in the browser.
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx create-walle install <dir> # Install Wall-E (or update if already installed)
|
|
17
|
+
npx create-walle update # Update to latest version, keep your config
|
|
18
|
+
npx create-walle start # Start as background service (auto-restarts on reboot)
|
|
19
|
+
npx create-walle stop # Stop the service
|
|
20
|
+
npx create-walle status # Check if Wall-E is running
|
|
21
|
+
npx create-walle logs # Tail the service logs
|
|
22
|
+
npx create-walle uninstall # Remove the auto-start service
|
|
23
|
+
npx create-walle -v # Show version
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Setup
|
|
27
|
+
|
|
28
|
+
On first launch, the browser setup page guides you through:
|
|
29
|
+
|
|
30
|
+
1. **Owner name** — auto-detected from `git config`
|
|
31
|
+
2. **API key** — enter manually, or click "Auto-detect" to find it from:
|
|
32
|
+
- Shell environment (`ANTHROPIC_API_KEY`)
|
|
33
|
+
- Claude Code OAuth token (macOS Keychain)
|
|
34
|
+
- Corporate devbox gateway (`~/.devbox/secrets/claude/auth_headers`)
|
|
35
|
+
3. **Integrations** — connect Slack (OAuth), email and calendar auto-detected on macOS
|
|
36
|
+
|
|
37
|
+
## API Authentication
|
|
38
|
+
|
|
39
|
+
Wall-E supports three ways to connect to Claude:
|
|
40
|
+
|
|
41
|
+
**Direct API key:**
|
|
42
|
+
```env
|
|
43
|
+
ANTHROPIC_API_KEY=sk-ant-...
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Portkey / API gateway:**
|
|
47
|
+
```env
|
|
48
|
+
ANTHROPIC_BASE_URL=https://your-gateway.example.com/v1
|
|
49
|
+
ANTHROPIC_AUTH_TOKEN=your-key
|
|
50
|
+
ANTHROPIC_CUSTOM_HEADERS_B64=<base64-encoded headers>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Corporate devbox:** Auto-detected — no config needed if you use `devbox ai -c claude`.
|
|
54
|
+
|
|
55
|
+
## Custom Port
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
CTM_PORT=5000 npx create-walle install ./my-agent
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Wall-E API runs on `CTM_PORT + 1` (e.g., 5001).
|
|
62
|
+
|
|
63
|
+
## What's Included
|
|
64
|
+
|
|
65
|
+
- **CTM Dashboard** — terminal multiplexer, prompt editor, task manager, code review
|
|
66
|
+
- **Wall-E Agent** — ingest/think/reflect loops, 7 bundled skills, chat with tool-use
|
|
67
|
+
- **Brain DB** — SQLite with full-text search across Slack, email, calendar
|
|
68
|
+
- **Multi-device** — share data via Dropbox/iCloud, sessions tagged by hostname
|
|
69
|
+
|
|
70
|
+
## Links
|
|
71
|
+
|
|
72
|
+
- [Full Documentation](https://walle.sh)
|
|
73
|
+
- [Configuration Reference](https://walle.sh/docs/guides/configuration/)
|
|
74
|
+
- [Skill Catalog](https://walle.sh/docs/skills/)
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
MIT
|
package/bin/create-walle.js
CHANGED
|
@@ -151,6 +151,9 @@ function install(targetDir) {
|
|
|
151
151
|
fs.mkdirSync(path.join(process.env.HOME, '.walle', 'data'), { recursive: true });
|
|
152
152
|
try { execFileSync('git', ['init', '-q'], { cwd: targetDir, stdio: 'ignore' }); } catch {}
|
|
153
153
|
|
|
154
|
+
// Stamp version into root package.json so the settings page shows it
|
|
155
|
+
stampVersion(targetDir);
|
|
156
|
+
|
|
154
157
|
saveWalleDir(path.resolve(targetDir));
|
|
155
158
|
|
|
156
159
|
// Start the service
|
|
@@ -208,7 +211,10 @@ function update() {
|
|
|
208
211
|
fs.writeFileSync(fullPath, content);
|
|
209
212
|
}
|
|
210
213
|
|
|
211
|
-
// 5.
|
|
214
|
+
// 5. Stamp version
|
|
215
|
+
stampVersion(dir);
|
|
216
|
+
|
|
217
|
+
// 6. Reinstall deps (in case package.json changed)
|
|
212
218
|
console.log(` Installing dependencies...\n`);
|
|
213
219
|
npmInstall(dir);
|
|
214
220
|
|
|
@@ -276,19 +282,24 @@ function uninstall() {
|
|
|
276
282
|
// ── Service Management ──
|
|
277
283
|
|
|
278
284
|
function stopQuiet(dir, port) {
|
|
279
|
-
// Try launchctl first
|
|
285
|
+
// Try launchctl first (macOS)
|
|
280
286
|
const plist = path.join(process.env.HOME, 'Library', 'LaunchAgents', `${LABEL}.plist`);
|
|
281
287
|
if (process.platform === 'darwin' && fs.existsSync(plist)) {
|
|
282
288
|
try { execFileSync('launchctl', ['unload', plist], { stdio: 'ignore' }); } catch {}
|
|
283
289
|
}
|
|
284
|
-
//
|
|
285
|
-
try {
|
|
286
|
-
|
|
287
|
-
|
|
290
|
+
// Kill process on the port directly
|
|
291
|
+
try {
|
|
292
|
+
const pids = execFileSync('lsof', ['-ti', ':' + port], { encoding: 'utf8', timeout: 3000 }).trim().split('\n').filter(Boolean);
|
|
293
|
+
for (const pid of pids) { try { process.kill(parseInt(pid), 'SIGTERM'); } catch {} }
|
|
294
|
+
} catch {}
|
|
295
|
+
// Brief wait for port to free
|
|
296
|
+
const deadline = Date.now() + 5000;
|
|
297
|
+
while (Date.now() < deadline) {
|
|
288
298
|
try {
|
|
289
|
-
execFileSync('curl', ['-s', '--max-time', '1', `http://localhost:${port}/api/services/status`], { timeout: 2000 });
|
|
290
|
-
// Still running
|
|
291
|
-
|
|
299
|
+
execFileSync('curl', ['-s', '--max-time', '1', `http://localhost:${port}/api/services/status`], { encoding: 'utf8', timeout: 2000 });
|
|
300
|
+
// Still running — busy-wait briefly
|
|
301
|
+
const waitUntil = Date.now() + 500;
|
|
302
|
+
while (Date.now() < waitUntil) { /* spin */ }
|
|
292
303
|
} catch {
|
|
293
304
|
break; // Port is free
|
|
294
305
|
}
|
|
@@ -392,6 +403,17 @@ function npmInstall(dir) {
|
|
|
392
403
|
}
|
|
393
404
|
}
|
|
394
405
|
|
|
406
|
+
function stampVersion(dir) {
|
|
407
|
+
try {
|
|
408
|
+
const ver = require('../package.json').version;
|
|
409
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
410
|
+
let pkg = {};
|
|
411
|
+
try { pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); } catch {}
|
|
412
|
+
pkg.version = ver;
|
|
413
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
414
|
+
} catch {}
|
|
415
|
+
}
|
|
416
|
+
|
|
395
417
|
function saveWalleDir(dir) {
|
|
396
418
|
const metaDir = path.join(process.env.HOME, '.walle');
|
|
397
419
|
fs.mkdirSync(metaDir, { recursive: true });
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-walle",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Wall-E — your personal digital twin. AI agent that learns from Slack, email & calendar. Includes dashboard, chat, and 7 bundled skills.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-walle": "bin/create-walle.js"
|
|
7
7
|
},
|
|
8
8
|
"files": [
|
|
9
9
|
"bin/",
|
|
10
|
-
"template/"
|
|
10
|
+
"template/",
|
|
11
|
+
"README.md"
|
|
11
12
|
],
|
|
12
13
|
"scripts": {
|
|
13
14
|
"build": "bash build.sh",
|
|
@@ -628,6 +628,13 @@ function handleListConversations(req, res, url) {
|
|
|
628
628
|
if (url.searchParams.has('search')) opts.search = url.searchParams.get('search');
|
|
629
629
|
if (url.searchParams.has('limit')) opts.limit = parseInt(url.searchParams.get('limit'));
|
|
630
630
|
if (url.searchParams.has('offset')) opts.offset = parseInt(url.searchParams.get('offset'));
|
|
631
|
+
// Filter by current device unless all_devices=1
|
|
632
|
+
if (url.searchParams.get('all_devices') === '1') {
|
|
633
|
+
opts.allDevices = true;
|
|
634
|
+
} else {
|
|
635
|
+
if (!handleListConversations._hostname) handleListConversations._hostname = require('os').hostname();
|
|
636
|
+
opts.hostname = handleListConversations._hostname;
|
|
637
|
+
}
|
|
631
638
|
jsonResponse(res, 200, db.listSessionConversations(opts));
|
|
632
639
|
}
|
|
633
640
|
|
|
@@ -781,7 +788,11 @@ async function handleScreenshot(req, res) {
|
|
|
781
788
|
|
|
782
789
|
// --- Backups ---
|
|
783
790
|
function handleListBackups(req, res) {
|
|
784
|
-
|
|
791
|
+
const backups = db.listBackups();
|
|
792
|
+
const dbPath = db.getDbPath();
|
|
793
|
+
let dbSize = 0;
|
|
794
|
+
try { dbSize = require('fs').statSync(dbPath).size; } catch {}
|
|
795
|
+
jsonResponse(res, 200, { backups, db_path: dbPath, backup_dir: require('path').join(require('path').dirname(dbPath), 'backups'), db_size: dbSize });
|
|
785
796
|
}
|
|
786
797
|
|
|
787
798
|
async function handleCreateBackup(req, res) {
|
|
@@ -588,6 +588,12 @@ function runMigrations() {
|
|
|
588
588
|
if (!hsCols.find(c => c.name === 'last_lifecycle_refresh_at')) {
|
|
589
589
|
getDb().exec("ALTER TABLE harvest_state ADD COLUMN last_lifecycle_refresh_at TEXT");
|
|
590
590
|
}
|
|
591
|
+
|
|
592
|
+
// Migration: add hostname to session_conversations for multi-device support
|
|
593
|
+
const scCols2 = getDb().prepare("PRAGMA table_info(session_conversations)").all();
|
|
594
|
+
if (!scCols2.find(c => c.name === 'hostname')) {
|
|
595
|
+
getDb().prepare("ALTER TABLE session_conversations ADD COLUMN hostname TEXT DEFAULT ''").run();
|
|
596
|
+
}
|
|
591
597
|
}
|
|
592
598
|
|
|
593
599
|
// --- Settings CRUD ---
|
|
@@ -1083,25 +1089,31 @@ function setAlwaysAsk(toolName, alwaysAsk) {
|
|
|
1083
1089
|
}
|
|
1084
1090
|
|
|
1085
1091
|
// --- Session Conversations ---
|
|
1086
|
-
function importSessionConversation({ session_id, project_path, messages, user_msg_count, assistant_msg_count, title, first_message, git_branch, file_size, session_created_at }) {
|
|
1092
|
+
function importSessionConversation({ session_id, project_path, messages, user_msg_count, assistant_msg_count, title, first_message, git_branch, file_size, session_created_at, hostname }) {
|
|
1087
1093
|
getDb().prepare(
|
|
1088
|
-
`INSERT INTO session_conversations (session_id, project_path, messages, user_msg_count, assistant_msg_count, title, first_message, git_branch, file_size, session_created_at)
|
|
1089
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1094
|
+
`INSERT INTO session_conversations (session_id, project_path, messages, user_msg_count, assistant_msg_count, title, first_message, git_branch, file_size, session_created_at, hostname)
|
|
1095
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1090
1096
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
1091
1097
|
messages=excluded.messages, user_msg_count=excluded.user_msg_count,
|
|
1092
1098
|
assistant_msg_count=excluded.assistant_msg_count, title=excluded.title,
|
|
1093
1099
|
first_message=excluded.first_message, git_branch=excluded.git_branch,
|
|
1094
|
-
file_size=excluded.file_size,
|
|
1100
|
+
file_size=excluded.file_size, hostname=COALESCE(NULLIF(excluded.hostname,''), session_conversations.hostname),
|
|
1101
|
+
imported_at=datetime('now')`
|
|
1095
1102
|
).run(
|
|
1096
1103
|
session_id, project_path || '', JSON.stringify(messages || []),
|
|
1097
1104
|
user_msg_count || 0, assistant_msg_count || 0, title || '',
|
|
1098
|
-
first_message || '', git_branch || '', file_size || 0, session_created_at || ''
|
|
1105
|
+
first_message || '', git_branch || '', file_size || 0, session_created_at || '',
|
|
1106
|
+
hostname || require('os').hostname()
|
|
1099
1107
|
);
|
|
1100
1108
|
}
|
|
1101
1109
|
|
|
1102
|
-
function listSessionConversations({ search, limit, offset } = {}) {
|
|
1103
|
-
let sql = 'SELECT id, session_id, project_path, user_msg_count, assistant_msg_count, title, first_message, git_branch, file_size, session_created_at, imported_at FROM session_conversations WHERE 1=1';
|
|
1110
|
+
function listSessionConversations({ search, limit, offset, hostname, allDevices } = {}) {
|
|
1111
|
+
let sql = 'SELECT id, session_id, project_path, user_msg_count, assistant_msg_count, title, first_message, git_branch, file_size, session_created_at, imported_at, hostname FROM session_conversations WHERE 1=1';
|
|
1104
1112
|
const params = [];
|
|
1113
|
+
if (!allDevices && hostname) {
|
|
1114
|
+
sql += ' AND (hostname = ? OR hostname = \'\' OR hostname IS NULL)';
|
|
1115
|
+
params.push(hostname);
|
|
1116
|
+
}
|
|
1105
1117
|
if (search) {
|
|
1106
1118
|
sql += ' AND (title LIKE ? OR first_message LIKE ? OR project_path LIKE ?)';
|
|
1107
1119
|
const q = `%${search}%`;
|
|
@@ -1280,7 +1292,7 @@ function deleteBackup(backupName) {
|
|
|
1280
1292
|
if (fs.existsSync(imagesPath)) fs.unlinkSync(imagesPath);
|
|
1281
1293
|
}
|
|
1282
1294
|
|
|
1283
|
-
function cleanOldBackups(maxAge =
|
|
1295
|
+
function cleanOldBackups(maxAge = 7) {
|
|
1284
1296
|
// Keep backups for maxAge days, but always keep at least 5
|
|
1285
1297
|
const backups = listBackups();
|
|
1286
1298
|
if (backups.length <= 5) return;
|
|
@@ -82,6 +82,8 @@
|
|
|
82
82
|
}
|
|
83
83
|
.walle-btn:hover { background: rgba(255,255,255,0.08); }
|
|
84
84
|
.walle-btn.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
85
|
+
.walle-btn.danger { color: #f85149; border-color: #f8514933; }
|
|
86
|
+
.walle-btn.danger:hover { background: #f8514918; border-color: #f85149; }
|
|
85
87
|
|
|
86
88
|
/* Loading / Empty */
|
|
87
89
|
.walle-loading { text-align: center; padding: 40px; color: var(--fg-muted, #888); font-size: 13px; }
|
|
@@ -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
|
+
<button class="nav-pill" data-nav="backups" onclick="navTo('backups')" title="Database backups">Backups</button>
|
|
2242
2243
|
<a href="/setup.html" class="nav-pill" title="Setup & Settings" style="text-decoration:none;margin-left:auto;font-size:16px;padding:4px 8px;">⚙</a>
|
|
2243
2244
|
</nav>
|
|
2244
2245
|
<div class="topbar-right">
|
|
@@ -2272,6 +2273,7 @@
|
|
|
2272
2273
|
<div class="display-toggle">
|
|
2273
2274
|
<label><input type="checkbox" id="hide-ctm" checked onchange="savePref('hide_ctm', this.checked); renderFilteredSessions()"> Hide CTM</label>
|
|
2274
2275
|
<label><input type="checkbox" id="show-titles" checked onchange="savePref('show_titles', this.checked); renderFilteredSessions()"> AI titles</label>
|
|
2276
|
+
<label title="Only show sessions from this computer"><input type="checkbox" id="this-device" checked onchange="savePref('this_device', this.checked); loadRecentSessions()"> This device</label>
|
|
2275
2277
|
<div class="view-mode-toggle">
|
|
2276
2278
|
<button class="active" id="view-list-btn" onclick="setViewMode('list')" title="List view">List</button>
|
|
2277
2279
|
<button id="view-group-btn" onclick="setViewMode('groups')" title="Group by topic">Groups</button>
|
|
@@ -2387,6 +2389,13 @@
|
|
|
2387
2389
|
</div>
|
|
2388
2390
|
<div id="walle-body" class="walle-body"></div>
|
|
2389
2391
|
</div>
|
|
2392
|
+
<div id="backups-panel" style="display:none;flex-direction:column;height:100%;overflow:hidden;">
|
|
2393
|
+
<div style="padding:16px 20px;border-bottom:1px solid var(--border);background:var(--bg);">
|
|
2394
|
+
<h2 style="font-size:16px;font-weight:600;margin-bottom:8px;">Backup Manager</h2>
|
|
2395
|
+
<p style="font-size:12px;color:var(--fg-dim);margin:0;">Auto-backups daily (7 day retention, minimum 3 kept). Click paths to copy.</p>
|
|
2396
|
+
</div>
|
|
2397
|
+
<div id="backups-body" style="flex:1;overflow-y:auto;padding:16px 20px;"></div>
|
|
2398
|
+
</div>
|
|
2390
2399
|
<div id="rules-panel">
|
|
2391
2400
|
<div id="rules-sidebar">
|
|
2392
2401
|
<div class="rules-sidebar-header">
|
|
@@ -3290,7 +3299,7 @@ function createTerminal(id) {
|
|
|
3290
3299
|
}
|
|
3291
3300
|
|
|
3292
3301
|
function activateTab(id) {
|
|
3293
|
-
const specialPanels = ['rules', 'insights', 'permissions', 'prompts', 'codereview', 'walle'];
|
|
3302
|
+
const specialPanels = ['rules', 'insights', 'permissions', 'prompts', 'codereview', 'walle', 'backups'];
|
|
3294
3303
|
const isPanel = specialPanels.includes(id);
|
|
3295
3304
|
|
|
3296
3305
|
// Hide all
|
|
@@ -3305,6 +3314,8 @@ function activateTab(id) {
|
|
|
3305
3314
|
document.getElementById('permissions-panel').classList.remove('active');
|
|
3306
3315
|
document.getElementById('prompts-panel').classList.remove('active');
|
|
3307
3316
|
document.getElementById('walle-panel').classList.remove('active');
|
|
3317
|
+
var bkp = document.getElementById('backups-panel');
|
|
3318
|
+
if (bkp) { bkp.classList.remove('active'); bkp.style.display = 'none'; }
|
|
3308
3319
|
|
|
3309
3320
|
state.activeTab = id;
|
|
3310
3321
|
|
|
@@ -3407,7 +3418,7 @@ function activateTab(id) {
|
|
|
3407
3418
|
|
|
3408
3419
|
function syncNavPills(activeId) {
|
|
3409
3420
|
const navPills = document.querySelectorAll('#topbar-nav .nav-pill');
|
|
3410
|
-
const specialPanels = ['rules', 'insights', 'permissions', 'prompts', 'codereview', 'walle'];
|
|
3421
|
+
const specialPanels = ['rules', 'insights', 'permissions', 'prompts', 'codereview', 'walle', 'backups'];
|
|
3411
3422
|
const navId = specialPanels.includes(activeId) ? activeId : 'sessions';
|
|
3412
3423
|
navPills.forEach(pill => {
|
|
3413
3424
|
pill.classList.toggle('active', pill.dataset.nav === navId);
|
|
@@ -3479,6 +3490,8 @@ function navTo(target, opts) {
|
|
|
3479
3490
|
document.getElementById('review-panel').classList.remove('active');
|
|
3480
3491
|
document.getElementById('codereview-panel').classList.remove('active');
|
|
3481
3492
|
document.getElementById('walle-panel').classList.remove('active');
|
|
3493
|
+
var bkPanel = document.getElementById('backups-panel');
|
|
3494
|
+
if (bkPanel) bkPanel.classList.remove('active');
|
|
3482
3495
|
// Restore sidebar & tabbar
|
|
3483
3496
|
const sidebar = document.getElementById('sidebar');
|
|
3484
3497
|
if (!state.sidebarManuallyHidden) {
|
|
@@ -3503,6 +3516,8 @@ function navTo(target, opts) {
|
|
|
3503
3516
|
showCodeReviewPanel();
|
|
3504
3517
|
} else if (target === 'walle') {
|
|
3505
3518
|
showWallePanel();
|
|
3519
|
+
} else if (target === 'backups') {
|
|
3520
|
+
showBackupsPanel();
|
|
3506
3521
|
}
|
|
3507
3522
|
}
|
|
3508
3523
|
|
|
@@ -3526,6 +3541,150 @@ function showWallePanel() {
|
|
|
3526
3541
|
if (window.WE) WE.init();
|
|
3527
3542
|
}
|
|
3528
3543
|
|
|
3544
|
+
function showBackupsPanel() {
|
|
3545
|
+
document.querySelectorAll('#terminal-area > div').forEach(function(d) { d.style.display = 'none'; });
|
|
3546
|
+
var panel = document.getElementById('backups-panel');
|
|
3547
|
+
panel.style.display = 'flex';
|
|
3548
|
+
panel.classList.add('active');
|
|
3549
|
+
syncNavPills('backups');
|
|
3550
|
+
updateTopbarContext('backups');
|
|
3551
|
+
document.getElementById('sidebar').classList.add('collapsed');
|
|
3552
|
+
document.getElementById('sidebar-resize').style.display = 'none';
|
|
3553
|
+
document.getElementById('tabbar').style.display = 'none';
|
|
3554
|
+
loadBackupsData();
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
function loadBackupsData() {
|
|
3558
|
+
var body = document.getElementById('backups-body');
|
|
3559
|
+
body.textContent = 'Loading...';
|
|
3560
|
+
var token = window._ctmState ? window._ctmState.token : '';
|
|
3561
|
+
Promise.all([
|
|
3562
|
+
fetch('/api/backups?token=' + token).then(function(r) { return r.json(); }),
|
|
3563
|
+
fetch('/api/wall-e/backups?token=' + token).then(function(r) { return r.json(); })
|
|
3564
|
+
]).then(function(results) {
|
|
3565
|
+
var ctm = results[0] || {};
|
|
3566
|
+
var brain = (results[1] && results[1].data) ? results[1].data : {};
|
|
3567
|
+
_renderBackupsBody(body, ctm, brain);
|
|
3568
|
+
}).catch(function(e) { body.textContent = 'Failed: ' + e.message; });
|
|
3569
|
+
}
|
|
3570
|
+
|
|
3571
|
+
function _renderBackupsBody(body, ctm, brain) {
|
|
3572
|
+
while (body.firstChild) body.removeChild(body.firstChild);
|
|
3573
|
+
|
|
3574
|
+
// DB info cards
|
|
3575
|
+
var grid = document.createElement('div');
|
|
3576
|
+
grid.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:20px;';
|
|
3577
|
+
grid.appendChild(_makeDbCard('CTM Database', ctm.db_path, ctm.db_size, ctm.backup_dir));
|
|
3578
|
+
grid.appendChild(_makeDbCard('Wall-E Brain', brain.db_path, brain.db_size, brain.backup_dir));
|
|
3579
|
+
body.appendChild(grid);
|
|
3580
|
+
|
|
3581
|
+
// Buttons
|
|
3582
|
+
var btns = document.createElement('div');
|
|
3583
|
+
btns.style.cssText = 'display:flex;gap:8px;margin-bottom:16px;';
|
|
3584
|
+
var b1 = document.createElement('button'); b1.className = 'btn primary small'; b1.textContent = 'Backup CTM';
|
|
3585
|
+
b1.onclick = function() { _createBackup('ctm'); };
|
|
3586
|
+
var b2 = document.createElement('button'); b2.className = 'btn primary small'; b2.textContent = 'Backup Brain';
|
|
3587
|
+
b2.onclick = function() { _createBackup('brain'); };
|
|
3588
|
+
btns.appendChild(b1); btns.appendChild(b2);
|
|
3589
|
+
body.appendChild(btns);
|
|
3590
|
+
|
|
3591
|
+
// CTM backups table
|
|
3592
|
+
_appendBackupTable(body, 'CTM Backups', ctm.backups || [], 'ctm', true);
|
|
3593
|
+
// Brain backups table
|
|
3594
|
+
_appendBackupTable(body, 'Wall-E Brain Backups', brain.backups || [], 'brain', false);
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
function _makeDbCard(title, dbPath, dbSize, backupDir) {
|
|
3598
|
+
var card = document.createElement('div');
|
|
3599
|
+
card.style.cssText = 'background:var(--bg-light,#161b22);border:1px solid var(--border);border-radius:8px;padding:12px;';
|
|
3600
|
+
var h = document.createElement('div'); h.style.cssText = 'font-size:13px;font-weight:600;margin-bottom:6px;'; h.textContent = title;
|
|
3601
|
+
card.appendChild(h);
|
|
3602
|
+
card.appendChild(_copyablePath('DB', dbPath));
|
|
3603
|
+
card.appendChild(_copyablePath('Backups', backupDir));
|
|
3604
|
+
var sz = document.createElement('div'); sz.style.cssText = 'font-size:11px;color:var(--fg-dim);'; sz.textContent = 'Size: ' + _fmtSize(dbSize || 0);
|
|
3605
|
+
card.appendChild(sz);
|
|
3606
|
+
return card;
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
function _copyablePath(label, p) {
|
|
3610
|
+
var row = document.createElement('div');
|
|
3611
|
+
row.style.cssText = 'font-size:11px;color:var(--fg-dim);margin-bottom:3px;cursor:pointer;';
|
|
3612
|
+
row.title = 'Click to copy';
|
|
3613
|
+
row.textContent = label + ': ';
|
|
3614
|
+
var span = document.createElement('span'); span.style.color = 'var(--accent)'; span.textContent = p || 'N/A';
|
|
3615
|
+
row.appendChild(span);
|
|
3616
|
+
row.onclick = function() { navigator.clipboard.writeText(p || ''); if (typeof showToast === 'function') showToast('Path copied'); };
|
|
3617
|
+
return row;
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
function _appendBackupTable(container, title, backups, type, showRestore) {
|
|
3621
|
+
var h3 = document.createElement('h3');
|
|
3622
|
+
h3.style.cssText = 'font-size:13px;color:var(--fg-dim);margin:20px 0 8px;';
|
|
3623
|
+
h3.textContent = title + ' (' + backups.length + ')';
|
|
3624
|
+
container.appendChild(h3);
|
|
3625
|
+
if (backups.length === 0) {
|
|
3626
|
+
var p = document.createElement('p'); p.style.cssText = 'font-size:12px;color:var(--fg-dim)'; p.textContent = 'No backups yet.';
|
|
3627
|
+
container.appendChild(p); return;
|
|
3628
|
+
}
|
|
3629
|
+
var table = document.createElement('table');
|
|
3630
|
+
table.style.cssText = 'width:100%;font-size:12px;border-collapse:collapse;';
|
|
3631
|
+
var thead = document.createElement('tr');
|
|
3632
|
+
thead.style.cssText = 'color:var(--fg-dim);text-align:left;';
|
|
3633
|
+
['Name','Size','Date',''].forEach(function(t) { var th = document.createElement('th'); th.style.padding = '4px 8px'; th.textContent = t; thead.appendChild(th); });
|
|
3634
|
+
table.appendChild(thead);
|
|
3635
|
+
backups.slice(0, 20).forEach(function(b) {
|
|
3636
|
+
var tr = document.createElement('tr'); tr.style.borderTop = '1px solid var(--border)';
|
|
3637
|
+
var td1 = document.createElement('td'); td1.style.cssText = 'padding:6px 8px;color:var(--fg)'; td1.textContent = b.name + (b.hasImages ? ' + images' : '');
|
|
3638
|
+
var td2 = document.createElement('td'); td2.style.cssText = 'padding:6px 8px;color:var(--fg-dim)'; td2.textContent = _fmtSize(b.size);
|
|
3639
|
+
var td3 = document.createElement('td'); td3.style.cssText = 'padding:6px 8px;color:var(--fg-dim)'; td3.textContent = _fmtDate(b.createdAt);
|
|
3640
|
+
var td4 = document.createElement('td'); td4.style.cssText = 'padding:6px 8px;text-align:right;';
|
|
3641
|
+
if (showRestore) {
|
|
3642
|
+
var rb = document.createElement('button'); rb.className = 'btn small'; rb.textContent = 'Restore';
|
|
3643
|
+
rb.onclick = (function(n) { return function() { _restoreBackup(type, n); }; })(b.name);
|
|
3644
|
+
td4.appendChild(rb);
|
|
3645
|
+
td4.appendChild(document.createTextNode(' '));
|
|
3646
|
+
}
|
|
3647
|
+
var db = document.createElement('button'); db.className = 'btn small'; db.style.color = 'var(--red)'; db.textContent = 'Delete';
|
|
3648
|
+
db.onclick = (function(n) { return function() { _deleteBackup(type, n); }; })(b.name);
|
|
3649
|
+
td4.appendChild(db);
|
|
3650
|
+
tr.appendChild(td1); tr.appendChild(td2); tr.appendChild(td3); tr.appendChild(td4);
|
|
3651
|
+
table.appendChild(tr);
|
|
3652
|
+
});
|
|
3653
|
+
container.appendChild(table);
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3656
|
+
function _fmtSize(bytes) {
|
|
3657
|
+
if (!bytes) return '0 B';
|
|
3658
|
+
if (bytes < 1024) return bytes + ' B';
|
|
3659
|
+
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
|
3660
|
+
return (bytes / 1048576).toFixed(1) + ' MB';
|
|
3661
|
+
}
|
|
3662
|
+
function _fmtDate(iso) {
|
|
3663
|
+
if (!iso) return '';
|
|
3664
|
+
try { return new Date(iso).toLocaleString(); } catch(e) { return iso; }
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
function _createBackup(type) {
|
|
3668
|
+
var token = window._ctmState ? window._ctmState.token : '';
|
|
3669
|
+
var url = type === 'ctm' ? '/api/backups?token=' + token : '/api/wall-e/backups?token=' + token;
|
|
3670
|
+
fetch(url, { method: 'POST', headers: {'Content-Type':'application/json'}, body: '{}' })
|
|
3671
|
+
.then(function() { if (typeof showToast === 'function') showToast((type === 'ctm' ? 'CTM' : 'Brain') + ' backup created'); loadBackupsData(); })
|
|
3672
|
+
.catch(function(e) { if (typeof showToast === 'function') showToast('Backup failed', 'var(--red)'); });
|
|
3673
|
+
}
|
|
3674
|
+
function _deleteBackup(type, name) {
|
|
3675
|
+
if (!confirm('Delete backup ' + name + '?')) return;
|
|
3676
|
+
var token = window._ctmState ? window._ctmState.token : '';
|
|
3677
|
+
var url = type === 'ctm' ? '/api/backups/' + encodeURIComponent(name) + '?token=' + token : '/api/wall-e/backups/' + encodeURIComponent(name) + '?token=' + token;
|
|
3678
|
+
fetch(url, { method: 'DELETE' }).then(function() { loadBackupsData(); });
|
|
3679
|
+
}
|
|
3680
|
+
function _restoreBackup(type, name) {
|
|
3681
|
+
if (!confirm('Restore from ' + name + '? Current data will be backed up first.')) return;
|
|
3682
|
+
var token = window._ctmState ? window._ctmState.token : '';
|
|
3683
|
+
fetch('/api/backups/restore?token=' + token, { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ name: name }) })
|
|
3684
|
+
.then(function() { if (typeof showToast === 'function') showToast('Restored from ' + name); loadBackupsData(); })
|
|
3685
|
+
.catch(function(e) { if (typeof showToast === 'function') showToast('Restore failed', 'var(--red)'); });
|
|
3686
|
+
}
|
|
3687
|
+
|
|
3529
3688
|
// --- Event Handlers ---
|
|
3530
3689
|
function onCreated(msg) {
|
|
3531
3690
|
const { id, pid, label, cwd } = msg;
|
|
@@ -4320,6 +4479,7 @@ async function loadPrefs() {
|
|
|
4320
4479
|
show_titles: 'show-titles',
|
|
4321
4480
|
hide_tool_msgs: 'hide-tool-msgs',
|
|
4322
4481
|
show_internal: 'show-internal-toggle',
|
|
4482
|
+
this_device: 'this-device',
|
|
4323
4483
|
};
|
|
4324
4484
|
for (const [pref, elId] of Object.entries(checkboxMap)) {
|
|
4325
4485
|
if (pref in prefs) {
|
|
@@ -4595,18 +4755,20 @@ function sessionDisplayText(s, fallback) {
|
|
|
4595
4755
|
|
|
4596
4756
|
function sessionItemHtml(s) {
|
|
4597
4757
|
const showTitles = document.getElementById('show-titles').checked;
|
|
4758
|
+
const thisDevice = document.getElementById('this-device')?.checked !== false;
|
|
4598
4759
|
const ago = timeAgo(s.modifiedAt);
|
|
4599
4760
|
const project = s.project.replace(/^\/Users\/[^/]+\//, '~/');
|
|
4600
4761
|
const displayText = sessionDisplayText(s, '(empty session)');
|
|
4601
4762
|
const branch = s.gitBranch ? `<span>${escHtml(s.gitBranch)}</span>` : '';
|
|
4602
4763
|
const emptyTag = s.isEmpty ? '<span style="color:var(--yellow);font-size:9px;margin-left:4px">[empty]</span>' : '';
|
|
4764
|
+
const deviceTag = (!thisDevice && s.hostname) ? `<span style="color:var(--accent);font-size:9px;margin-left:4px">${escHtml(s.hostname)}</span>` : '';
|
|
4603
4765
|
const msgCount = s.userMsgCount > 0 ? `<span>${s.userMsgCount} msgs</span>` : '';
|
|
4604
4766
|
const reviewing = state.reviewingSessionId === s.sessionId ? ' reviewing' : '';
|
|
4605
4767
|
const isPinned = pinnedSessionIds.includes(s.sessionId);
|
|
4606
4768
|
const pinCls = isPinned ? ' pinned' : '';
|
|
4607
4769
|
const dragAttrs = isPinned ? ` draggable="true" ondragstart="pinDragStart(event)" ondragover="pinDragOver(event)" ondragleave="pinDragLeave(event)" ondrop="pinDrop(event)" ondragend="pinDragEnd(event)"` : '';
|
|
4608
4770
|
return `<div class="recent-item${reviewing}${pinCls}" data-session-id="${s.sessionId}"${dragAttrs} onclick="openSessionReview('${s.sessionId}')" oncontextmenu="event.preventDefault();togglePinSession('${s.sessionId}')">
|
|
4609
|
-
<div class="recent-msg"><span class="pin-icon" style="${isPinned ? 'cursor:grab;' : ''}">★</span><span class="recent-msg-text">${escHtml(displayText)}</span>${emptyTag}</div>
|
|
4771
|
+
<div class="recent-msg"><span class="pin-icon" style="${isPinned ? 'cursor:grab;' : ''}">★</span><span class="recent-msg-text">${escHtml(displayText)}</span>${emptyTag}${deviceTag}</div>
|
|
4610
4772
|
<div class="recent-meta">
|
|
4611
4773
|
<span class="project">${escHtml(project)}</span>
|
|
4612
4774
|
${branch}
|
|
@@ -7120,7 +7282,7 @@ window.addEventListener('message', (e) => {
|
|
|
7120
7282
|
});
|
|
7121
7283
|
|
|
7122
7284
|
// --- Hash routing ---
|
|
7123
|
-
const NAV_TARGETS = ['sessions', 'prompts', 'rules', 'insights', 'permissions', 'codereview', 'walle'];
|
|
7285
|
+
const NAV_TARGETS = ['sessions', 'prompts', 'rules', 'insights', 'permissions', 'codereview', 'walle', 'backups'];
|
|
7124
7286
|
|
|
7125
7287
|
function handleHashRoute() {
|
|
7126
7288
|
const hash = location.hash.slice(1);
|
|
@@ -2315,7 +2315,8 @@ async function deleteTemplate(id) {
|
|
|
2315
2315
|
// ============================================================
|
|
2316
2316
|
async function loadBackups() {
|
|
2317
2317
|
const res = await fetch(API('/backups'));
|
|
2318
|
-
const
|
|
2318
|
+
const data = await res.json();
|
|
2319
|
+
const backups = data.backups || (Array.isArray(data) ? data : []);
|
|
2319
2320
|
const list = document.getElementById('pe-backup-list');
|
|
2320
2321
|
if (!backups.length) {
|
|
2321
2322
|
list.innerHTML = `<div class="pe-empty-state"><h3>No Backups Yet</h3><p>Click "Backup Now" to create your first manual backup.</p></div>`;
|
|
@@ -778,6 +778,7 @@ function renderChatUI() {
|
|
|
778
778
|
html += '<span class="we-export-count">' + chatSelected.size + ' selected</span>';
|
|
779
779
|
html += '<button class="walle-btn" onclick="WE._exportAsText()">Copy as Text</button>';
|
|
780
780
|
html += '<button class="walle-btn" onclick="WE._exportAsImage()">Save as Image</button>';
|
|
781
|
+
html += '<button class="walle-btn danger" onclick="WE._deleteSelected()">Delete</button>';
|
|
781
782
|
html += '<button class="we-chat-search-clear" onclick="WE._clearSelection()" title="Clear selection">×</button>';
|
|
782
783
|
html += '</div>';
|
|
783
784
|
}
|
|
@@ -1381,7 +1382,52 @@ WE._toggleTurn = function(turnIdx) {
|
|
|
1381
1382
|
} else {
|
|
1382
1383
|
chatSelected.add(turnIdx);
|
|
1383
1384
|
}
|
|
1384
|
-
|
|
1385
|
+
// Update just the clicked turn visually — don't re-render (preserves scroll)
|
|
1386
|
+
var el = document.querySelector('.we-turn-group[data-turn="' + turnIdx + '"]');
|
|
1387
|
+
if (el) {
|
|
1388
|
+
el.classList.toggle('we-selected', chatSelected.has(turnIdx));
|
|
1389
|
+
var cb = el.querySelector('.we-turn-checkbox');
|
|
1390
|
+
if (cb) cb.textContent = chatSelected.has(turnIdx) ? '\u2611' : '\u2610';
|
|
1391
|
+
}
|
|
1392
|
+
// Update the export bar (insert via DOM to preserve scroll)
|
|
1393
|
+
var bar = document.querySelector('.we-export-bar');
|
|
1394
|
+
if (chatSelected.size > 0 && !bar) {
|
|
1395
|
+
var newBar = document.createElement('div');
|
|
1396
|
+
newBar.className = 'we-export-bar';
|
|
1397
|
+
newBar.textContent = ''; // clear
|
|
1398
|
+
var countSpan = document.createElement('span');
|
|
1399
|
+
countSpan.className = 'we-export-count';
|
|
1400
|
+
countSpan.textContent = chatSelected.size + ' selected';
|
|
1401
|
+
newBar.appendChild(countSpan);
|
|
1402
|
+
var copyBtn = document.createElement('button');
|
|
1403
|
+
copyBtn.className = 'walle-btn';
|
|
1404
|
+
copyBtn.textContent = 'Copy as Text';
|
|
1405
|
+
copyBtn.onclick = function() { WE._exportAsText(); };
|
|
1406
|
+
newBar.appendChild(copyBtn);
|
|
1407
|
+
var imgBtn = document.createElement('button');
|
|
1408
|
+
imgBtn.className = 'walle-btn';
|
|
1409
|
+
imgBtn.textContent = 'Save as Image';
|
|
1410
|
+
imgBtn.onclick = function() { WE._exportAsImage(); };
|
|
1411
|
+
newBar.appendChild(imgBtn);
|
|
1412
|
+
var delBtn = document.createElement('button');
|
|
1413
|
+
delBtn.className = 'walle-btn danger';
|
|
1414
|
+
delBtn.textContent = 'Delete';
|
|
1415
|
+
delBtn.onclick = function() { WE._deleteSelected(); };
|
|
1416
|
+
newBar.appendChild(delBtn);
|
|
1417
|
+
var clearBtn = document.createElement('button');
|
|
1418
|
+
clearBtn.className = 'we-chat-search-clear';
|
|
1419
|
+
clearBtn.title = 'Clear selection';
|
|
1420
|
+
clearBtn.textContent = '\u00d7';
|
|
1421
|
+
clearBtn.onclick = function() { WE._clearSelection(); };
|
|
1422
|
+
newBar.appendChild(clearBtn);
|
|
1423
|
+
var searchBar = document.querySelector('.we-chat-search-bar');
|
|
1424
|
+
if (searchBar) searchBar.after(newBar);
|
|
1425
|
+
} else if (chatSelected.size === 0 && bar) {
|
|
1426
|
+
bar.remove();
|
|
1427
|
+
} else if (bar) {
|
|
1428
|
+
var countEl = bar.querySelector('.we-export-count');
|
|
1429
|
+
if (countEl) countEl.textContent = chatSelected.size + ' selected';
|
|
1430
|
+
}
|
|
1385
1431
|
};
|
|
1386
1432
|
|
|
1387
1433
|
WE._clearSelection = function() {
|
|
@@ -1515,6 +1561,36 @@ WE._exportAsImage = function() {
|
|
|
1515
1561
|
}
|
|
1516
1562
|
};
|
|
1517
1563
|
|
|
1564
|
+
WE._deleteSelected = function() {
|
|
1565
|
+
var turns = _getSelectedTurns();
|
|
1566
|
+
if (turns.length === 0) return;
|
|
1567
|
+
if (!confirm('Delete ' + turns.length + ' message' + (turns.length > 1 ? 's' : '') + '? This cannot be undone.')) return;
|
|
1568
|
+
|
|
1569
|
+
var count = turns.length;
|
|
1570
|
+
var promises = turns.map(function(t) {
|
|
1571
|
+
return apiPost('/chat/delete', {
|
|
1572
|
+
session_id: cache.chatSessionId || 'default',
|
|
1573
|
+
user_content: t.userText || undefined,
|
|
1574
|
+
assistant_content: t.assistantText || undefined,
|
|
1575
|
+
});
|
|
1576
|
+
});
|
|
1577
|
+
|
|
1578
|
+
Promise.all(promises).then(function() {
|
|
1579
|
+
chatSelected.clear();
|
|
1580
|
+
chatSelectMode = false;
|
|
1581
|
+
// Reload chat history
|
|
1582
|
+
api('/chat/history?session_id=' + encodeURIComponent(cache.chatSessionId || 'default') + '&limit=200').then(function(result) {
|
|
1583
|
+
chatHistory = (result.data || []).map(function(m) {
|
|
1584
|
+
return { role: m.role, text: m.content || m.text || '', ts: m.timestamp };
|
|
1585
|
+
});
|
|
1586
|
+
renderChatUI();
|
|
1587
|
+
if (typeof showToast === 'function') showToast('Deleted ' + count + ' message' + (count > 1 ? 's' : ''));
|
|
1588
|
+
});
|
|
1589
|
+
}).catch(function(err) {
|
|
1590
|
+
if (typeof showToast === 'function') showToast('Delete failed: ' + (err.message || 'unknown error'), 'var(--red)');
|
|
1591
|
+
});
|
|
1592
|
+
};
|
|
1593
|
+
|
|
1518
1594
|
// ---- Chat Search ----
|
|
1519
1595
|
WE._onChatSearch = function(val) {
|
|
1520
1596
|
chatSearchQuery = val;
|