create-walle 0.9.11 → 0.9.13

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 (167) hide show
  1. package/README.md +3 -3
  2. package/package.json +2 -2
  3. package/template/bin/dev.sh +7 -1
  4. package/template/bin/setup.js +53 -9
  5. package/template/bin/sync-images.js +53 -0
  6. package/template/builder-journal.md +17 -0
  7. package/template/claude-task-manager/api-prompts.js +98 -13
  8. package/template/claude-task-manager/api-reviews.js +82 -5
  9. package/template/claude-task-manager/db.js +32 -5
  10. package/template/claude-task-manager/docs/session-capture-foundation-design.md +1273 -0
  11. package/template/claude-task-manager/lib/claude-desktop-sessions.js +696 -0
  12. package/template/claude-task-manager/lib/coding-agent-models.js +49 -1
  13. package/template/claude-task-manager/lib/session-capture.js +421 -0
  14. package/template/claude-task-manager/lib/session-history.js +135 -15
  15. package/template/claude-task-manager/lib/session-jobs.js +10 -5
  16. package/template/claude-task-manager/lib/session-stream.js +87 -19
  17. package/template/claude-task-manager/lib/setup-provider-config.js +115 -0
  18. package/template/claude-task-manager/lib/walle-ctm-history.js +72 -0
  19. package/template/claude-task-manager/lib/walle-session-context.js +61 -0
  20. package/template/claude-task-manager/lib/walle-transcript.js +176 -0
  21. package/template/claude-task-manager/public/css/setup.css +35 -8
  22. package/template/claude-task-manager/public/css/walle-session.css +56 -0
  23. package/template/claude-task-manager/public/css/walle.css +120 -0
  24. package/template/claude-task-manager/public/index.html +814 -181
  25. package/template/claude-task-manager/public/js/message-renderer.js +148 -19
  26. package/template/claude-task-manager/public/js/reviews.js +120 -62
  27. package/template/claude-task-manager/public/js/setup.js +75 -31
  28. package/template/claude-task-manager/public/js/stream-view.js +115 -55
  29. package/template/claude-task-manager/public/js/walle-session.js +84 -2
  30. package/template/claude-task-manager/public/js/walle.js +308 -54
  31. package/template/claude-task-manager/server.js +1092 -146
  32. package/template/claude-task-manager/session-integrity.js +181 -54
  33. package/template/claude-task-manager/session-utils.js +123 -41
  34. package/template/claude-task-manager/workers/state-detectors/codex.js +5 -2
  35. package/template/package.json +1 -1
  36. package/template/wall-e/adapters/ctm.js +39 -18
  37. package/template/wall-e/agent-runners/contract.js +17 -0
  38. package/template/wall-e/agent-runners/index.js +22 -0
  39. package/template/wall-e/agent-runtime/harness.js +212 -0
  40. package/template/wall-e/agent-runtime/index.js +8 -0
  41. package/template/wall-e/agent-runtime/registry.js +67 -0
  42. package/template/wall-e/agent-runtime/session-store.js +179 -0
  43. package/template/wall-e/agent-runtime/spawn.js +208 -0
  44. package/template/wall-e/api-walle.js +174 -7
  45. package/template/wall-e/brain.js +266 -28
  46. package/template/wall-e/channels/policy.js +88 -0
  47. package/template/wall-e/channels/registry.js +15 -1
  48. package/template/wall-e/channels/reply-dispatcher.js +70 -0
  49. package/template/wall-e/channels/session-bindings.js +51 -0
  50. package/template/wall-e/chat/code-review-context.js +29 -0
  51. package/template/wall-e/chat.js +188 -42
  52. package/template/wall-e/coding/acp-adapter.js +188 -0
  53. package/template/wall-e/coding/agent-catalog.js +129 -0
  54. package/template/wall-e/coding/compaction-service.js +247 -0
  55. package/template/wall-e/coding/execution-trace.js +3 -0
  56. package/template/wall-e/coding/instruction-service.js +224 -0
  57. package/template/wall-e/coding/model-message.js +67 -0
  58. package/template/wall-e/coding/permission-rules-store.js +111 -0
  59. package/template/wall-e/coding/permission-service.js +266 -0
  60. package/template/wall-e/coding/prompt-bundle.js +67 -0
  61. package/template/wall-e/coding/prompt-runtime.js +243 -0
  62. package/template/wall-e/coding/provider-transform.js +188 -0
  63. package/template/wall-e/coding/runtime-mode.js +132 -0
  64. package/template/wall-e/coding/snapshot-service.js +155 -0
  65. package/template/wall-e/coding/stream-processor.js +268 -0
  66. package/template/wall-e/coding/task-tool.js +255 -0
  67. package/template/wall-e/coding/tool-registry.js +361 -0
  68. package/template/wall-e/coding/transcript-writer.js +143 -0
  69. package/template/wall-e/coding/workspace-replay.js +324 -0
  70. package/template/wall-e/coding-context.js +4 -22
  71. package/template/wall-e/coding-orchestrator.js +307 -18
  72. package/template/wall-e/coding-prompts.js +44 -3
  73. package/template/wall-e/context/context-builder.js +43 -1
  74. package/template/wall-e/context/topic-matcher.js +1 -1
  75. package/template/wall-e/eval/agent-runner.js +59 -13
  76. package/template/wall-e/eval/benchmarks/memory-retrieval.json +155 -57
  77. package/template/wall-e/eval/benchmarks.js +100 -16
  78. package/template/wall-e/eval/eval-orchestrator.js +218 -8
  79. package/template/wall-e/eval/harvester.js +62 -5
  80. package/template/wall-e/eval/head-to-head.js +23 -2
  81. package/template/wall-e/eval/humaneval-adapter.js +30 -5
  82. package/template/wall-e/eval/livecodebench-adapter.js +29 -5
  83. package/template/wall-e/eval/manifest.js +186 -0
  84. package/template/wall-e/eval/run-agent-benchmarks.js +66 -2
  85. package/template/wall-e/eval/session-retrieval-benchmark.js +150 -0
  86. package/template/wall-e/eval/session-transcripts.js +57 -4
  87. package/template/wall-e/eval/swebench-adapter.js +109 -3
  88. package/template/wall-e/evaluation/agent-router.js +53 -1
  89. package/template/wall-e/evaluation/coding-quorum.js +48 -1
  90. package/template/wall-e/evaluation/router.js +4 -2
  91. package/template/wall-e/evaluation/tier-selector.js +11 -1
  92. package/template/wall-e/extraction/contradiction.js +2 -2
  93. package/template/wall-e/extraction/indexer.js +2 -1
  94. package/template/wall-e/extraction/knowledge-extractor.js +2 -2
  95. package/template/wall-e/hooks/cli.js +92 -0
  96. package/template/wall-e/hooks/discovery.js +119 -0
  97. package/template/wall-e/hooks/index.js +7 -0
  98. package/template/wall-e/hooks/manifest.js +55 -0
  99. package/template/wall-e/hooks/runtime.js +84 -0
  100. package/template/wall-e/hooks/session-memory.js +225 -0
  101. package/template/wall-e/http/auth.js +6 -2
  102. package/template/wall-e/http/chat-api.js +54 -8
  103. package/template/wall-e/integrations/claude-plugin/hooks/hooks.json +27 -0
  104. package/template/wall-e/integrations/claude-plugin/hooks/walle-precompact-hook.sh +5 -0
  105. package/template/wall-e/integrations/claude-plugin/hooks/walle-stop-hook.sh +5 -0
  106. package/template/wall-e/integrations/codex-plugin/hooks/walle-hook.sh +7 -0
  107. package/template/wall-e/integrations/codex-plugin/hooks.json +37 -0
  108. package/template/wall-e/listening/calendar.js +3 -1
  109. package/template/wall-e/llm/client.js +64 -10
  110. package/template/wall-e/llm/google.js +39 -5
  111. package/template/wall-e/llm/ollama.js +1 -1
  112. package/template/wall-e/llm/ollama.plugin.json +1 -1
  113. package/template/wall-e/llm/provider-availability.js +10 -0
  114. package/template/wall-e/llm/provider-error.js +269 -0
  115. package/template/wall-e/llm/tool-adapter.js +48 -12
  116. package/template/wall-e/loops/boot.js +2 -1
  117. package/template/wall-e/loops/initiative.js +2 -2
  118. package/template/wall-e/loops/tasks.js +8 -47
  119. package/template/wall-e/loops/workspace-prompts.js +20 -0
  120. package/template/wall-e/mcp-server.js +442 -1
  121. package/template/wall-e/memory/session-ingest-service.js +159 -0
  122. package/template/wall-e/memory/source-indexer.js +289 -0
  123. package/template/wall-e/plugins/discovery.js +83 -0
  124. package/template/wall-e/plugins/manifest-loader.js +50 -10
  125. package/template/wall-e/plugins/manifest-schema.js +69 -0
  126. package/template/wall-e/plugins/model-catalog.js +55 -0
  127. package/template/wall-e/prompts/coding/base.txt +2 -0
  128. package/template/wall-e/prompts/coding/deepseek.txt +1 -0
  129. package/template/wall-e/prompts/coding/memory-protocol.md +9 -0
  130. package/template/wall-e/prompts/coding/plan.txt +1 -0
  131. package/template/wall-e/runtime/execution-trace.js +220 -0
  132. package/template/wall-e/security/audit.js +266 -0
  133. package/template/wall-e/security/ssrf.js +236 -0
  134. package/template/wall-e/session-files.js +303 -0
  135. package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +3 -0
  136. package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +3 -0
  137. package/template/wall-e/skills/internal-skill-registry.js +2 -2
  138. package/template/wall-e/skills/script-skill-runner.js +143 -0
  139. package/template/wall-e/skills/skill-executor.js +5 -6
  140. package/template/wall-e/skills/skill-fallback.js +3 -1
  141. package/template/wall-e/skills/skill-harness-registry.js +7 -8
  142. package/template/wall-e/skills/skill-planner.js +52 -4
  143. package/template/wall-e/skills/slack-ingest.js +11 -3
  144. package/template/wall-e/sources/base.js +90 -0
  145. package/template/wall-e/sources/builtin.js +33 -0
  146. package/template/wall-e/sources/claude-code-jsonl.js +78 -0
  147. package/template/wall-e/sources/codex-jsonl.js +125 -0
  148. package/template/wall-e/sources/coding-session-utils.js +117 -0
  149. package/template/wall-e/sources/contract-suite.js +59 -0
  150. package/template/wall-e/sources/gemini-jsonl.js +85 -0
  151. package/template/wall-e/sources/index.js +9 -0
  152. package/template/wall-e/sources/jsonl-utils.js +181 -0
  153. package/template/wall-e/sources/record-types.js +252 -0
  154. package/template/wall-e/sources/registry.js +92 -0
  155. package/template/wall-e/sources/transforms.js +100 -0
  156. package/template/wall-e/sources/walle-jsonl.js +108 -0
  157. package/template/wall-e/tools/coding-middleware.js +31 -1
  158. package/template/wall-e/tools/file-tracker.js +25 -1
  159. package/template/wall-e/tools/local-tools.js +75 -47
  160. package/template/wall-e/tools/session-sharing.js +68 -1
  161. package/template/wall-e/tools/shell-analyzer.js +1 -1
  162. package/template/wall-e/tools/shell-policy.js +47 -0
  163. package/template/wall-e/tools/snapshot.js +42 -0
  164. package/template/wall-e/training/harvester.js +62 -5
  165. package/template/wall-e/utils/repair.js +253 -1
  166. package/template/website/index.html +3 -3
  167. package/template/wall-e/skills/_bundled/slack-mentions/.watched-threads.json +0 -18
