create-walle 0.9.13 → 0.9.15

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 (98) hide show
  1. package/README.md +8 -3
  2. package/bin/create-walle.js +232 -32
  3. package/bin/mcp-inject.js +18 -53
  4. package/package.json +3 -1
  5. package/template/claude-task-manager/api-prompts.js +11 -2
  6. package/template/claude-task-manager/approval-agent.js +7 -0
  7. package/template/claude-task-manager/db.js +94 -75
  8. package/template/claude-task-manager/docs/session-standup-command-center-design.md +242 -0
  9. package/template/claude-task-manager/docs/session-tooltip-freshness-design.md +224 -0
  10. package/template/claude-task-manager/docs/session-ux-issue-review-2026-05-01.md +369 -0
  11. package/template/claude-task-manager/fuzzy-utils.js +10 -2
  12. package/template/claude-task-manager/git-utils.js +140 -10
  13. package/template/claude-task-manager/lib/agent-capabilities.js +1 -1
  14. package/template/claude-task-manager/lib/agent-presets.js +38 -5
  15. package/template/claude-task-manager/lib/codex-terminal-final.js +53 -0
  16. package/template/claude-task-manager/lib/ctm-session-context-api.js +222 -0
  17. package/template/claude-task-manager/lib/session-diagnostics.js +56 -0
  18. package/template/claude-task-manager/lib/session-history.js +309 -16
  19. package/template/claude-task-manager/lib/session-standup.js +409 -0
  20. package/template/claude-task-manager/lib/session-stream.js +253 -20
  21. package/template/claude-task-manager/lib/standup-attention.js +200 -0
  22. package/template/claude-task-manager/lib/status-hooks.js +8 -2
  23. package/template/claude-task-manager/lib/update-telemetry.js +114 -0
  24. package/template/claude-task-manager/lib/walle-ctm-history.js +49 -6
  25. package/template/claude-task-manager/lib/walle-default-model.js +55 -0
  26. package/template/claude-task-manager/lib/walle-mcp-auto-config.js +66 -0
  27. package/template/claude-task-manager/lib/walle-supervisor.js +86 -19
  28. package/template/claude-task-manager/lib/walle-transcript.js +1 -3
  29. package/template/claude-task-manager/lib/worktree-cwd.js +82 -0
  30. package/template/claude-task-manager/package.json +1 -0
  31. package/template/claude-task-manager/providers/codex-mcp.js +104 -0
  32. package/template/claude-task-manager/providers/index.js +2 -0
  33. package/template/claude-task-manager/public/css/setup.css +2 -1
  34. package/template/claude-task-manager/public/css/walle.css +71 -0
  35. package/template/claude-task-manager/public/index.html +2388 -429
  36. package/template/claude-task-manager/public/js/message-renderer.js +314 -35
  37. package/template/claude-task-manager/public/js/session-search-utils.js +185 -3
  38. package/template/claude-task-manager/public/js/session-status-precedence.js +125 -0
  39. package/template/claude-task-manager/public/js/setup.js +62 -19
  40. package/template/claude-task-manager/public/js/stream-view.js +396 -55
  41. package/template/claude-task-manager/public/js/terminal-restore-state.js +57 -0
  42. package/template/claude-task-manager/public/js/walle-session.js +234 -26
  43. package/template/claude-task-manager/public/js/walle.js +143 -2
  44. package/template/claude-task-manager/server.js +1402 -433
  45. package/template/claude-task-manager/session-integrity.js +77 -28
  46. package/template/claude-task-manager/workers/approval-widget-validator.js +15 -5
  47. package/template/claude-task-manager/workers/scrollback-worker.js +5 -6
  48. package/template/claude-task-manager/workers/state-detectors/codex.js +6 -0
  49. package/template/package.json +1 -1
  50. package/template/wall-e/agent-runners/claude-code.js +2 -0
  51. package/template/wall-e/agent.js +63 -8
  52. package/template/wall-e/api-walle.js +330 -52
  53. package/template/wall-e/brain.js +291 -42
  54. package/template/wall-e/chat.js +172 -15
  55. package/template/wall-e/coding/compaction-service.js +19 -5
  56. package/template/wall-e/coding/stream-processor.js +22 -2
  57. package/template/wall-e/coding/workspace-replay.js +1 -4
  58. package/template/wall-e/coding-orchestrator.js +250 -80
  59. package/template/wall-e/compat.js +0 -28
  60. package/template/wall-e/context/context-builder.js +3 -1
  61. package/template/wall-e/embeddings.js +2 -7
  62. package/template/wall-e/eval/agent-runner.js +30 -9
  63. package/template/wall-e/eval/benchmark-generator.js +21 -1
  64. package/template/wall-e/eval/benchmarks/chat-eval.json +66 -6
  65. package/template/wall-e/eval/benchmarks/coding-agent.json +0 -596
  66. package/template/wall-e/eval/cc-replay.js +1 -0
  67. package/template/wall-e/eval/codex-cli-baseline.js +633 -0
  68. package/template/wall-e/eval/debug-agent003.js +1 -0
  69. package/template/wall-e/eval/eval-orchestrator.js +3 -3
  70. package/template/wall-e/eval/run-agent-benchmarks.js +11 -3
  71. package/template/wall-e/eval/run-codex-cli-baseline.js +177 -0
  72. package/template/wall-e/eval/run-model-comparison.js +1 -0
  73. package/template/wall-e/eval/swebench-adapter.js +1 -0
  74. package/template/wall-e/evaluation/quorum-evaluator.js +0 -1
  75. package/template/wall-e/extraction/knowledge-extractor.js +1 -2
  76. package/template/wall-e/lib/mcp-integration.js +336 -0
  77. package/template/wall-e/llm/ollama.js +47 -8
  78. package/template/wall-e/llm/ollama.plugin.json +1 -1
  79. package/template/wall-e/llm/tool-adapter.js +1 -0
  80. package/template/wall-e/loops/ingest.js +42 -8
  81. package/template/wall-e/loops/initiative.js +87 -2
  82. package/template/wall-e/mcp-server.js +872 -19
  83. package/template/wall-e/memory/ctm-context-client.js +230 -0
  84. package/template/wall-e/memory/ctm-session-context.js +1376 -0
  85. package/template/wall-e/prompts/coding/memory-protocol.md +6 -0
  86. package/template/wall-e/server.js +30 -1
  87. package/template/wall-e/skills/_bundled/memory-search/SKILL.md +8 -0
  88. package/template/wall-e/skills/_bundled/scan-ctm-sessions/SKILL.md +20 -0
  89. package/template/wall-e/skills/_bundled/scan-ctm-sessions/run.js +43 -0
  90. package/template/wall-e/skills/_bundled/slack-mentions/run.js +471 -188
  91. package/template/wall-e/skills/skill-planner.js +86 -4
  92. package/template/wall-e/slack/socket-mode-listener.js +276 -0
  93. package/template/wall-e/telemetry.js +70 -2
  94. package/template/wall-e/tools/builtin-middleware.js +55 -2
  95. package/template/wall-e/tools/shell-policy.js +1 -1
  96. package/template/wall-e/tools/slack-owner.js +104 -0
  97. package/template/website/index.html +4 -4
  98. package/template/builder-journal.md +0 -17
