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,236 @@
1
+ 'use strict';
2
+
3
+ const net = require('node:net');
4
+
5
+ const DEFAULT_ALLOWED_PROTOCOLS = ['http:', 'https:'];
6
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
7
+
8
+ class SsrfGuardError extends Error {
9
+ constructor(message, details = {}) {
10
+ super(message);
11
+ this.name = 'SsrfGuardError';
12
+ this.code = 'ERR_WALLE_SSRF_GUARD';
13
+ this.details = details;
14
+ }
15
+ }
16
+
17
+ function validateSafeUrl(input, options = {}) {
18
+ const {
19
+ allowLocal = false,
20
+ allowedProtocols = DEFAULT_ALLOWED_PROTOCOLS,
21
+ resolvedAddresses = [],
22
+ } = options;
23
+ const parsed = parseUrl(input);
24
+ if (!parsed.ok) return parsed;
25
+
26
+ const protocolAllowed = allowedProtocols.includes(parsed.url.protocol);
27
+ if (!protocolAllowed) {
28
+ return failure('blocked-protocol', `URL protocol is not allowed: ${parsed.url.protocol}`, parsed.url);
29
+ }
30
+
31
+ const host = normalizeHostname(parsed.url.hostname);
32
+ if (!host) return failure('missing-hostname', 'URL must include a hostname', parsed.url);
33
+
34
+ if (!allowLocal) {
35
+ if (isLocalHostname(host)) return failure('local-hostname', `URL hostname is local: ${host}`, parsed.url);
36
+ if (isPrivateAddress(host)) return failure('private-address', `URL hostname resolves to private address: ${host}`, parsed.url);
37
+ for (const address of resolvedAddresses || []) {
38
+ if (isPrivateAddress(address)) {
39
+ return failure('private-resolved-address', `URL hostname resolved to private address: ${address}`, parsed.url, { address });
40
+ }
41
+ }
42
+ }
43
+
44
+ return {
45
+ ok: true,
46
+ url: parsed.url.toString(),
47
+ protocol: parsed.url.protocol,
48
+ hostname: host,
49
+ };
50
+ }
51
+
52
+ async function validateSafeUrlAsync(input, options = {}) {
53
+ const initial = validateSafeUrl(input, options);
54
+ if (!initial.ok) return initial;
55
+ if (options.allowLocal || !options.resolveHostname) return initial;
56
+ if (net.isIP(initial.hostname)) return initial;
57
+
58
+ let resolved = [];
59
+ try {
60
+ const result = await options.resolveHostname(initial.hostname);
61
+ resolved = Array.isArray(result) ? result : [result];
62
+ } catch (err) {
63
+ return failure('dns-resolution-failed', `Could not resolve URL hostname: ${initial.hostname}`, initial.url, {
64
+ error: err.message,
65
+ });
66
+ }
67
+
68
+ const addresses = resolved
69
+ .map(item => typeof item === 'string' ? item : item && item.address)
70
+ .filter(Boolean);
71
+ return validateSafeUrl(initial.url, { ...options, resolvedAddresses: addresses });
72
+ }
73
+
74
+ function createSafeFetch(options = {}) {
75
+ const {
76
+ fetchImpl = globalThis.fetch,
77
+ maxRedirects = 5,
78
+ ...guardOptions
79
+ } = options;
80
+ if (typeof fetchImpl !== 'function') throw new Error('fetchImpl is required');
81
+
82
+ return async function safeFetch(input, init = {}) {
83
+ let currentUrl = inputToUrl(input);
84
+ let redirects = 0;
85
+
86
+ while (true) {
87
+ await assertSafeUrl(currentUrl, guardOptions);
88
+ const response = await fetchImpl(currentUrl, { ...init, redirect: 'manual' });
89
+ if (response && response.url && response.url !== currentUrl) {
90
+ await assertSafeUrl(response.url, guardOptions);
91
+ }
92
+
93
+ if (init.redirect === 'manual' || !isRedirectResponse(response)) return response;
94
+ const location = response.headers && typeof response.headers.get === 'function'
95
+ ? response.headers.get('location')
96
+ : null;
97
+ if (!location) return response;
98
+ if (init.redirect === 'error') {
99
+ throw new SsrfGuardError('Redirect received while redirect mode is error', { url: currentUrl, location });
100
+ }
101
+ if (redirects >= maxRedirects) {
102
+ throw new SsrfGuardError('Too many redirects while fetching URL', { url: currentUrl, maxRedirects });
103
+ }
104
+ redirects += 1;
105
+ currentUrl = new URL(location, currentUrl).toString();
106
+ if (response.body && typeof response.body.cancel === 'function') {
107
+ try { await response.body.cancel(); } catch {}
108
+ }
109
+ }
110
+ };
111
+ }
112
+
113
+ async function assertSafeUrl(input, options = {}) {
114
+ const result = await validateSafeUrlAsync(input, options);
115
+ if (!result.ok) throw new SsrfGuardError(result.message, result);
116
+ return result;
117
+ }
118
+
119
+ function parseUrl(input) {
120
+ try {
121
+ const url = input instanceof URL ? input : new URL(inputToUrl(input));
122
+ return { ok: true, url };
123
+ } catch (err) {
124
+ return {
125
+ ok: false,
126
+ reason: 'invalid-url',
127
+ message: `Invalid URL: ${err.message}`,
128
+ url: String(input || ''),
129
+ };
130
+ }
131
+ }
132
+
133
+ function inputToUrl(input) {
134
+ if (input instanceof URL) return input.toString();
135
+ if (typeof Request !== 'undefined' && input instanceof Request) return input.url;
136
+ if (input && typeof input === 'object' && typeof input.url === 'string') return input.url;
137
+ return String(input || '');
138
+ }
139
+
140
+ function isRedirectResponse(response) {
141
+ return response && REDIRECT_STATUSES.has(response.status);
142
+ }
143
+
144
+ function normalizeHostname(hostname) {
145
+ return String(hostname || '')
146
+ .trim()
147
+ .replace(/^\[/, '')
148
+ .replace(/\]$/, '')
149
+ .replace(/\.$/, '')
150
+ .split('%')[0]
151
+ .toLowerCase();
152
+ }
153
+
154
+ function isLocalHostname(hostname) {
155
+ const host = normalizeHostname(hostname);
156
+ return host === 'localhost' ||
157
+ host.endsWith('.localhost') ||
158
+ host === 'localdomain' ||
159
+ host.endsWith('.localdomain') ||
160
+ host.endsWith('.local');
161
+ }
162
+
163
+ function isPrivateAddress(address) {
164
+ const value = normalizeHostname(address);
165
+ const ipVersion = net.isIP(value);
166
+ if (ipVersion === 4) return isPrivateIpv4(value);
167
+ if (ipVersion === 6) return isPrivateIpv6(value);
168
+ return false;
169
+ }
170
+
171
+ function isPrivateIpv4(address) {
172
+ const parts = address.split('.').map(part => Number(part));
173
+ if (parts.length !== 4 || parts.some(part => !Number.isInteger(part) || part < 0 || part > 255)) return false;
174
+ const [a, b] = parts;
175
+ if (a === 0 || a === 10 || a === 127) return true;
176
+ if (a === 100 && b >= 64 && b <= 127) return true;
177
+ if (a === 169 && b === 254) return true;
178
+ if (a === 172 && b >= 16 && b <= 31) return true;
179
+ if (a === 192 && (b === 0 || b === 168)) return true;
180
+ if (a === 198 && (b === 18 || b === 19)) return true;
181
+ if (a >= 224) return true;
182
+ return false;
183
+ }
184
+
185
+ function isPrivateIpv6(address) {
186
+ const ip = normalizeHostname(address);
187
+ if (ip === '::' || ip === '::1') return true;
188
+ const mapped = ip.match(/^::ffff:(.+)$/);
189
+ if (mapped) {
190
+ const mappedIpv4 = ipv4FromMappedIpv6(mapped[1]);
191
+ if (mappedIpv4) return isPrivateIpv4(mappedIpv4);
192
+ }
193
+ const first = parseInt(ip.split(':')[0] || '0', 16);
194
+ if (!Number.isFinite(first)) return false;
195
+ if ((first & 0xfe00) === 0xfc00) return true; // unique local fc00::/7
196
+ if ((first & 0xffc0) === 0xfe80) return true; // link-local fe80::/10
197
+ if ((first & 0xff00) === 0xff00) return true; // multicast ff00::/8
198
+ return false;
199
+ }
200
+
201
+ function ipv4FromMappedIpv6(value) {
202
+ if (net.isIP(value) === 4) return value;
203
+ const parts = value.split(':');
204
+ if (parts.length !== 2) return null;
205
+ const high = parseInt(parts[0] || '0', 16);
206
+ const low = parseInt(parts[1] || '0', 16);
207
+ if (!Number.isInteger(high) || !Number.isInteger(low) || high < 0 || high > 0xffff || low < 0 || low > 0xffff) {
208
+ return null;
209
+ }
210
+ return [
211
+ (high >> 8) & 0xff,
212
+ high & 0xff,
213
+ (low >> 8) & 0xff,
214
+ low & 0xff,
215
+ ].join('.');
216
+ }
217
+
218
+ function failure(reason, message, url, details = {}) {
219
+ return {
220
+ ok: false,
221
+ reason,
222
+ message,
223
+ url: url instanceof URL ? url.toString() : String(url || ''),
224
+ ...details,
225
+ };
226
+ }
227
+
228
+ module.exports = {
229
+ SsrfGuardError,
230
+ assertSafeUrl,
231
+ createSafeFetch,
232
+ isLocalHostname,
233
+ isPrivateAddress,
234
+ validateSafeUrl,
235
+ validateSafeUrlAsync,
236
+ };
@@ -0,0 +1,303 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const { execFileSync } = require('node:child_process');
8
+
9
+ const SESSION_SCHEMA = 'wall-e-session-v1';
10
+ const MAX_SEGMENT_LENGTH = 180;
11
+ const LAST_EVENT_SCAN_BYTES = 4 * 1024 * 1024;
12
+
13
+ const lastUuidByFile = new Map();
14
+ const gitBranchByCwd = new Map();
15
+
16
+ function getDataDir() {
17
+ return process.env.WALL_E_DATA_DIR || path.join(os.homedir(), '.walle', 'data');
18
+ }
19
+
20
+ function getSessionRoot() {
21
+ return process.env.WALL_E_SESSION_DIR
22
+ || process.env.WALLE_SESSION_DIR
23
+ || path.join(getDataDir(), 'sessions');
24
+ }
25
+
26
+ function isSessionFilesEnabled() {
27
+ const value = String(process.env.WALL_E_SESSION_FILES || process.env.WALLE_SESSION_FILES || '').toLowerCase();
28
+ return value !== '0' && value !== 'false' && value !== 'off';
29
+ }
30
+
31
+ function projectSlug(cwd) {
32
+ const resolved = path.resolve(cwd || process.cwd());
33
+ const normalized = resolved.replace(/\\/g, '/');
34
+ const slug = normalized
35
+ .replace(/\//g, '-')
36
+ .replace(/[^A-Za-z0-9._-]/g, '-')
37
+ .replace(/-+/g, '-');
38
+ return safeSegment(slug || 'unknown-project', 'unknown-project', 240);
39
+ }
40
+
41
+ function safeSegment(value, fallback = 'session', maxLength = MAX_SEGMENT_LENGTH) {
42
+ const raw = String(value || fallback);
43
+ const cleaned = raw
44
+ .replace(/[\\/]/g, '_')
45
+ .replace(/[^A-Za-z0-9._:-]/g, '_')
46
+ .replace(/:+/g, '_')
47
+ .replace(/_+/g, '_')
48
+ .replace(/^_+|_+$/g, '')
49
+ .slice(0, maxLength);
50
+ return cleaned || fallback;
51
+ }
52
+
53
+ function resolveSessionFile({ sessionId, cwd, rootDir } = {}) {
54
+ const id = sessionId || 'default';
55
+ const baseDir = rootDir || getSessionRoot();
56
+ const projectDir = path.join(baseDir, projectSlug(cwd));
57
+ const fileName = `${safeSegment(id)}.jsonl`;
58
+ return {
59
+ rootDir: baseDir,
60
+ projectDir,
61
+ filePath: path.join(projectDir, fileName),
62
+ sessionId: id,
63
+ };
64
+ }
65
+
66
+ class WallESessionRecorder {
67
+ constructor({ sessionId, channel = 'ctm', cwd, rootDir, version, gitBranch, metadata } = {}) {
68
+ this.sessionId = sessionId || 'default';
69
+ this.channel = channel || 'ctm';
70
+ this.cwd = path.resolve(cwd || process.cwd());
71
+ this.version = version || getWallEVersion();
72
+ this.gitBranch = gitBranch || detectGitBranch(this.cwd);
73
+ this.metadata = metadata || {};
74
+ this.enabled = isSessionFilesEnabled();
75
+
76
+ const resolved = resolveSessionFile({ sessionId: this.sessionId, cwd: this.cwd, rootDir });
77
+ this.rootDir = resolved.rootDir;
78
+ this.projectDir = resolved.projectDir;
79
+ this.filePath = resolved.filePath;
80
+ this._started = false;
81
+ this._parentUuid = this.enabled ? (lastUuidByFile.get(this.filePath) || readLastEventUuid(this.filePath)) : null;
82
+ }
83
+
84
+ ensureStarted() {
85
+ if (!this.enabled || this._started) return null;
86
+ this._started = true;
87
+ if (hasExistingContent(this.filePath)) return null;
88
+ return this._appendRecord(this._baseRecord('session_start', {
89
+ data: {
90
+ channel: this.channel,
91
+ cwd: this.cwd,
92
+ metadata: this.metadata,
93
+ },
94
+ }));
95
+ }
96
+
97
+ appendMessage(role, content, extra = {}) {
98
+ if (!this.enabled) return null;
99
+ this.ensureStarted();
100
+ return this._appendRecord(this._baseRecord(role, {
101
+ ...extra,
102
+ message: {
103
+ role,
104
+ content: normalizeContent(content),
105
+ ...(extra.message && typeof extra.message === 'object' ? extra.message : {}),
106
+ },
107
+ }));
108
+ }
109
+
110
+ appendProgress(data, extra = {}) {
111
+ if (!this.enabled) return null;
112
+ this.ensureStarted();
113
+ return this._appendRecord(this._baseRecord('progress', {
114
+ ...extra,
115
+ data: normalizeContent(data),
116
+ }));
117
+ }
118
+
119
+ appendError(error, extra = {}) {
120
+ if (!this.enabled) return null;
121
+ this.ensureStarted();
122
+ return this._appendRecord(this._baseRecord('error', {
123
+ ...extra,
124
+ error: serializeError(error),
125
+ }));
126
+ }
127
+
128
+ _baseRecord(type, fields = {}) {
129
+ const uuid = crypto.randomUUID();
130
+ return {
131
+ parentUuid: this._parentUuid || null,
132
+ isSidechain: false,
133
+ userType: 'external',
134
+ cwd: this.cwd,
135
+ sessionId: this.sessionId,
136
+ version: this.version,
137
+ gitBranch: this.gitBranch || undefined,
138
+ type,
139
+ timestamp: new Date().toISOString(),
140
+ uuid,
141
+ walle: {
142
+ schema: SESSION_SCHEMA,
143
+ channel: this.channel,
144
+ projectSlug: projectSlug(this.cwd),
145
+ },
146
+ ...fields,
147
+ };
148
+ }
149
+
150
+ _appendRecord(record) {
151
+ fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
152
+ fs.appendFileSync(this.filePath, JSON.stringify(record) + '\n', 'utf8');
153
+ this._parentUuid = record.uuid;
154
+ lastUuidByFile.set(this.filePath, record.uuid);
155
+ return { uuid: record.uuid, filePath: this.filePath };
156
+ }
157
+ }
158
+
159
+ function createSessionRecorder(opts) {
160
+ return new WallESessionRecorder(opts);
161
+ }
162
+
163
+ function readSessionFile({ sessionId, cwd, rootDir, filePath } = {}) {
164
+ const target = filePath || resolveSessionFile({ sessionId, cwd, rootDir }).filePath;
165
+ let raw = '';
166
+ try { raw = fs.readFileSync(target, 'utf8'); } catch {
167
+ return { filePath: target, events: [] };
168
+ }
169
+ const events = [];
170
+ for (const line of raw.split('\n')) {
171
+ if (!line.trim()) continue;
172
+ try { events.push(JSON.parse(line)); } catch {
173
+ events.push({ type: 'malformed', raw: line });
174
+ }
175
+ }
176
+ return { filePath: target, events };
177
+ }
178
+
179
+ function listSessionFiles({ rootDir, limit = 100 } = {}) {
180
+ const root = rootDir || getSessionRoot();
181
+ const files = [];
182
+ collectJsonlFiles(root, files);
183
+ files.sort((a, b) => b.mtimeMs - a.mtimeMs);
184
+ return files.slice(0, Math.max(1, limit)).map((entry) => ({
185
+ filePath: entry.filePath,
186
+ sessionId: path.basename(entry.filePath, '.jsonl'),
187
+ projectSlug: path.basename(path.dirname(entry.filePath)),
188
+ size: entry.size,
189
+ mtime: new Date(entry.mtimeMs).toISOString(),
190
+ }));
191
+ }
192
+
193
+ function collectJsonlFiles(dir, out) {
194
+ let entries;
195
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
196
+ for (const entry of entries) {
197
+ const fullPath = path.join(dir, entry.name);
198
+ if (entry.isDirectory()) {
199
+ collectJsonlFiles(fullPath, out);
200
+ continue;
201
+ }
202
+ if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue;
203
+ try {
204
+ const stat = fs.statSync(fullPath);
205
+ out.push({ filePath: fullPath, size: stat.size, mtimeMs: stat.mtimeMs });
206
+ } catch {}
207
+ }
208
+ }
209
+
210
+ function hasExistingContent(filePath) {
211
+ try { return fs.statSync(filePath).size > 0; } catch { return false; }
212
+ }
213
+
214
+ function readLastEventUuid(filePath) {
215
+ const evt = readLastJsonObject(filePath);
216
+ return evt && typeof evt.uuid === 'string' ? evt.uuid : null;
217
+ }
218
+
219
+ function readLastJsonObject(filePath) {
220
+ let stat;
221
+ try { stat = fs.statSync(filePath); } catch { return null; }
222
+ if (!stat.size) return null;
223
+
224
+ const fd = fs.openSync(filePath, 'r');
225
+ try {
226
+ const bytes = Math.min(stat.size, LAST_EVENT_SCAN_BYTES);
227
+ const buffer = Buffer.alloc(bytes);
228
+ fs.readSync(fd, buffer, 0, bytes, stat.size - bytes);
229
+ const text = buffer.toString('utf8');
230
+ const lines = text.split('\n').filter((line) => line.trim());
231
+ for (let i = lines.length - 1; i >= 0; i--) {
232
+ try { return JSON.parse(lines[i]); } catch {}
233
+ }
234
+ } finally {
235
+ fs.closeSync(fd);
236
+ }
237
+ return null;
238
+ }
239
+
240
+ function normalizeContent(value) {
241
+ if (value == null) return value;
242
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value;
243
+ if (Array.isArray(value)) return value.map(normalizeContent);
244
+ if (typeof value !== 'object') return String(value);
245
+
246
+ const out = {};
247
+ for (const [key, child] of Object.entries(value)) {
248
+ if (key === 'data' && typeof child === 'string' && child.length > 1024 && looksLikeBase64(child)) {
249
+ out.dataSha256 = crypto.createHash('sha256').update(child).digest('hex');
250
+ out.dataBytes = Buffer.byteLength(child, 'utf8');
251
+ out.data = '[base64 omitted: persisted attachment payload is stored outside the session transcript]';
252
+ continue;
253
+ }
254
+ out[key] = normalizeContent(child);
255
+ }
256
+ return out;
257
+ }
258
+
259
+ function looksLikeBase64(value) {
260
+ return /^[A-Za-z0-9+/=\r\n]+$/.test(value);
261
+ }
262
+
263
+ function serializeError(error) {
264
+ if (!error) return { message: 'Unknown error' };
265
+ return {
266
+ name: error.name || 'Error',
267
+ message: error.message || String(error),
268
+ code: error.code || undefined,
269
+ };
270
+ }
271
+
272
+ function detectGitBranch(cwd) {
273
+ const key = path.resolve(cwd || process.cwd());
274
+ if (gitBranchByCwd.has(key)) return gitBranchByCwd.get(key);
275
+ let branch = null;
276
+ try {
277
+ branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
278
+ cwd: key,
279
+ encoding: 'utf8',
280
+ stdio: ['ignore', 'pipe', 'ignore'],
281
+ timeout: 500,
282
+ }).trim() || null;
283
+ } catch {}
284
+ gitBranchByCwd.set(key, branch);
285
+ return branch;
286
+ }
287
+
288
+ function getWallEVersion() {
289
+ try { return require('./package.json').version || 'unknown'; } catch { return 'unknown'; }
290
+ }
291
+
292
+ module.exports = {
293
+ SESSION_SCHEMA,
294
+ WallESessionRecorder,
295
+ createSessionRecorder,
296
+ getSessionRoot,
297
+ isSessionFilesEnabled,
298
+ listSessionFiles,
299
+ projectSlug,
300
+ readSessionFile,
301
+ resolveSessionFile,
302
+ safeSegment,
303
+ };
@@ -11,6 +11,9 @@ entry: ../../../scripts/slack-backfill.js
11
11
  args: []