@@ -0,0 +1,225 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const crypto = require('node:crypto');
7
+ const { spawn } = require('node:child_process');
8
+
9
+ const { createSessionIngestService } = require('../memory/session-ingest-service');
10
+ const { collectIngestRecords } = require('../sources/base');
11
+ const { ensureBuiltinSourceAdapters } = require('../sources/builtin');
12
+ const sourceRegistry = require('../sources/registry');
13
+
14
+ const SAVE_INTERVAL = 15;
15
+ const SUPPORTED_HARNESSES = new Set(['claude-code', 'codex', 'walle']);
16
+
17
+ function getHookStateDir() {
18
+ return process.env.WALL_E_HOOK_STATE_DIR || path.join(os.homedir(), '.walle', 'hook_state');
19
+ }
20
+
21
+ function parseHarnessInput(data = {}, harness = 'walle') {
22
+ if (!SUPPORTED_HARNESSES.has(harness)) throw new Error(`Unsupported harness: ${harness}`);
23
+ const transcriptPath = data.transcript_path
24
+ || data.transcriptPath
25
+ || data.transcript
26
+ || data.session_file
27
+ || data.sessionFile
28
+ || data.path
29
+ || data.file
30
+ || '';
31
+ const cwd = data.cwd || data.workspace || data.project_dir || data.projectDir || '';
32
+ return {
33
+ harness,
34
+ transcriptPath,
35
+ cwd,
36
+ sessionId: data.session_id || data.sessionId || data.id || '',
37
+ stopHookActive: Boolean(data.stop_hook_active || data.stopHookActive),
38
+ };
39
+ }
40
+
41
+ function validateTranscriptPath(transcriptPath) {
42
+ if (!transcriptPath || typeof transcriptPath !== 'string') {
43
+ throw new Error('transcript path is required');
44
+ }
45
+ if (transcriptPath.split(/[\\/]+/).includes('..')) {
46
+ throw new Error('transcript path must not contain traversal segments');
47
+ }
48
+ if (!transcriptPath.endsWith('.jsonl')) {
49
+ throw new Error('transcript path must end in .jsonl');
50
+ }
51
+ return path.resolve(transcriptPath);
52
+ }
53
+
54
+ function adapterIdForHarness(harness, transcriptPath = '') {
55
+ if (harness === 'codex') return 'codex-jsonl';
56
+ if (harness === 'claude-code') return 'claude-code-jsonl';
57
+ if (harness === 'walle') return 'walle-jsonl';
58
+ if (transcriptPath.includes(`${path.sep}.codex${path.sep}`)) return 'codex-jsonl';
59
+ if (transcriptPath.includes(`${path.sep}.claude${path.sep}`)) return 'claude-code-jsonl';
60
+ return 'walle-jsonl';
61
+ }
62
+
63
+ function sourceRefFromHarness(data = {}, harness = 'walle') {
64
+ const parsed = parseHarnessInput(data, harness);
65
+ const sourceFile = validateTranscriptPath(parsed.transcriptPath);
66
+ const adapterId = adapterIdForHarness(parsed.harness, sourceFile);
67
+ return {
68
+ adapterId,
69
+ uri: sourceFile,
70
+ sourceFile,
71
+ sourceId: parsed.sessionId ? `${adapterId}:${parsed.sessionId}` : undefined,
72
+ cwd: parsed.cwd,
73
+ metadata: { harness: parsed.harness },
74
+ };
75
+ }
76
+
77
+ async function countHumanMessages(sourceRef) {
78
+ ensureBuiltinSourceAdapters();
79
+ const adapter = sourceRegistry.instantiate(sourceRef.adapterId);
80
+ if (!adapter) throw new Error(`Unknown source adapter: ${sourceRef.adapterId}`);
81
+ const { records } = await collectIngestRecords(adapter, sourceRef);
82
+ return records.filter((record) => record.type === 'memory_record' && record.role === 'user').length;
83
+ }
84
+
85
+ function readState(sourceRef) {
86
+ const statePath = stateFilePath(sourceRef);
87
+ try {
88
+ return JSON.parse(fs.readFileSync(statePath, 'utf8'));
89
+ } catch {
90
+ return { lastHumanCount: 0 };
91
+ }
92
+ }
93
+
94
+ function writeState(sourceRef, state) {
95
+ const statePath = stateFilePath(sourceRef);
96
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
97
+ fs.writeFileSync(statePath, JSON.stringify({ ...state, updatedAt: new Date().toISOString() }, null, 2));
98
+ }
99
+
100
+ async function handleSessionStart(data = {}, { harness = 'walle' } = {}) {
101
+ const sourceRef = sourceRefFromHarness(data, harness);
102
+ writeState(sourceRef, { lastHumanCount: 0, sourceRef });
103
+ return { ok: true, action: 'session_start', sourceRef };
104
+ }
105
+
106
+ async function handleStop(data = {}, {
107
+ harness = 'walle',
108
+ saveInterval = SAVE_INTERVAL,
109
+ service = null,
110
+ background = false,
111
+ } = {}) {
112
+ const parsed = parseHarnessInput(data, harness);
113
+ if (parsed.stopHookActive) return { ok: true, action: 'skip_active_stop_hook' };
114
+ const sourceRef = sourceRefFromHarness(data, harness);
115
+ const humanCount = await countHumanMessages(sourceRef);
116
+ const state = readState(sourceRef);
117
+ const sinceLast = humanCount - Number(state.lastHumanCount || 0);
118
+ if (humanCount === 0 || sinceLast < saveInterval) {
119
+ return { ok: true, action: 'skip_interval', humanCount, sinceLast, sourceRef };
120
+ }
121
+
122
+ writeState(sourceRef, { ...state, lastHumanCount: humanCount, sourceRef });
123
+ if (background && !service) {
124
+ const spawned = spawnBackgroundIngest(sourceRef);
125
+ return { ok: true, action: 'background_ingest', humanCount, sinceLast, pid: spawned.pid, sourceRef };
126
+ }
127
+ const ingest = await runLockedIngest(sourceRef, { service });
128
+ return { ok: true, action: 'ingested', humanCount, sinceLast, ingest, sourceRef };
129
+ }
130
+
131
+ async function handlePrecompact(data = {}, { harness = 'walle', service = null } = {}) {
132
+ const sourceRef = sourceRefFromHarness(data, harness);
133
+ const ingest = await runLockedIngest(sourceRef, { service });
134
+ return { ok: true, action: 'precompact_ingested', ingest, sourceRef };
135
+ }
136
+
137
+ async function runLockedIngest(sourceRef, { service = null, dryRun = false } = {}) {
138
+ const release = acquireLock(sourceRef);
139
+ if (!release.acquired) return { skipped: true, reason: 'lock_exists', lockPath: release.lockPath };
140
+ try {
141
+ const ingestService = service || createSessionIngestService();
142
+ return await ingestService.ingestSource(sourceRef, { dryRun });
143
+ } finally {
144
+ release();
145
+ }
146
+ }
147
+
148
+ function spawnBackgroundIngest(sourceRef) {
149
+ const cli = path.join(__dirname, 'cli.js');
150
+ const child = spawn(process.execPath, [
151
+ cli,
152
+ 'ingest-file',
153
+ '--adapter',
154
+ sourceRef.adapterId,
155
+ '--file',
156
+ sourceRef.sourceFile,
157
+ '--source-id',
158
+ sourceRef.sourceId || '',
159
+ '--cwd',
160
+ sourceRef.cwd || '',
161
+ ], {
162
+ detached: true,
163
+ stdio: 'ignore',
164
+ });
165
+ child.unref();
166
+ return child;
167
+ }
168
+
169
+ function acquireLock(sourceRef) {
170
+ const lockPath = lockFilePath(sourceRef);
171
+ fs.mkdirSync(path.dirname(lockPath), { recursive: true });
172
+ let fd = null;
173
+ try {
174
+ fd = fs.openSync(lockPath, 'wx');
175
+ fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString(), sourceRef }));
176
+ } catch (err) {
177
+ if (err.code === 'EEXIST') {
178
+ const skipped = () => false;
179
+ skipped.acquired = false;
180
+ skipped.lockPath = lockPath;
181
+ return skipped;
182
+ }
183
+ throw err;
184
+ } finally {
185
+ if (fd != null) fs.closeSync(fd);
186
+ }
187
+ const release = () => {
188
+ try { fs.unlinkSync(lockPath); } catch {}
189
+ return true;
190
+ };
191
+ release.acquired = true;
192
+ release.lockPath = lockPath;
193
+ return release;
194
+ }
195
+
196
+ function stateFilePath(sourceRef) {
197
+ return path.join(getHookStateDir(), `${hashSource(sourceRef)}.json`);
198
+ }
199
+
200
+ function lockFilePath(sourceRef) {
201
+ return path.join(getHookStateDir(), `${hashSource(sourceRef)}.lock`);
202
+ }
203
+
204
+ function hashSource(sourceRef) {
205
+ return crypto.createHash('sha256')
206
+ .update(`${sourceRef.adapterId}:${sourceRef.sourceFile || sourceRef.uri || sourceRef.sourceId}`)
207
+ .digest('hex')
208
+ .slice(0, 24);
209
+ }
210
+
211
+ module.exports = {
212
+ SAVE_INTERVAL,
213
+ SUPPORTED_HARNESSES,
214
+ acquireLock,
215
+ adapterIdForHarness,
216
+ countHumanMessages,
217
+ getHookStateDir,
218
+ handlePrecompact,
219
+ handleSessionStart,
220
+ handleStop,
221
+ parseHarnessInput,
222
+ runLockedIngest,
223
+ sourceRefFromHarness,
224
+ validateTranscriptPath,
225
+ };
@@ -1,10 +1,14 @@
1
1
  'use strict';