@@ -27,8 +27,8 @@ function getTelemetryDb() {
27
27
  const fs = require('fs');
28
28
  if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
29
29
  telemetryDb = new Database(TELEMETRY_DB_PATH);
30
- telemetryDb.pragma('journal_mode = DELETE');
31
- telemetryDb.pragma('busy_timeout = 3000');
30
+ telemetryDb.pragma('journal_mode = WAL');
31
+ telemetryDb.pragma('busy_timeout = 5000');
32
32
  telemetryDb.exec(`
33
33
  CREATE TABLE IF NOT EXISTS telemetry_events (
34
34
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -53,12 +53,14 @@ function getTelemetryDb() {
53
53
  os TEXT,
54
54
  node_version TEXT,
55
55
  ip TEXT,
56
+ machine_bucket TEXT,
56
57
  event_count INTEGER DEFAULT 0
57
58
  );
58
59
  `);
59
60
  // Migrate: add ip column if missing
60
61
  try { telemetryDb.prepare('ALTER TABLE telemetry_events ADD COLUMN ip TEXT').run(); } catch {}
61
62
  try { telemetryDb.prepare('ALTER TABLE telemetry_installs ADD COLUMN ip TEXT').run(); } catch {}
63
+ try { telemetryDb.prepare('ALTER TABLE telemetry_installs ADD COLUMN machine_bucket TEXT').run(); } catch {}
62
64
  return telemetryDb;
63
65
  } catch (e) {
64
66
  console.error('[wall-e] Failed to init telemetry DB:', e.message);
@@ -158,6 +160,53 @@ function ensureBrainInit() {
158
160
  return true;
159
161
  }
160
162
 
163
+ const SETUP_PROVIDER_TYPES = ['anthropic', 'openai', 'google', 'ollama', 'deepseek'];
164
+ const SETUP_PROVIDER_NAMES = {
165
+ anthropic: 'Anthropic',
166
+ openai: 'OpenAI',
167
+ google: 'Google Gemini',
168
+ ollama: 'Ollama (Local)',
169
+ deepseek: 'DeepSeek',
170
+ };
171
+
172
+ function sanitizeSetupProviderType(value) {
173
+ const type = String(value || '').toLowerCase().replace(/[^a-z0-9_-]/g, '').slice(0, 32);
174
+ return SETUP_PROVIDER_TYPES.includes(type) ? type : '';
175
+ }
176
+
177
+ function sanitizeSetupModel(value) {
178
+ return typeof value === 'string' ? value.replace(/[\r\n\s]/g, '').slice(0, 100) : '';
179
+ }
180
+
181
+ function sanitizeTaskType(value) {
182
+ return typeof value === 'string' ? value.replace(/[^a-z0-9_-]/gi, '').slice(0, 64) : '';
183
+ }
184
+
185
+ function sanitizeModelRegistryId(value) {
186
+ return value == null || value === ''
187
+ ? null
188
+ : String(value).replace(/[^a-z0-9_:.@/\-]/gi, '').slice(0, 128);
189
+ }
190
+
191
+ function syncRuntimeDefaultsFromBrain() {
192
+ if (!brain) return;
193
+ try {
194
+ const provider = brain.getKv('walle_provider');
195
+ const model = brain.getKv('walle_model');
196
+ if (provider) process.env.WALLE_PROVIDER = provider;
197
+ if (model) process.env.WALLE_MODEL = model;
198
+ else delete process.env.WALLE_MODEL;
199
+ _runtimeDefaultsSynced = true;
200
+ } catch {}
201
+ }
202
+
203
+ function resetDefaultClientQuiet() {
204
+ try {
205
+ const { resetDefaultClient } = require('./llm/client');
206
+ resetDefaultClient();
207
+ } catch {}
208
+ }
209
+
161
210
  // --- Individual route handlers (exported for testability) ---
162
211
 
163
212
  function getStatus() {
@@ -305,12 +354,48 @@ function handleWalleApi(req, res, url) {
305
354
  }
306
355
  }
307
356
 
357
+ // POST /api/wall-e/update/prompt-event — anonymous upgrade prompt funnel
358
+ if (p === '/api/wall-e/update/prompt-event' && m === 'POST') {
359
+ return readBody(req).then(body => {
360
+ try {
361
+ const allowed = new Set(['shown', 'later', 'dismissed', 'copy_command']);
362
+ const action = allowed.has(body?.action) ? body.action : 'unknown';
363
+ const updateRaw = brain.getKv('update_available');
364
+ const update = updateRaw ? JSON.parse(updateRaw) : {};
365
+ const telemetry = require('./telemetry');
366
+ telemetry.track('upgrade_prompt', {
367
+ action,
368
+ current: update.current || 'unknown',
369
+ latest: update.latest || 'unknown',
370
+ source: 'walle_ui',
371
+ });
372
+ if (action === 'shown') telemetry.trackFunnelStep('upgrade_prompt_shown');
373
+ if (action === 'dismissed') telemetry.trackFunnelStep('upgrade_prompt_dismissed');
374
+ if (action === 'copy_command') telemetry.trackFunnelStep('upgrade_prompt_command_copied');
375
+ return jsonResponse(res, { ok: true }), true;
376
+ } catch (e) {
377
+ return jsonResponse(res, { error: e.message }, 500), true;
378
+ }
379
+ }).catch(e => jsonResponse(res, { error: e.message }, 500));
380
+ }
381
+
308
382
  // DELETE /api/wall-e/alerts/:id — dismiss an alert
309
383
  if (p.startsWith('/api/wall-e/alerts/') && m === 'DELETE') {
310
384
  try {
311
385
  const alertId = decodeURIComponent(p.split('/api/wall-e/alerts/')[1]);
312
386
  if (alertId === 'version_update') {
313
- const brain = require('./brain');
387
+ try {
388
+ const telemetry = require('./telemetry');
389
+ const updateRaw = brain.getKv('update_available');
390
+ const update = updateRaw ? JSON.parse(updateRaw) : {};
391
+ telemetry.track('upgrade_prompt', {
392
+ action: 'dismissed',
393
+ current: update.current || 'unknown',
394
+ latest: update.latest || 'unknown',
395
+ source: 'alert_delete',
396
+ });
397
+ telemetry.trackFunnelStep('upgrade_prompt_dismissed');
398
+ } catch {}
314
399
  brain.setKv('update_available', '');
315
400
  } else {
316
401
  const { dismissServiceAlert } = require('./skills/skill-planner');
@@ -362,14 +447,68 @@ function handleWalleApi(req, res, url) {
362
447
 
363
448
  // GET /api/wall-e/slack/status — check Slack OAuth status
364
449
  if (p === '/api/wall-e/slack/status' && m === 'GET') {
450
+ (async () => {
451
+ try {
452
+ const slackMcp = require('./tools/slack-mcp');
453
+ const { getSlackOwnerRepairState } = require('./tools/slack-owner');
454
+ const { clearResolvedSlackHealthAlerts } = require('./skills/skill-planner');
455
+ const { getSlackSocketModeStatus } = require('./slack/socket-mode-listener');
456
+ const authenticated = await slackMcp.isAuthenticated();
457
+ const token = slackMcp.loadToken();
458
+ const owner = getSlackOwnerRepairState();
459
+ let clearedAlerts = 0;
460
+ if (authenticated) {
461
+ try {
462
+ const cleared = clearResolvedSlackHealthAlerts({
463
+ authenticated: true,
464
+ ownerConfigured: owner.configured,
465
+ });
466
+ clearedAlerts = cleared.cleared || 0;
467
+ } catch {}
468
+ }
469
+ jsonResponse(res, {
470
+ data: {
471
+ authenticated,
472
+ team: token?.team_name,
473
+ user: token?.user_id,
474
+ obtained_at: token?.obtained_at,
475
+ owner_configured: owner.configured,
476
+ owner_can_repair: owner.canRepair,
477
+ socket_mode: getSlackSocketModeStatus(),
478
+ cleared_stale_alerts: clearedAlerts,
479
+ },
480
+ });
481
+ } catch (e) {
482
+ jsonResponse(res, { data: { authenticated: false, error: e.message } });
483
+ }
484
+ })();
485
+ return true;
486
+ }
487
+
488
+ // POST /api/wall-e/slack/repair-owner — derive Slack owner id from OAuth token
489
+ if (p === '/api/wall-e/slack/repair-owner' && m === 'POST') {
365
490
  try {
366
- const slackMcp = require('./tools/slack-mcp');
367
- const token = slackMcp.loadToken();
368
- jsonResponse(res, { data: { authenticated: !!token?.access_token, team: token?.team_name, user: token?.user_id, obtained_at: token?.obtained_at } });
491
+ const { repairSlackOwnerIdentity } = require('./tools/slack-owner');
492
+ const { clearServiceAlerts } = require('./skills/skill-planner');
493
+ const result = repairSlackOwnerIdentity({ persist: true });
494
+ if (!result.ok) {
495
+ return jsonResponse(res, {
496
+ ok: false,
497
+ error: result.error || 'Could not repair Slack owner identity',
498
+ needsSlackAuth: !!result.needsSlackAuth,
499
+ }, result.needsSlackAuth ? 409 : 500), true;
500
+ }
501
+ clearServiceAlerts('slack');
502
+ return jsonResponse(res, {
503
+ ok: true,
504
+ user_id_configured: true,
505
+ source: result.source,
506
+ persisted: !!result.persisted,
507
+ already_configured: !!result.alreadyConfigured,
508
+ }), true;
369
509
  } catch (e) {
370
- jsonResponse(res, { data: { authenticated: false } });
510
+ return jsonResponse(res, { ok: false, error: e.message }, 500), true;
371
511
  }
372
- return true;
373
512
  }
374
513
 
375
514
  // POST /api/wall-e/slack/auth — start OAuth flow (opens browser)
@@ -378,11 +517,27 @@ function handleWalleApi(req, res, url) {
378
517
  const slackMcp = require('./tools/slack-mcp');
379
518
  // If already authenticated, return immediately
380
519
  if (slackMcp.isAuthenticatedSync()) {
520
+ try {
521
+ const { repairSlackOwnerIdentity } = require('./tools/slack-owner');
522
+ const { clearServiceAlerts } = require('./skills/skill-planner');
523
+ const repaired = repairSlackOwnerIdentity({ persist: true });
524
+ if (repaired.ok) clearServiceAlerts('slack');
525
+ } catch (repairErr) {
526
+ console.warn('[wall-e] Slack owner repair skipped:', repairErr.message);
527
+ }
381
528
  jsonResponse(res, { ok: true, already: true });
382
529
  return true;
383
530
  }
384
531
  // Start OAuth — opens browser, temp server on port 3118 handles callback
385
532
  slackMcp.authenticate().then(() => {
533
+ try {
534
+ const { repairSlackOwnerIdentity } = require('./tools/slack-owner');
535
+ const { clearServiceAlerts } = require('./skills/skill-planner');
536
+ const repaired = repairSlackOwnerIdentity({ persist: true });
537
+ if (repaired.ok) clearServiceAlerts('slack');
538
+ } catch (repairErr) {
539
+ console.error('[wall-e] Slack owner repair failed:', repairErr.message);
540
+ }
386
541
  console.log('[wall-e] Slack OAuth completed');
387
542
  }).catch(err => {
388
543
  console.error('[wall-e] Slack OAuth failed:', err.message);
@@ -714,24 +869,9 @@ function handleWalleApi(req, res, url) {
714
869
  // GET /api/wall-e/mcp/integrations — check which AI tools have Wall-E MCP configured
715
870
  if (p === '/api/wall-e/mcp/integrations' && m === 'GET') {
716
871
  try {
717
- const fs = require('fs');
718
- const { MCP_TARGETS } = require('../create-walle/bin/mcp-inject');
872
+ const { detectMcpIntegrations } = require('./lib/mcp-integration');
719
873
  const wallePort = parseInt(process.env.WALL_E_PORT) || 3457;
720
- const home = process.env.HOME;
721
- const results = MCP_TARGETS.map(target => {
722
- const detectPath = path.join(home, target.detectDir);
723
- const configPath = path.join(home, target.configPath);
724
- if (!fs.existsSync(detectPath)) return { tool: target.tool, status: 'not_installed' };
725
- try {
726
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
727
- const entry = config?.mcpServers?.['wall-e'];
728
- if (entry && entry.url === `http://localhost:${wallePort}/mcp`) return { tool: target.tool, status: 'configured', configPath };
729
- if (entry) return { tool: target.tool, status: 'wrong_port', configPath };
730
- return { tool: target.tool, status: 'not_configured', configPath };
731
- } catch {
732
- return { tool: target.tool, status: 'not_configured', configPath };
733
- }
734
- });
874
+ const results = detectMcpIntegrations(wallePort);
735
875
  jsonResponse(res, { data: results, wallePort });
736
876
  } catch (e) {
737
877
  jsonResponse(res, { data: [], error: e.message });
@@ -742,11 +882,12 @@ function handleWalleApi(req, res, url) {
742
882
  // POST /api/wall-e/mcp/inject — run MCP config injection for all detected AI tools
743
883
  if (p === '/api/wall-e/mcp/inject' && m === 'POST') {
744
884
  try {
745
- const { injectMcpConfigs } = require('../create-walle/bin/mcp-inject');
885
+ const { ensureMcpIntegrations } = require('./lib/mcp-integration');
746
886
  const wallePort = parseInt(process.env.WALL_E_PORT) || 3457;
747
- const results = injectMcpConfigs(wallePort);
748
- const added = results.filter(r => r.action === 'added' || r.action === 'updated').length;
749
- try { require('./telemetry').track('mcp_inject', { added, total: results.length }); } catch {}
887
+ const results = ensureMcpIntegrations(wallePort);
888
+ const added = results.filter(r => r.kind === 'mcp_config' && (r.action === 'added' || r.action === 'updated')).length;
889
+ const instructions = results.filter(r => r.kind === 'agent_instructions' && (r.action === 'added' || r.action === 'updated')).length;
890
+ try { require('./telemetry').track('mcp_inject', { added, instructions, total: results.length }); } catch {}
750
891
  jsonResponse(res, { ok: true, results });
751
892
  } catch (e) {
752
893
  jsonResponse(res, { ok: false, error: e.message });
@@ -754,6 +895,20 @@ function handleWalleApi(req, res, url) {
754
895
  return true;
755
896
  }
756
897
 
898
+ // GET /api/wall-e/mcp/test - verify the live Wall-E MCP endpoint responds
899
+ if (p === '/api/wall-e/mcp/test' && m === 'GET') {
900
+ try {
901
+ const { testWallEMcpEndpoint } = require('./lib/mcp-integration');
902
+ const wallePort = parseInt(process.env.WALL_E_PORT) || 3457;
903
+ testWallEMcpEndpoint(wallePort, { timeoutMs: 1500 })
904
+ .then(result => jsonResponse(res, { data: result, wallePort }))
905
+ .catch(e => jsonResponse(res, { data: { ok: false, error: e.message }, wallePort }, 500));
906
+ } catch (e) {
907
+ jsonResponse(res, { data: { ok: false, error: e.message } }, 500);
908
+ }
909
+ return true;
910
+ }
911
+
757
912
  // GET /api/wall-e/status
758
913
  if (p === '/api/wall-e/status' && m === 'GET') {
759
914
  const result = getStatus();
@@ -862,6 +1017,12 @@ function handleWalleApi(req, res, url) {
862
1017
  serviceAlerts = getServiceAlerts({ includeVersionUpdate: true });
863
1018
  } catch {}
864
1019
 
1020
+ let slackSocketMode = null;
1021
+ try {
1022
+ const { getSlackSocketModeStatus } = require('./slack/socket-mode-listener');
1023
+ slackSocketMode = getSlackSocketModeStatus();
1024
+ } catch {}
1025
+
865
1026
  return jsonResponse(res, {
866
1027
  data: {
867
1028
  ...result,
@@ -878,6 +1039,7 @@ function handleWalleApi(req, res, url) {
878
1039
  walle_model: walleModel,
879
1040
  has_api_key: hasApiKey,
880
1041
  service_alerts: serviceAlerts,
1042
+ slack_socket_mode: slackSocketMode,
881
1043
  }
882
1044
  }), true;
883
1045
  }
@@ -964,32 +1126,49 @@ function handleWalleApi(req, res, url) {
964
1126
  const providerConfig = body.providerConfig || null;
965
1127
 
966
1128
  if (provider) {
967
- brain.setKv('walle_provider', provider);
968
- process.env.WALLE_PROVIDER = provider;
969
- }
970
- if (model) {
1129
+ if (typeof brain.setDefaultProviderSelection === 'function') {
1130
+ brain.setDefaultProviderSelection({ type: provider, requestedModel: model, targetModel: model || brain.getKv('walle_model') || '' });
1131
+ } else {
1132
+ brain.setKv('walle_provider', provider);
1133
+ process.env.WALLE_PROVIDER = provider;
1134
+ if (model) {
1135
+ brain.setKv('walle_model', model);
1136
+ process.env.WALLE_MODEL = model;
1137
+ }
1138
+ }
1139
+ } else if (model) {
971
1140
  brain.setKv('walle_model', model);
972
- process.env.WALLE_MODEL = model;
973
1141
  }
974
1142
  if (ownerName) brain.setKv('walle_owner_name', ownerName);
975
1143
 
976
1144
  if (provider && providerConfig) {
977
- const providerNames = { anthropic: 'Anthropic', openai: 'OpenAI', google: 'Google Gemini', deepseek: 'DeepSeek', ollama: 'Ollama (Local)', mlx: 'MLX (Local)' };
978
- brain.upsertModelProvider({
979
- id: providerConfig.id || `${provider}-default`,
980
- name: providerConfig.name || providerNames[provider] || provider,
981
- type: provider,
982
- baseUrl: providerConfig.baseUrl || null,
983
- apiKeyEncrypted: providerConfig.apiKeyEncrypted || null,
984
- customHeaders: providerConfig.customHeaders || null,
985
- enabled: true,
986
- });
1145
+ if (typeof brain.saveSetupProvider === 'function') {
1146
+ brain.saveSetupProvider({
1147
+ id: providerConfig.id || `${provider}-default`,
1148
+ name: providerConfig.name || SETUP_PROVIDER_NAMES[provider] || provider,
1149
+ type: provider,
1150
+ baseUrl: providerConfig.baseUrl || null,
1151
+ apiKeyEncrypted: providerConfig.apiKeyEncrypted || null,
1152
+ customHeaders: providerConfig.customHeaders || null,
1153
+ enabled: true,
1154
+ model,
1155
+ setDefault: false,
1156
+ });
1157
+ } else {
1158
+ brain.upsertModelProvider({
1159
+ id: providerConfig.id || `${provider}-default`,
1160
+ name: providerConfig.name || SETUP_PROVIDER_NAMES[provider] || provider,
1161
+ type: provider,
1162
+ baseUrl: providerConfig.baseUrl || null,
1163
+ apiKeyEncrypted: providerConfig.apiKeyEncrypted || null,
1164
+ customHeaders: providerConfig.customHeaders || null,
1165
+ enabled: true,
1166
+ });
1167
+ }
987
1168
  }
988
1169
 
989
- try {
990
- const { resetDefaultClient } = require('./llm/client');
991
- resetDefaultClient();
992
- } catch {}
1170
+ syncRuntimeDefaultsFromBrain();
1171
+ resetDefaultClientQuiet();
993
1172
 
994
1173
  jsonResponse(res, { ok: true });
995
1174
  } catch (e) {
@@ -999,6 +1178,88 @@ function handleWalleApi(req, res, url) {
999
1178
  return true;
1000
1179
  }
1001
1180
 
1181
+ if (p === '/api/wall-e/setup/provider' && m === 'POST') {
1182
+ readBody(req).then((body) => {
1183
+ try {
1184
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { ok: false, error: 'Brain module not available' }, 503);
1185
+ const type = sanitizeSetupProviderType(body.type);
1186
+ if (!type) return jsonResponse(res, { ok: false, error: 'Invalid provider type' }, 400);
1187
+ const model = sanitizeSetupModel(body.model);
1188
+ const result = brain.saveSetupProvider({
1189
+ id: body.id || `${type}-default`,
1190
+ name: body.name || SETUP_PROVIDER_NAMES[type] || type,
1191
+ type,
1192
+ baseUrl: body.base_url || body.baseUrl || null,
1193
+ apiKeyEncrypted: typeof body.api_key === 'string' ? body.api_key.replace(/[\r\n\s]/g, '').slice(0, 200) : (body.apiKeyEncrypted || null),
1194
+ customHeaders: body.custom_headers || body.customHeaders || null,
1195
+ enabled: body.enabled !== false,
1196
+ model,
1197
+ setDefault: Boolean(body.set_default),
1198
+ authMethod: body.auth_method || null,
1199
+ });
1200
+ syncRuntimeDefaultsFromBrain();
1201
+ resetDefaultClientQuiet();
1202
+ jsonResponse(res, { ok: true, authority: 'wall-e', ...result });
1203
+ } catch (e) {
1204
+ jsonResponse(res, { ok: false, error: e.message }, 400);
1205
+ }
1206
+ }).catch((e) => jsonResponse(res, { ok: false, error: e.message }, 400));
1207
+ return true;
1208
+ }
1209
+
1210
+ if (p === '/api/wall-e/setup/default-provider' && m === 'POST') {
1211
+ readBody(req).then((body) => {
1212
+ try {
1213
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { ok: false, error: 'Brain module not available' }, 503);
1214
+ const type = sanitizeSetupProviderType(body.type);
1215
+ if (!type) return jsonResponse(res, { ok: false, error: 'Invalid provider type' }, 400);
1216
+ const requestedModel = sanitizeSetupModel(body.requested_model ?? body.requestedModel ?? body.model);
1217
+ const targetModel = sanitizeSetupModel(body.target_model ?? body.targetModel ?? body.model);
1218
+ const result = brain.setDefaultProviderSelection({ type, requestedModel, targetModel });
1219
+ syncRuntimeDefaultsFromBrain();
1220
+ resetDefaultClientQuiet();
1221
+ jsonResponse(res, { ok: true, authority: 'wall-e', ...result });
1222
+ } catch (e) {
1223
+ jsonResponse(res, { ok: false, error: e.message }, 400);
1224
+ }
1225
+ }).catch((e) => jsonResponse(res, { ok: false, error: e.message }, 400));
1226
+ return true;
1227
+ }
1228
+
1229
+ const setupProviderDelete = p.match(/^\/api\/wall-e\/setup\/provider\/([a-z0-9_-]+)$/);
1230
+ if (setupProviderDelete && m === 'DELETE') {
1231
+ try {
1232
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { ok: false, error: 'Brain module not available' }, 503), true;
1233
+ const type = sanitizeSetupProviderType(setupProviderDelete[1]);
1234
+ if (!type) return jsonResponse(res, { ok: false, error: 'Invalid provider type' }, 400), true;
1235
+ const result = brain.disableModelProviderByType(type);
1236
+ syncRuntimeDefaultsFromBrain();
1237
+ resetDefaultClientQuiet();
1238
+ jsonResponse(res, { ok: true, authority: 'wall-e', ...result });
1239
+ } catch (e) {
1240
+ jsonResponse(res, { ok: false, error: e.message }, 400);
1241
+ }
1242
+ return true;
1243
+ }
1244
+
1245
+ if (p === '/api/wall-e/setup/task-default' && m === 'POST') {
1246
+ readBody(req).then((body) => {
1247
+ try {
1248
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { ok: false, error: 'Brain module not available' }, 503);
1249
+ const taskType = sanitizeTaskType(body.task_type);
1250
+ if (!taskType) return jsonResponse(res, { ok: false, error: 'task_type required (alphanumeric/underscore/hyphen)' }, 400);
1251
+ const modelRegistryId = sanitizeModelRegistryId(body.model_registry_id);
1252
+ const strategy = typeof body.strategy === 'string' && /^(single|quorum)$/.test(body.strategy) ? body.strategy : 'single';
1253
+ const quorumSize = Number.isInteger(body.quorum_size) && body.quorum_size > 0 && body.quorum_size <= 10 ? body.quorum_size : 1;
1254
+ const result = brain.setTaskModelDefault({ taskType, modelRegistryId, strategy, quorumSize });
1255
+ jsonResponse(res, { ok: true, authority: 'wall-e', ...result });
1256
+ } catch (e) {
1257
+ jsonResponse(res, { ok: false, error: e.message }, 400);
1258
+ }
1259
+ }).catch((e) => jsonResponse(res, { ok: false, error: e.message }, 400));
1260
+ return true;
1261
+ }
1262
+
1002
1263
  if (p === '/api/wall-e/prompt-embeddings/upsert' && m === 'POST') {
1003
1264
  readBody(req).then(async (body) => {
1004
1265
  try {
@@ -3849,7 +4110,11 @@ function handleWalleApi(req, res, url) {
3849
4110
  if (!tdb) return jsonResponse(res, { error: 'Telemetry not enabled' }, 404), true;
3850
4111
  // Basic rate limiting: max 60 requests per minute per IP
3851
4112
  const clientIpRL = (req.headers['x-forwarded-for'] || '').split(',')[0].trim() || req.socket?.remoteAddress || 'unknown';
3852
- if (!handleWalleApi._rlMap) { handleWalleApi._rlMap = new Map(); setInterval(() => handleWalleApi._rlMap.clear(), 60000); }
4113
+ if (!handleWalleApi._rlMap) {
4114
+ handleWalleApi._rlMap = new Map();
4115
+ handleWalleApi._rlTimer = setInterval(() => handleWalleApi._rlMap.clear(), 60000);
4116
+ if (typeof handleWalleApi._rlTimer.unref === 'function') handleWalleApi._rlTimer.unref();
4117
+ }
3853
4118
  const rlCount = (handleWalleApi._rlMap.get(clientIpRL) || 0) + 1;
3854
4119
  handleWalleApi._rlMap.set(clientIpRL, rlCount);
3855
4120
  if (rlCount > 60) return jsonResponse(res, { error: 'Rate limited' }, 429), true;
@@ -3860,25 +4125,29 @@ function handleWalleApi(req, res, url) {
3860
4125
  // Capture client IP from request (supports reverse proxy headers)
3861
4126
  const clientIp = (req.headers['x-forwarded-for'] || '').split(',')[0].trim()
3862
4127
  || req.socket?.remoteAddress || '';
4128
+ const machineBucket = typeof body.machine === 'string' && /^[a-f0-9]{12,64}$/i.test(body.machine)
4129
+ ? body.machine.toLowerCase().slice(0, 64)
4130
+ : null;
3863
4131
  const insertEvent = tdb.prepare(
3864
4132
  'INSERT INTO telemetry_events (install_id, version, os, node_version, event, meta, client_ts, ip) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
3865
4133
  );
3866
4134
  const upsertInstall = tdb.prepare(`
3867
- INSERT INTO telemetry_installs (install_id, version, os, node_version, ip, event_count)
3868
- VALUES (?, ?, ?, ?, ?, ?)
4135
+ INSERT INTO telemetry_installs (install_id, version, os, node_version, ip, machine_bucket, event_count)
4136
+ VALUES (?, ?, ?, ?, ?, ?, ?)
3869
4137
  ON CONFLICT(install_id) DO UPDATE SET
3870
4138
  last_seen = datetime('now'),
3871
4139
  version = excluded.version,
3872
4140
  os = excluded.os,
3873
4141
  node_version = excluded.node_version,
3874
4142
  ip = excluded.ip,
4143
+ machine_bucket = COALESCE(excluded.machine_bucket, telemetry_installs.machine_bucket),
3875
4144
  event_count = telemetry_installs.event_count + excluded.event_count
3876
4145
  `);
3877
4146
  const runBatch = tdb.transaction((events) => {
3878
4147
  for (const ev of events) {
3879
4148
  insertEvent.run(body.id, body.v, body.os, body.node, ev.e, JSON.stringify(ev.m || {}), ev.t, clientIp);
3880
4149
  }
3881
- upsertInstall.run(body.id, body.v, body.os, body.node, clientIp, events.length);
4150
+ upsertInstall.run(body.id, body.v, body.os, body.node, clientIp, machineBucket, events.length);
3882
4151
  });
3883
4152
  runBatch(body.events.slice(0, 500)); // cap at 500 events per request
3884
4153
  jsonResponse(res, { ok: true, received: Math.min(body.events.length, 500) });
@@ -3899,6 +4168,12 @@ function handleWalleApi(req, res, url) {
3899
4168
  const summary = {
3900
4169
  total_installs: tdb.prepare('SELECT count(*) as cnt FROM telemetry_installs').get().cnt,
3901
4170
  active_installs: tdb.prepare('SELECT count(*) as cnt FROM telemetry_installs WHERE last_seen >= ?').get(since).cnt,
4171
+ unique_machines: tdb.prepare(
4172
+ "SELECT count(DISTINCT machine_bucket) as cnt FROM telemetry_installs WHERE machine_bucket IS NOT NULL AND machine_bucket != ''"
4173
+ ).get().cnt,
4174
+ active_machines: tdb.prepare(
4175
+ "SELECT count(DISTINCT machine_bucket) as cnt FROM telemetry_installs WHERE last_seen >= ? AND machine_bucket IS NOT NULL AND machine_bucket != ''"
4176
+ ).get(since).cnt,
3902
4177
  total_events: tdb.prepare('SELECT count(*) as cnt FROM telemetry_events WHERE received_at >= ?').get(since).cnt,
3903
4178
  events_by_type: tdb.prepare(
3904
4179
  'SELECT event, count(*) as cnt FROM telemetry_events WHERE received_at >= ? GROUP BY event ORDER BY cnt DESC LIMIT 50'
@@ -3922,6 +4197,9 @@ function handleWalleApi(req, res, url) {
3922
4197
  "SELECT date(received_at) as day, count(DISTINCT install_id) as users, count(*) as events FROM telemetry_events WHERE received_at >= ? GROUP BY day ORDER BY day DESC LIMIT ?"
3923
4198
  ).all(since, days),
3924
4199
  };
4200
+ summary.install_machine_ratio = summary.unique_machines > 0
4201
+ ? Math.round(summary.total_installs / summary.unique_machines * 100) / 100
4202
+ : null;
3925
4203
 
3926
4204
  // Compat lifecycle data (Phase 4)
3927
4205
  try {