12
12
  trigger:
13
13
  type: manual
14
+ requires:
15
+ env:
16
+ - SLACK_OWNER_USER_ID
14
17
  config:
15
18
  mode:
16
19
  type: string
@@ -12,6 +12,9 @@ args: ["--sync"]
12
12
  trigger:
13
13
  type: interval
14
14
  schedule: "every 15m"
15
+ requires:
16
+ env:
17
+ - SLACK_OWNER_USER_ID
15
18
  config:
16
19
  mode:
17
20
  type: string
@@ -39,7 +39,7 @@ const BUNDLED_SKILL_TIMEOUT_MS = parseInt(
39
39
  // the bundled script (which historically crashed wall-e — see the
40
40
  // `gws-workspace/run.js` "no accounts" path for the canonical case).
41
41
  function _makeBundledFactory(modPath, name) {
42
- return async function bundledFactory({ brain: _brain, log }) {
42
+ return async function bundledFactory({ brain: _brain, log } = {}) {
43
43
  void _brain; // children load their own brain singleton
44
44
  return new Promise((resolve) => {
45
45
  const child = spawn(process.execPath, [modPath], {
@@ -215,7 +215,7 @@ function clearInternalSkillRegistry() {
215
215
  // The pre-existing slack-ingest hardcoded path becomes an explicit
216
216
  // registration here. Other bundled skills continue to work through
217
217
  // the filesystem fallback in resolveInternalSkill.
218
- registerInternalSkill('slack-ingest', async ({ brain, log }) => {
218
+ registerInternalSkill('slack-ingest', async ({ brain, log } = {}) => {
219
219
  const slackIngest = require('./slack-ingest');
220
220
  return await slackIngest.runIngestBatch({ brain, log });
221
221
  });