2
2
 
3
- // Public routes are intentionally limited to webhook ingestion. Everything else
4
- // must come from localhost or present a shared bearer token.
3
+ // Public routes are intentionally limited to external ingress. Telemetry admin
4
+ // routes are public only at this outer gate; api-walle enforces
5
+ // WALLE_TELEMETRY_SECRET before returning any data.
5
6
  const PUBLIC_ROUTES = new Set([
6
7
  '/api/wall-e/health',
7
8
  '/api/wall-e/webhook',
9
+ '/api/wall-e/telemetry/ingest',
10
+ '/api/wall-e/telemetry/summary',
11
+ '/api/wall-e/telemetry/events',
8
12
  ]);
9
13
 
10
14
  function getAllowedOrigin(req) {
@@ -47,9 +47,17 @@ function buildSmartRoutingCandidates(brain, opts = {}) {
47
47
  }
48
48
 
49
49
  const currentProvider = String(opts.currentProvider || '').toLowerCase();
50
+ const currentModel = String(opts.currentModel || '').toLowerCase();
50
51
  const providerOrder = [currentProvider, 'openai', 'google', 'deepseek', 'anthropic', 'ollama', 'mlx']
51
52
  .filter(Boolean)
52
53
  .filter((value, idx, arr) => arr.indexOf(value) === idx);
54
+ let providerHealth = null;
55
+ try {
56
+ const { default: providerAvailability } = require('../llm/provider-availability');
57
+ providerHealth = new Map(
58
+ providerAvailability.getConfiguredProviders().map((provider) => [provider.providerId, provider]),
59
+ );
60
+ } catch {}
53
61
 
54
62
  const candidates = [];
55
63
  const rows = brain.listAllModels() || [];
@@ -57,9 +65,12 @@ function buildSmartRoutingCandidates(brain, opts = {}) {
57
65
  if (!model || model.enabled === 0 || model.enabled === false) continue;
58
66
  const provider = brain.getModelProvider(model.provider_id);
59
67
  if (!providerHasRuntimeAccess(provider)) continue;
68
+ const providerId = provider.id || model.provider_id;
69
+ if (providerHealth?.get(providerId)?.status === 'unhealthy') continue;
60
70
  candidates.push({
61
71
  registryId: model.id,
62
72
  providerType: String(provider.type || model.provider_type || '').toLowerCase(),
73
+ providerId,
63
74
  modelId: model.model_id,
64
75
  displayName: model.display_name,
65
76
  speedTier: model.speed_tier == null ? 3 : Number(model.speed_tier),
@@ -88,11 +99,24 @@ function buildSmartRoutingCandidates(brain, opts = {}) {
88
99
  }
89
100
 
90
101
  if (opts.includeAutoFirst) {
102
+ const isCurrentModel = (candidate) => {
103
+ if (!currentModel) return false;
104
+ return String(candidate.modelId || '').toLowerCase() === currentModel
105
+ || String(candidate.registryId || '').toLowerCase() === currentModel;
106
+ };
107
+ const sameProviderAlternative = currentProvider && currentModel
108
+ ? candidates.find((candidate) => candidate.providerType === currentProvider && !isCurrentModel(candidate))
109
+ : null;
91
110
  const alternatives = selected
111
+ .filter((candidate) => !sameProviderAlternative || candidate.registryId !== sameProviderAlternative.registryId)
92
112
  .filter((candidate) => !currentProvider || candidate.providerType !== currentProvider)
93
113
  .map((candidate) => candidate.registryId)
94
114
  .filter(Boolean);
95
- return alternatives.length > 0 ? [null, ...alternatives.slice(0, 5)] : [];
115
+ const ordered = [
116
+ sameProviderAlternative?.registryId,
117
+ ...alternatives,
118
+ ].filter(Boolean);
119
+ return ordered.length > 0 ? [null, ...ordered.slice(0, 5)] : [];
96
120
  }
97
121
 
98
122
  return selected.map((candidate) => candidate.registryId).filter(Boolean).slice(0, 6);
@@ -114,9 +138,11 @@ function handleWalleChatApi(req, res, url, { brain, ensureBrainInit }) {
114
138
  let requestFinished = false;
115
139
  try {
116
140
  const abortController = new AbortController();
117
- res.on('close', () => {
118
- if (!requestFinished) abortController.abort();
119
- });
141
+ if (typeof res.on === 'function') {
142
+ res.on('close', () => {
143
+ if (!requestFinished) abortController.abort();
144
+ });
145
+ }
120
146
  const chatModule = require('../chat');
121
147
  // Validate + parse attachments before invoking chat. The validator
122
148
  // throws AttachmentError with .code + .status so we can surface 4xx
@@ -198,10 +224,19 @@ function handleWalleChatApi(req, res, url, { brain, ensureBrainInit }) {
198
224
  const shouldSmartFallback = !body.model && parsedAttachments.length === 0;
199
225
  if (shouldSmartFallback) {
200
226
  try {
201
- const { getDefaultProviderType } = require('../llm/client');
202
- const currentProvider = body.provider || getDefaultProviderType();
227
+ const { getDefaultModel, getDefaultProviderType } = require('../llm/client');
228
+ const currentProvider = body.provider
229
+ || brain.getKv?.('walle_provider')
230
+ || process.env.WALLE_PROVIDER
231
+ || getDefaultProviderType();
232
+ const currentModel = body.model
233
+ || brain.getKv?.('walle_model')
234
+ || brain.getKv?.('walle_model_' + currentProvider)
235
+ || process.env.WALLE_MODEL
236
+ || getDefaultModel();
203
237
  const candidates = buildSmartRoutingCandidates(brain, {
204
238
  currentProvider,
239
+ currentModel,
205
240
  includeAutoFirst: true,
206
241
  });
207
242
  if (candidates.length > 1) {
@@ -241,11 +276,22 @@ function handleWalleChatApi(req, res, url, { brain, ensureBrainInit }) {
241
276
  } catch (err) {
242
277
  requestFinished = true;
243
278
  try { require('../telemetry').trackError('chat-api', err); } catch {}
279
+ const isProviderError = err && (err.code === 'AI_PROVIDER_ERROR' || err.providerError);
280
+ const providerPayload = isProviderError
281
+ ? require('../llm/provider-error').toApiPayload(err)
282
+ : null;
244
283
  if (wantsStream) {
245
- try { res.write('data: ' + JSON.stringify({ type: 'error', error: err.message }) + '\n\n'); } catch {}
284
+ try {
285
+ res.write('data: ' + JSON.stringify({
286
+ type: 'error',
287
+ error: providerPayload ? providerPayload.message : err.message,
288
+ ...(providerPayload || {}),
289
+ }) + '\n\n');
290
+ } catch {}
246
291
  try { res.end(); } catch {}
247
292
  } else {
248
- jsonResponse(res, { error: err.message }, 500);
293
+ const status = providerPayload?.providerError?.status || err.status || 500;
294
+ jsonResponse(res, providerPayload || { error: err.message }, status);
249
295
  }
250
296
  }
251
297
  }).catch(e => {
@@ -0,0 +1,27 @@
1
+ {
2
+ "description": "Wall-E session memory hooks",
3
+ "hooks": {
4
+ "Stop": [
5
+ {
6
+ "matcher": "*",
7
+ "hooks": [
8
+ {
9
+ "type": "command",
10
+ "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/walle-stop-hook.sh"
11
+ }
12
+ ]
13
+ }
14
+ ],
15
+ "PreCompact": [
16
+ {
17
+ "matcher": "*",
18
+ "hooks": [
19
+ {
20
+ "type": "command",
21
+ "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/walle-precompact-hook.sh"
22
+ }
23
+ ]
24
+ }
25
+ ]
26
+ }
27
+ }
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT="${WALL_E_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)}"
5
+ exec node "$ROOT/wall-e/hooks/cli.js" precompact --harness claude-code
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT="${WALL_E_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)}"
5
+ exec node "$ROOT/wall-e/hooks/cli.js" stop --harness claude-code
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT="${WALL_E_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)}"
5
+ HOOK_NAME="${1:-stop}"
6
+
7
+ exec node "$ROOT/wall-e/hooks/cli.js" "$HOOK_NAME" --harness codex
@@ -0,0 +1,37 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "matcher": "*",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "${CODEX_PLUGIN_ROOT}/hooks/walle-hook.sh session-start"
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "Stop": [
15
+ {
16
+ "matcher": "*",
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "${CODEX_PLUGIN_ROOT}/hooks/walle-hook.sh stop"
21
+ }
22
+ ]
23
+ }
24
+ ],
25
+ "PreCompact": [
26
+ {
27
+ "matcher": "*",
28
+ "hooks": [
29
+ {
30
+ "type": "command",
31
+ "command": "${CODEX_PLUGIN_ROOT}/hooks/walle-hook.sh precompact"
32
+ }
33
+ ]
34
+ }
35
+ ]
36
+ }
37
+ }
@@ -6,6 +6,7 @@ const { execFileSync } = require('child_process');
6
6
  const path = require('path');
7
7
  const { v4: uuidv4 } = require('uuid');
8
8
  const brain = require('../brain');
9
+ const { createSafeFetch } = require('../security/ssrf');
9
10
 
10
11
  const CAL_READER = path.join(__dirname, '..', 'skills', '_bundled', 'google-calendar', 'cal-reader');
11
12
 
@@ -200,7 +201,8 @@ function _readEventKit(hoursAhead) {
200
201
  * @returns {Promise<Array<{uid, title, start, end, attendees, location}>>}
201
202
  */
202
203
  async function _readICS(url, hoursAhead) {
203
- const resp = await fetch(url, { signal: AbortSignal.timeout(15000) });
204
+ const safeFetch = createSafeFetch({ fetchImpl: globalThis.fetch });
205
+ const resp = await safeFetch(url, { signal: AbortSignal.timeout(15000) });
204
206
  if (!resp.ok) throw new Error('ICS fetch failed: ' + resp.status);
205
207
  const text = await resp.text();
206
208
 
@@ -23,7 +23,20 @@ function createClient(type, config = {}) {
23
23
  if (!provider) {
24
24
  throw new Error(`Unknown provider type: ${type}`);
25
25
  }
26
- return provider;
26
+ return withStreamFallback(provider);
27
+ }
28
+
29
+ function withStreamFallback(provider) {
30
+ if (!provider || typeof provider.chat !== 'function' || typeof provider.chatStream === 'function') {
31
+ return provider;
32
+ }
33
+ return {
34
+ ...provider,
35
+ async *chatStream(opts = {}) {
36
+ const { streamFromChat } = require('../coding/stream-processor');
37
+ yield* streamFromChat(provider, opts);
38
+ },
39
+ };
27
40
  }
28
41
 
29
42
  // ============================================================
@@ -33,28 +46,60 @@ function createClient(type, config = {}) {
33
46
  let _defaultClient = null;
34
47
 
35
48
  function getDefaultProviderType() {
36
- // Check brain DB first (set by setup page), fall back to env
49
+ // Setup persists to .env and mirrors to process.env for the running daemon.
50
+ if (process.env.WALLE_PROVIDER) return process.env.WALLE_PROVIDER.toLowerCase();
51
+ // Fall back to Wall-E's kv_store for older/live setup writes.
37
52
  try {
38
53
  const brain = require('../brain');
39
- const val = brain.getDb().prepare("SELECT value FROM brain_metadata WHERE key = 'walle_provider'").get()?.value;
54
+ const val = typeof brain.getKv === 'function'
55
+ ? brain.getKv('walle_provider')
56
+ : brain.getDb().prepare("SELECT value FROM brain_metadata WHERE key = 'walle_provider'").get()?.value;
40
57
  if (val) return val.toLowerCase();
41
58
  } catch {}
42
- return (process.env.WALLE_PROVIDER || 'anthropic').toLowerCase();
59
+ return 'anthropic';
43
60
  }
44
61
 
45
62
  function getDefaultModel() {
46
63
  providerRegistry.ensureBootstrapped();
47
64
  const providerType = getDefaultProviderType();
48
- // Check brain DB first, fall back to env
65
+ if (process.env.WALLE_MODEL && _modelMatchesProvider(process.env.WALLE_MODEL, providerType)) return process.env.WALLE_MODEL;
49
66
  try {
50
67
  const brain = require('../brain');
51
- const val = brain.getDb().prepare("SELECT value FROM brain_metadata WHERE key = 'walle_model'").get()?.value;
68
+ const val = typeof brain.getKv === 'function'
69
+ ? brain.getKv('walle_model')
70
+ : brain.getDb().prepare("SELECT value FROM brain_metadata WHERE key = 'walle_model'").get()?.value;
52
71
  if (val && _modelMatchesProvider(val, providerType)) return val;
53
72
  } catch {}
54
- if (process.env.WALLE_MODEL && _modelMatchesProvider(process.env.WALLE_MODEL, providerType)) return process.env.WALLE_MODEL;
73
+ return getDefaultModelForProvider(providerType);
74
+ }
75
+
76
+ function getDefaultModelForProvider(providerType) {
77
+ providerRegistry.ensureBootstrapped();
55
78
  return providerRegistry.getDefaultModel(providerType) || providerRegistry.getDefaultModel('anthropic');
56
79
  }
57
80
 
81
+ function getDefaultAuthMethod(providerType) {
82
+ const type = (providerType || getDefaultProviderType()).toLowerCase();
83
+ try {
84
+ const brain = require('../brain');
85
+ const dbMethod = typeof brain.getProviderAuthMethod === 'function'
86
+ ? brain.getProviderAuthMethod(type)
87
+ : null;
88
+ if (dbMethod) return dbMethod;
89
+ } catch {}
90
+ if (type === 'anthropic' || type === 'openai') {
91
+ return process.env.WALLE_AUTH_METHOD || '';
92
+ }
93
+ return '';
94
+ }
95
+
96
+ function resolveCompatibleModel(model, providerType) {
97
+ providerRegistry.ensureBootstrapped();
98
+ const targetProvider = providerType || getDefaultProviderType();
99
+ if (model && _modelMatchesProvider(model, targetProvider)) return model;
100
+ return getDefaultModelForProvider(targetProvider);
101
+ }
102
+
58
103
  /**
59
104
  * Guard against model-provider mismatch. Delegates to the registry's
60
105
  * manifest-driven prefix matching (replaces the old hardcoded
@@ -74,11 +119,12 @@ function getDefaultClient() {
74
119
  providerRegistry.ensureBootstrapped();
75
120
  const providerType = getDefaultProviderType();
76
121
  const dbKey = _getProviderKeyFromDb(providerType);
122
+ const authMethod = getDefaultAuthMethod(providerType);
77
123
 
78
124
  // Anthropic auth_method=claude_cli routes through the user's `claude`
79
125
  // CLI (subscription billing) instead of the SDK against api.anthropic.com.
80
126
  // Other providers ignore this — claude_cli only makes sense for Claude.
81
- if (providerType === 'anthropic' && process.env.WALLE_AUTH_METHOD === 'claude_cli') {
127
+ if (providerType === 'anthropic' && authMethod === 'claude_cli') {
82
128
  _defaultClient = createClient('claude-cli', {});
83
129
  return _defaultClient;
84
130
  }
@@ -88,7 +134,7 @@ function getDefaultClient() {
88
134
  // forwards requests with the user's claude.ai OAuth Bearer token. We
89
135
  // pass an empty placeholder apiKey; the proxy ignores it and replaces
90
136
  // auth headers with its own.
91
- if (providerType === 'anthropic' && process.env.WALLE_AUTH_METHOD === 'oauth_proxy') {
137
+ if (providerType === 'anthropic' && authMethod === 'oauth_proxy') {
92
138
  const port = process.env.OAUTH_PROXY_PORT || '3458';
93
139
  _defaultClient = createClient('anthropic', {
94
140
  apiKey: 'oauth-proxy-placeholder',
@@ -100,7 +146,7 @@ function getDefaultClient() {
100
146
  // OpenAI auth_method=codex_cli routes through the user's `codex` CLI
101
147
  // (ChatGPT subscription billing) instead of api.openai.com. Mirrors the
102
148
  // Anthropic claude_cli path but with codex-cli as the underlying provider.
103
- if (providerType === 'openai' && process.env.WALLE_AUTH_METHOD === 'codex_cli') {
149
+ if (providerType === 'openai' && authMethod === 'codex_cli') {
104
150
  _defaultClient = createClient('codex-cli', {});
105
151
  return _defaultClient;
106
152
  }
@@ -158,9 +204,13 @@ function resetDefaultClient() {
158
204
 
159
205
  module.exports = {
160
206
  createClient,
207
+ withStreamFallback,
161
208
  getDefaultClient,
162
209
  getDefaultProviderType,
210
+ getDefaultAuthMethod,
163
211
  getDefaultModel,
212
+ getDefaultModelForProvider,
213
+ resolveCompatibleModel,
164
214
  resetDefaultClient,
165
215
  DEFAULT_TIER_MODELS,
166
216
  // Re-exported for callers that want to introspect the manifest
@@ -169,4 +219,8 @@ module.exports = {
169
219
  providerRegistry.ensureBootstrapped();
170
220
  return providerRegistry.detectProviderForModel(model);
171
221
  },
222
+ modelMatchesProvider: (model, provider) => {
223
+ providerRegistry.ensureBootstrapped();
224
+ return providerRegistry.modelMatchesProvider(model, provider);
225
+ },
172
226
  };