create-walle 0.4.5 → 0.5.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.
@@ -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. Reinstall deps (in case package.json changed)
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
 
@@ -392,6 +398,17 @@ function npmInstall(dir) {
392
398
  }
393
399
  }
394
400
 
401
+ function stampVersion(dir) {
402
+ try {
403
+ const ver = require('../package.json').version;
404
+ const pkgPath = path.join(dir, 'package.json');
405
+ let pkg = {};
406
+ try { pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); } catch {}
407
+ pkg.version = ver;
408
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
409
+ } catch {}
410
+ }
411
+
395
412
  function saveWalleDir(dir) {
396
413
  const metaDir = path.join(process.env.HOME, '.walle');
397
414
  fs.mkdirSync(metaDir, { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-walle",
3
- "version": "0.4.5",
3
+ "version": "0.5.0",
4
4
  "description": "Set up Wall-E — your personal digital twin",
5
5
  "bin": {
6
6
  "create-walle": "bin/create-walle.js"
@@ -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; }
@@ -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">&times;</button>';
782
783
  html += '</div>';
783
784
  }
@@ -1381,7 +1382,23 @@ WE._toggleTurn = function(turnIdx) {
1381
1382
  } else {
1382
1383
  chatSelected.add(turnIdx);
1383
1384
  }
1384
- renderChatUI();
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
1393
+ var bar = document.querySelector('.we-export-bar');
1394
+ if (chatSelected.size > 0 && !bar) {
1395
+ renderChatUI(); // first selection — need to add toolbar
1396
+ } else if (chatSelected.size === 0 && bar) {
1397
+ bar.remove();
1398
+ } else if (bar) {
1399
+ var countEl = bar.querySelector('.we-export-count');
1400
+ if (countEl) countEl.textContent = chatSelected.size + ' selected';
1401
+ }
1385
1402
  };
1386
1403
 
1387
1404
  WE._clearSelection = function() {
@@ -1515,6 +1532,36 @@ WE._exportAsImage = function() {
1515
1532
  }
1516
1533
  };
1517
1534
 
1535
+ WE._deleteSelected = function() {
1536
+ var turns = _getSelectedTurns();
1537
+ if (turns.length === 0) return;
1538
+ if (!confirm('Delete ' + turns.length + ' message' + (turns.length > 1 ? 's' : '') + '? This cannot be undone.')) return;
1539
+
1540
+ var count = turns.length;
1541
+ var promises = turns.map(function(t) {
1542
+ return apiPost('/chat/delete', {
1543
+ session_id: cache.chatSessionId || 'default',
1544
+ user_content: t.userText || undefined,
1545
+ assistant_content: t.assistantText || undefined,
1546
+ });
1547
+ });
1548
+
1549
+ Promise.all(promises).then(function() {
1550
+ chatSelected.clear();
1551
+ chatSelectMode = false;
1552
+ // Reload chat history
1553
+ api('/chat/history?session_id=' + encodeURIComponent(cache.chatSessionId || 'default') + '&limit=200').then(function(result) {
1554
+ chatHistory = (result.data || []).map(function(m) {
1555
+ return { role: m.role, text: m.content || m.text || '', ts: m.timestamp };
1556
+ });
1557
+ renderChatUI();
1558
+ if (typeof showToast === 'function') showToast('Deleted ' + count + ' message' + (count > 1 ? 's' : ''));
1559
+ });
1560
+ }).catch(function(err) {
1561
+ if (typeof showToast === 'function') showToast('Delete failed: ' + (err.message || 'unknown error'), 'var(--red)');
1562
+ });
1563
+ };
1564
+
1518
1565
  // ---- Chat Search ----
1519
1566
  WE._onChatSearch = function(val) {
1520
1567
  chatSearchQuery = val;
@@ -17,7 +17,6 @@
17
17
  .status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
18
18
  .status-dot.ok { background: var(--green); }
19
19
  .status-dot.missing { background: var(--yellow); }
20
- .status-dot.error { background: var(--red); }
21
20
  label { display: block; font-size: 13px; font-weight: 500; margin-bottom: 6px; color: var(--dim); }
22
21
  input[type="text"], input[type="password"] {
23
22
  width: 100%; padding: 10px 12px; background: var(--bg); border: 1px solid var(--border);
@@ -32,9 +31,8 @@
32
31
  .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
33
32
  .btn-secondary { background: var(--border); color: var(--text); }
34
33
  .btn-secondary:hover { background: #3d444d; }
35
- .btn-row { display: flex; gap: 12px; align-items: center; margin-top: 8px; }
36
- .success-msg { color: var(--green); font-size: 13px; display: none; }
37
- .error-msg { color: var(--red); font-size: 13px; display: none; }
34
+ .btn-row { display: flex; gap: 12px; align-items: center; margin-top: 8px; flex-wrap: wrap; }
35
+ .error-msg { color: var(--red); font-size: 13px; display: none; width: 100%; margin-top: 4px; }
38
36
  .integration { display: flex; align-items: center; justify-content: space-between; padding: 12px 0; border-bottom: 1px solid var(--border); }
39
37
  .integration:last-child { border-bottom: none; }
40
38
  .integration-info { display: flex; align-items: center; gap: 12px; }
@@ -47,6 +45,19 @@
47
45
  .done-section { text-align: center; padding: 16px 0; }
48
46
  .done-section a { color: var(--accent); text-decoration: none; font-weight: 500; }
49
47
  .done-section a:hover { text-decoration: underline; }
48
+
49
+ /* Toast notification */
50
+ .toast {
51
+ position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%) translateY(80px);
52
+ background: var(--card); border: 1px solid var(--border); border-radius: 10px;
53
+ padding: 12px 24px; font-size: 14px; font-weight: 500;
54
+ box-shadow: 0 8px 32px rgba(0,0,0,0.4);
55
+ opacity: 0; transition: all 0.3s ease; pointer-events: none; z-index: 100;
56
+ display: flex; align-items: center; gap: 8px;
57
+ }
58
+ .toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
59
+ .toast.success { border-color: var(--green); color: var(--green); }
60
+ .toast.error { border-color: var(--red); color: var(--red); }
50
61
  </style>
51
62
  </head>
52
63
  <body>
@@ -54,7 +65,7 @@
54
65
  <h1>Welcome to Wall-E</h1>
55
66
  <p class="subtitle">Let's get you set up. This takes about 30 seconds.</p>
56
67
 
57
- <!-- Step 1: Owner -->
68
+ <!-- Owner -->
58
69
  <div class="card">
59
70
  <h2><span class="status-dot ok" id="owner-dot"></span> Owner</h2>
60
71
  <p>Auto-detected from your system.</p>
@@ -64,7 +75,7 @@
64
75
  </div>
65
76
  </div>
66
77
 
67
- <!-- Step 2: API Key -->
78
+ <!-- API Key -->
68
79
  <div class="card">
69
80
  <h2><span class="status-dot missing" id="api-dot"></span> Anthropic API Key</h2>
70
81
  <p>Required for AI features (chat, think, reflect). Get one at <a href="https://console.anthropic.com/settings/keys" target="_blank" style="color:var(--accent)">console.anthropic.com</a></p>
@@ -73,18 +84,16 @@
73
84
  <input type="password" id="api-key" placeholder="sk-ant-...">
74
85
  </div>
75
86
  <div class="btn-row">
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>
78
- <span class="success-msg" id="save-ok">Saved!</span>
87
+ <button class="btn btn-primary" id="save-btn" onclick="saveConfig()">Save</button>
88
+ <button class="btn btn-secondary" id="detect-btn" onclick="detectKey()">Detect from environment</button>
79
89
  <span class="error-msg" id="save-err"></span>
80
90
  </div>
81
91
  </div>
82
92
 
83
- <!-- Step 3: Integrations -->
93
+ <!-- Integrations -->
84
94
  <div class="card">
85
95
  <h2>Integrations</h2>
86
96
  <p>Optional — connect these anytime from the Wall-E settings.</p>
87
-
88
97
  <div class="integration" id="int-slack">
89
98
  <div class="integration-info">
90
99
  <div class="integration-icon">💬</div>
@@ -95,7 +104,6 @@
95
104
  </div>
96
105
  <button class="btn btn-secondary" id="slack-btn" onclick="connectSlack()">Connect</button>
97
106
  </div>
98
-
99
107
  <div class="integration">
100
108
  <div class="integration-info">
101
109
  <div class="integration-icon">📧</div>
@@ -106,7 +114,6 @@
106
114
  </div>
107
115
  <span class="badge badge-ready">Auto</span>
108
116
  </div>
109
-
110
117
  <div class="integration">
111
118
  <div class="integration-info">
112
119
  <div class="integration-icon">📅</div>
@@ -127,7 +134,19 @@
127
134
  </div>
128
135
  </div>
129
136
 
137
+ <!-- Toast -->
138
+ <div class="toast" id="toast"></div>
139
+
130
140
  <script>
141
+ function showToast(msg, type) {
142
+ const t = document.getElementById('toast');
143
+ t.className = 'toast ' + (type || 'success');
144
+ t.textContent = type === 'error' ? msg : '✓ ' + msg;
145
+ t.classList.add('show');
146
+ clearTimeout(t._timer);
147
+ t._timer = setTimeout(() => t.classList.remove('show'), 3000);
148
+ }
149
+
131
150
  async function loadStatus() {
132
151
  try {
133
152
  const r = await fetch('/api/setup/status');
@@ -138,7 +157,7 @@
138
157
  document.getElementById('api-key').placeholder = '••••••••••••••• (configured)';
139
158
  }
140
159
  if (d.slack_connected) {
141
- document.getElementById('slack-btn').outerHTML = '<span class="badge badge-connected">Connected</span>';
160
+ _showSlackConnected(d.slack_team);
142
161
  }
143
162
  if (d.version) {
144
163
  document.getElementById('version-label').textContent = 'Wall-E v' + d.version;
@@ -148,9 +167,7 @@
148
167
 
149
168
  async function saveConfig() {
150
169
  const btn = document.getElementById('save-btn');
151
- const okMsg = document.getElementById('save-ok');
152
170
  const errMsg = document.getElementById('save-err');
153
- okMsg.style.display = 'none';
154
171
  errMsg.style.display = 'none';
155
172
  btn.disabled = true;
156
173
 
@@ -163,19 +180,18 @@
163
180
  btn.disabled = false;
164
181
  return;
165
182
  }
166
- const body = { owner_name: ownerVal, api_key: apiVal };
167
183
 
168
184
  try {
169
185
  const r = await fetch('/api/setup/save', {
170
186
  method: 'POST',
171
187
  headers: { 'Content-Type': 'application/json' },
172
- body: JSON.stringify(body),
188
+ body: JSON.stringify({ owner_name: ownerVal, api_key: apiVal }),
173
189
  });
174
190
  const d = await r.json();
175
191
  if (d.ok) {
176
- okMsg.style.display = 'inline';
177
- document.getElementById('api-dot').className = 'status-dot ok';
178
192
  document.getElementById('owner-dot').className = 'status-dot ok';
193
+ if (apiVal) document.getElementById('api-dot').className = 'status-dot ok';
194
+ showToast('Settings saved');
179
195
  } else {
180
196
  errMsg.textContent = d.error || 'Save failed';
181
197
  errMsg.style.display = 'inline';
@@ -187,6 +203,16 @@
187
203
  btn.disabled = false;
188
204
  }
189
205
 
206
+ function _showSlackConnected(team) {
207
+ const el = document.getElementById('int-slack');
208
+ if (!el) return;
209
+ const badge = team
210
+ ? '<span class="badge badge-connected">Connected to ' + team + '</span>'
211
+ : '<span class="badge badge-connected">Connected</span>';
212
+ const btn = el.querySelector('#slack-btn');
213
+ if (btn) btn.outerHTML = badge;
214
+ }
215
+
190
216
  async function connectSlack() {
191
217
  const btn = document.getElementById('slack-btn');
192
218
  try {
@@ -195,10 +221,10 @@
195
221
  const r = await fetch('/api/wall-e/slack/auth', { method: 'POST' });
196
222
  const d = await r.json();
197
223
  if (d.ok && d.already) {
198
- btn.outerHTML = '<span class="badge badge-connected">Connected</span>';
224
+ _showSlackConnected('');
225
+ showToast('Slack already connected');
199
226
  } else if (d.ok) {
200
- btn.textContent = 'Check browser...';
201
- // Poll for completion (OAuth callback happens server-side)
227
+ btn.textContent = 'Waiting for browser...';
202
228
  let attempts = 0;
203
229
  const poll = setInterval(async () => {
204
230
  attempts++;
@@ -207,10 +233,11 @@
207
233
  const sd = await sr.json();
208
234
  if (sd.slack_connected) {
209
235
  clearInterval(poll);
210
- btn.outerHTML = '<span class="badge badge-connected">Connected</span>';
211
- } else if (attempts > 60) {
236
+ _showSlackConnected(sd.slack_team);
237
+ showToast('Slack connected' + (sd.slack_team ? ' to ' + sd.slack_team : ''));
238
+ } else if (attempts > 150) {
212
239
  clearInterval(poll);
213
- btn.textContent = 'Timed out';
240
+ btn.textContent = 'Timed out — try again';
214
241
  btn.disabled = false;
215
242
  }
216
243
  } catch {}
@@ -218,6 +245,7 @@
218
245
  } else {
219
246
  btn.textContent = d.error || 'Failed';
220
247
  btn.disabled = false;
248
+ showToast(d.error || 'Slack connection failed', 'error');
221
249
  }
222
250
  } catch (e) {
223
251
  btn.textContent = 'Connect';
@@ -228,9 +256,7 @@
228
256
  async function detectKey() {
229
257
  const btn = document.getElementById('detect-btn');
230
258
  const errMsg = document.getElementById('save-err');
231
- const okMsg = document.getElementById('save-ok');
232
259
  errMsg.style.display = 'none';
233
- okMsg.style.display = 'none';
234
260
  btn.disabled = true;
235
261
  btn.textContent = 'Checking...';
236
262
  try {
@@ -240,8 +266,6 @@
240
266
  document.getElementById('api-key').value = '';
241
267
  document.getElementById('api-key').placeholder = '••••••••••••••• (from ' + (d.source || 'environment') + ')';
242
268
  document.getElementById('api-dot').className = 'status-dot ok';
243
- okMsg.textContent = 'Detected: ' + (d.source || 'environment') + '!';
244
- okMsg.style.display = 'inline';
245
269
  const ownerVal = document.getElementById('owner-name').value.trim();
246
270
  const saveBody = { owner_name: ownerVal };
247
271
  if (d.gateway) saveBody.gateway = d.gateway;
@@ -251,6 +275,7 @@
251
275
  headers: { 'Content-Type': 'application/json' },
252
276
  body: JSON.stringify(saveBody),
253
277
  });
278
+ showToast('Detected and saved: ' + (d.source || 'environment'));
254
279
  } else {
255
280
  errMsg.textContent = d.hint || 'No API key found. Enter one manually.';
256
281
  errMsg.style.display = 'inline';
@@ -197,14 +197,19 @@ function handleApi(req, res, url) {
197
197
  } catch {}
198
198
  }
199
199
  let slackConnected = false;
200
+ let slackTeam = '';
200
201
  try {
201
- const tokPath = path.join(process.env.HOME, '.walle', 'data', 'oauth-tokens', 'slack.json');
202
- slackConnected = fs.existsSync(tokPath);
202
+ const tokPath = path.join(process.env.HOME, '.claude', 'wall-e-slack-token.json');
203
+ if (fs.existsSync(tokPath)) {
204
+ const tok = JSON.parse(fs.readFileSync(tokPath, 'utf8'));
205
+ slackConnected = !!(tok.access_token);
206
+ slackTeam = tok.team_name || '';
207
+ }
203
208
  } catch {}
204
209
  res.writeHead(200, { 'Content-Type': 'application/json' });
205
210
  let version = '';
206
211
  try { version = require('../package.json').version; } catch {}
207
- res.end(JSON.stringify({ owner_name: ownerName, has_api_key: hasApiKey, slack_connected: slackConnected, needs_setup: setup.needsSetup(), version }));
212
+ res.end(JSON.stringify({ owner_name: ownerName, has_api_key: hasApiKey, slack_connected: slackConnected, slack_team: slackTeam, needs_setup: setup.needsSetup(), version }));
208
213
  return;
209
214
  }
210
215
  if (url.pathname === '/api/setup/detect-key' && req.method === 'GET') {
@@ -313,16 +318,21 @@ function handleApi(req, res, url) {
313
318
  // Accept any non-empty key (Anthropic, Portkey, or other providers)
314
319
  const envPath = path.resolve(__dirname, '..', '.env');
315
320
  const lines = [];
316
- // Build set of keys we're about to write
321
+ // Build set of keys to strip API key and gateway are mutually exclusive
317
322
  const keysToReplace = new Set();
318
323
  if (ownerName) keysToReplace.add('WALLE_OWNER_NAME');
319
- if (apiKey) keysToReplace.add('ANTHROPIC_API_KEY');
320
- if (gw) { keysToReplace.add('ANTHROPIC_BASE_URL'); keysToReplace.add('ANTHROPIC_AUTH_TOKEN'); keysToReplace.add('ANTHROPIC_CUSTOM_HEADERS_B64'); }
324
+ if (apiKey || gw) {
325
+ // Always strip both they're mutually exclusive
326
+ keysToReplace.add('ANTHROPIC_API_KEY');
327
+ keysToReplace.add('ANTHROPIC_BASE_URL');
328
+ keysToReplace.add('ANTHROPIC_AUTH_TOKEN');
329
+ keysToReplace.add('ANTHROPIC_CUSTOM_HEADERS_B64');
330
+ }
321
331
  // Read existing .env, keep lines that aren't being replaced
322
332
  try {
323
333
  const existing = fs.readFileSync(envPath, 'utf8');
324
334
  for (const line of existing.split('\n')) {
325
- const m = line.match(/^\s*#?\s*([A-Z_]+)\s*=/);
335
+ const m = line.match(/^\s*#?\s*([A-Z0-9_]+)\s*=/);
326
336
  if (m && keysToReplace.has(m[1])) continue; // skip — will re-add below
327
337
  lines.push(line);
328
338
  }
@@ -336,16 +346,21 @@ function handleApi(req, res, url) {
336
346
  process.env.WALLE_OWNER_NAME = ownerName;
337
347
  }
338
348
  if (gw) {
339
- // Gateway setup: save all three env vars
349
+ // Gateway setup: save gateway vars, clear direct API key
340
350
  lines.push(`ANTHROPIC_BASE_URL=${gw.base_url}`);
341
351
  lines.push(`ANTHROPIC_AUTH_TOKEN=${gw.auth_token}`);
342
352
  lines.push(`ANTHROPIC_CUSTOM_HEADERS_B64=${gw.custom_headers_b64}`);
343
353
  process.env.ANTHROPIC_BASE_URL = gw.base_url;
344
354
  process.env.ANTHROPIC_AUTH_TOKEN = gw.auth_token;
345
355
  process.env.ANTHROPIC_CUSTOM_HEADERS_B64 = gw.custom_headers_b64;
356
+ delete process.env.ANTHROPIC_API_KEY;
346
357
  } else if (apiKey) {
358
+ // Direct API key: save key, clear gateway vars
347
359
  lines.push(`ANTHROPIC_API_KEY=${apiKey}`);
348
360
  process.env.ANTHROPIC_API_KEY = apiKey;
361
+ delete process.env.ANTHROPIC_BASE_URL;
362
+ delete process.env.ANTHROPIC_AUTH_TOKEN;
363
+ delete process.env.ANTHROPIC_CUSTOM_HEADERS_B64;
349
364
  }
350
365
  fs.writeFileSync(envPath, lines.join('\n') + '\n', { mode: 0o600 });
351
366
  setup.clearSetupCache(); // so next / request goes to dashboard
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "walle",
3
- "version": "0.1.1",
3
+ "version": "0.5.0",
4
4
  "private": true,
5
5
  "description": "Wall-E — your personal digital twin",
6
6
  "scripts": {