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,289 @@
1
+ 'use strict';
2
+
3
+ const { generateMemoryIndex, extractKeyEntities, extractKeyTopics } = require('../extraction/indexer');
4
+
5
+ const MAX_FIELD = 20;
6
+
7
+ function createSourceIndexEntry(memory) {
8
+ if (!memory || !memory.id || !memory.content) return null;
9
+ const content = String(memory.content || '');
10
+ if (content.trim().length < 10) return null;
11
+
12
+ const metadata = parseMetadata(memory.metadata);
13
+ const base = generateMemoryIndex(memory) || {
14
+ memory_id: memory.id,
15
+ summary: content.slice(0, 200).trim(),
16
+ key_entities: null,
17
+ key_topics: null,
18
+ domain: memory.domain || null,
19
+ topic: memory.topic || null,
20
+ timestamp: memory.timestamp,
21
+ };
22
+
23
+ const filesTouched = uniqueStrings([
24
+ ...arrayOfStrings(metadata.filesEdited),
25
+ ...arrayOfStrings(metadata.changed_files),
26
+ ...extractFilesTouched(content),
27
+ ], MAX_FIELD);
28
+ const commandsRun = uniqueStrings([
29
+ ...arrayOfStrings(metadata.commands),
30
+ ...extractCommandsRun(content),
31
+ ], MAX_FIELD);
32
+ const decisions = uniqueStrings(extractDecisions(content), MAX_FIELD);
33
+ const failuresBlockers = uniqueStrings(extractFailuresBlockers(content), MAX_FIELD);
34
+ const exactQuotes = extractExactQuotes(content);
35
+ validateExactQuotes(content, exactQuotes);
36
+
37
+ const genericTopics = parseJsonArray(base.key_topics);
38
+ const sourceId = metadata.sourceId || inferSourceId(memory.source_id);
39
+ const sourceFacts = {
40
+ topics: uniqueStrings([
41
+ ...genericTopics,
42
+ ...extractKeyTopics(content),
43
+ ...filesTouched,
44
+ ...commandsRun.map(commandTopic),
45
+ ...decisions.map(shortTopic),
46
+ ...failuresBlockers.map(shortTopic),
47
+ sourceId,
48
+ ], 60),
49
+ filesTouched,
50
+ commandsRun,
51
+ decisions,
52
+ failuresBlockers,
53
+ exactQuotes,
54
+ sourceId,
55
+ cwd: metadata.cwd || memory.source_channel || '',
56
+ branch: metadata.gitBranch || metadata.branch || '',
57
+ role: metadata.role || '',
58
+ memoryType: memory.memory_type || '',
59
+ };
60
+
61
+ return {
62
+ memory_id: memory.id,
63
+ summary: buildSummary(base.summary, sourceFacts),
64
+ key_entities: JSON.stringify(uniqueStrings([
65
+ ...parseJsonArray(base.key_entities),
66
+ ...extractKeyEntities(content),
67
+ ], 30)),
68
+ key_topics: JSON.stringify(sourceFacts),
69
+ domain: memory.domain || domainForMemory(memory),
70
+ topic: memory.topic || topicForMemory(memory, sourceFacts),
71
+ timestamp: memory.timestamp,
72
+ };
73
+ }
74
+
75
+ function indexMemory(memory, { brain = require('../brain') } = {}) {
76
+ const entry = createSourceIndexEntry(memory);
77
+ if (!entry) return null;
78
+ return brain.insertMemoryIndex(entry);
79
+ }
80
+
81
+ function rebuildSourceIndex({ brain = require('../brain'), sourceId = '', dryRun = false, limit = 10000 } = {}) {
82
+ const db = brain.getDb();
83
+ const params = [];
84
+ const conditions = ['archived_at IS NULL', 'content IS NOT NULL', 'length(content) > 10'];
85
+ if (sourceId) {
86
+ conditions.push('(source_id = ? OR source_id LIKE ? OR metadata LIKE ?)');
87
+ params.push(sourceId, `${sourceId}:%`, `%"sourceId":"${escapeLikeJson(sourceId)}"%`);
88
+ }
89
+ params.push(Math.min(Math.max(Number(limit) || 10000, 1), 50000));
90
+ const memories = db.prepare(`
91
+ SELECT * FROM memories
92
+ WHERE ${conditions.join(' AND ')}
93
+ ORDER BY timestamp ASC
94
+ LIMIT ?
95
+ `).all(...params);
96
+
97
+ if (dryRun) {
98
+ return { dry_run: true, source_id: sourceId || null, scanned: memories.length, indexed: 0, skipped: 0 };
99
+ }
100
+
101
+ let indexed = 0;
102
+ let skipped = 0;
103
+ const run = db.transaction(() => {
104
+ for (const memory of memories) {
105
+ const entry = createSourceIndexEntry(memory);
106
+ if (!entry) {
107
+ skipped++;
108
+ continue;
109
+ }
110
+ brain.insertMemoryIndex(entry);
111
+ indexed++;
112
+ }
113
+ });
114
+ run();
115
+ return { dry_run: false, source_id: sourceId || null, scanned: memories.length, indexed, skipped };
116
+ }
117
+
118
+ function extractFilesTouched(content) {
119
+ const files = [];
120
+ const re = /(?:^|[\s`'"(])((?:\.?[A-Za-z0-9_.@-]+\/)+[A-Za-z0-9_.@-]+\.[A-Za-z0-9]+|[A-Za-z0-9_.@-]+\.(?:js|ts|tsx|jsx|json|md|py|go|rs|java|rb|css|html|sql|sh|yml|yaml))(?=[\s`'",):;.\]]|$)/g;
121
+ let match;
122
+ while ((match = re.exec(content))) {
123
+ const value = match[1].replace(/^[./]*\/+/, '').replace(/[),.;:]+$/, '');
124
+ if (value && !value.includes('node_modules/')) files.push(value);
125
+ }
126
+ return files;
127
+ }
128
+
129
+ function extractCommandsRun(content) {
130
+ const commands = [];
131
+ const lineRe = /(?:^|\n)\s*(?:\$|>|Command:|Run:|ran)\s+([^\n]{2,220})/gi;
132
+ let match;
133
+ while ((match = lineRe.exec(content))) commands.push(cleanCommand(match[1]));
134
+ const inlineRe = /\b((?:node|npm|pnpm|yarn|git|npx|python|pytest|cargo|go test|ruby|bash|make)\s+[^\n.;]{2,180})/gi;
135
+ while ((match = inlineRe.exec(content))) commands.push(cleanCommand(match[1]));
136
+ return commands.filter(Boolean);
137
+ }
138
+
139
+ function extractDecisions(content) {
140
+ return extractLabeledLines(content, [
141
+ /\bdecision\s*:\s*(.+)$/i,
142
+ /\bdecided\s+(?:to|that)\s+(.+)$/i,
143
+ /\bwe will\s+(.+)$/i,
144
+ /\buse\s+(.+?)\s+instead of\s+(.+)$/i,
145
+ ]);
146
+ }
147
+
148
+ function extractFailuresBlockers(content) {
149
+ return extractLabeledLines(content, [
150
+ /\bblocker\s*:\s*(.+)$/i,
151
+ /\bblocked by\s+(.+)$/i,
152
+ /\bfailed because\s+(.+)$/i,
153
+ /\berror\s*:\s*(.+)$/i,
154
+ ]);
155
+ }
156
+
157
+ function extractLabeledLines(content, patterns) {
158
+ const results = [];
159
+ for (const rawLine of String(content || '').split('\n')) {
160
+ const line = rawLine.trim();
161
+ if (!line) continue;
162
+ for (const pattern of patterns) {
163
+ const match = line.match(pattern);
164
+ if (match?.[1]) {
165
+ results.push(match[1].trim());
166
+ break;
167
+ }
168
+ }
169
+ }
170
+ return results;
171
+ }
172
+
173
+ function extractExactQuotes(content) {
174
+ const text = String(content || '');
175
+ const lines = text.split('\n').map((line) => line.trim()).filter((line) => line.length >= 18);
176
+ const scored = lines
177
+ .map((line) => ({ line, score: quoteScore(line) }))
178
+ .sort((a, b) => b.score - a.score)
179
+ .map(({ line }) => line.slice(0, 180));
180
+ return uniqueStrings(scored, 5);
181
+ }
182
+
183
+ function validateExactQuotes(content, quotes) {
184
+ for (const quote of quotes || []) {
185
+ if (!String(content || '').includes(quote)) {
186
+ throw new Error(`source index quote is not verbatim: ${quote.slice(0, 40)}`);
187
+ }
188
+ }
189
+ }
190
+
191
+ function quoteScore(line) {
192
+ let score = Math.min(line.length, 160) / 160;
193
+ if (/\b(decision|decided|blocker|failed|error|command|test|file|fix)\b/i.test(line)) score += 1;
194
+ if (/[`$]/.test(line)) score += 0.4;
195
+ if (/\.[A-Za-z0-9]+/.test(line)) score += 0.3;
196
+ return score;
197
+ }
198
+
199
+ function buildSummary(baseSummary, facts) {
200
+ const parts = [baseSummary || 'Coding session memory'];
201
+ if (facts.filesTouched.length) parts.push(`Files: ${facts.filesTouched.slice(0, 4).join(', ')}`);
202
+ if (facts.decisions.length) parts.push(`Decisions: ${facts.decisions.slice(0, 2).join('; ')}`);
203
+ if (facts.failuresBlockers.length) parts.push(`Blockers: ${facts.failuresBlockers.slice(0, 2).join('; ')}`);
204
+ return parts.join(' | ').slice(0, 700);
205
+ }
206
+
207
+ function domainForMemory(memory) {
208
+ const source = memory.source || '';
209
+ const type = memory.memory_type || '';
210
+ return source.endsWith('-jsonl') || type.startsWith('coding_session_') ? 'technical' : null;
211
+ }
212
+
213
+ function topicForMemory(memory, facts) {
214
+ if (facts.failuresBlockers.length) return 'bugs';
215
+ if (facts.decisions.length) return 'decision';
216
+ if ((memory.memory_type || '').includes('tool')) return 'tools';
217
+ return facts.filesTouched.length ? 'coding' : null;
218
+ }
219
+
220
+ function parseMetadata(value) {
221
+ try {
222
+ const parsed = JSON.parse(value || '{}');
223
+ return parsed && typeof parsed === 'object' ? parsed : {};
224
+ } catch {
225
+ return {};
226
+ }
227
+ }
228
+
229
+ function parseJsonArray(value) {
230
+ try {
231
+ const parsed = JSON.parse(value || '[]');
232
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === 'string') : [];
233
+ } catch {
234
+ return [];
235
+ }
236
+ }
237
+
238
+ function arrayOfStrings(value) {
239
+ return Array.isArray(value)
240
+ ? value.filter((item) => typeof item === 'string' && item.trim()).map((item) => item.trim())
241
+ : [];
242
+ }
243
+
244
+ function uniqueStrings(values, limit = MAX_FIELD) {
245
+ const seen = new Set();
246
+ const out = [];
247
+ for (const value of values || []) {
248
+ const item = String(value || '').trim();
249
+ if (!item || seen.has(item)) continue;
250
+ seen.add(item);
251
+ out.push(item);
252
+ if (out.length >= limit) break;
253
+ }
254
+ return out;
255
+ }
256
+
257
+ function inferSourceId(sourceId = '') {
258
+ const parts = String(sourceId || '').split(':');
259
+ if (parts.length <= 2) return sourceId || '';
260
+ return parts.slice(0, 2).join(':');
261
+ }
262
+
263
+ function cleanCommand(value) {
264
+ return String(value || '').trim().replace(/^`|`$/g, '').replace(/\s+/g, ' ').slice(0, 220);
265
+ }
266
+
267
+ function commandTopic(command) {
268
+ return String(command || '').split(/\s+/).slice(0, 3).join(' ');
269
+ }
270
+
271
+ function shortTopic(value) {
272
+ return String(value || '').slice(0, 120);
273
+ }
274
+
275
+ function escapeLikeJson(value) {
276
+ return String(value || '').replace(/["\\]/g, '');
277
+ }
278
+
279
+ module.exports = {
280
+ createSourceIndexEntry,
281
+ extractCommandsRun,
282
+ extractDecisions,
283
+ extractExactQuotes,
284
+ extractFailuresBlockers,
285
+ extractFilesTouched,
286
+ indexMemory,
287
+ rebuildSourceIndex,
288
+ validateExactQuotes,
289
+ };
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const MAX_PLUGIN_MANIFEST_BYTES = 256 * 1024;
7
+
8
+ function inspectPluginCandidate(pluginDir, { rootDir = null, maxManifestBytes = MAX_PLUGIN_MANIFEST_BYTES } = {}) {
9
+ const diagnostics = [];
10
+ let dirReal;
11
+ try {
12
+ dirReal = fs.realpathSync(pluginDir);
13
+ } catch (err) {
14
+ return { ok: false, diagnostics: [{ level: 'warn', message: err.message, path: pluginDir }] };
15
+ }
16
+ const rootReal = rootDir ? safeRealpath(rootDir) : dirReal;
17
+ if (rootDir && (!rootReal || !isInside(rootReal, dirReal))) {
18
+ diagnostics.push({ level: 'error', message: 'plugin directory escapes root', path: pluginDir });
19
+ }
20
+ const manifestPath = path.join(dirReal, 'walle.plugin.json');
21
+ if (!fs.existsSync(manifestPath)) return { ok: false, diagnostics };
22
+ const stat = fs.statSync(manifestPath);
23
+ if (stat.size > maxManifestBytes) {
24
+ diagnostics.push({ level: 'error', message: 'plugin manifest too large', path: manifestPath });
25
+ }
26
+ const dirMode = fs.statSync(dirReal).mode;
27
+ if ((dirMode & 0o002) !== 0) {
28
+ diagnostics.push({ level: 'error', message: 'plugin directory is world-writable', path: dirReal });
29
+ }
30
+ return {
31
+ ok: diagnostics.filter(d => d.level === 'error').length === 0,
32
+ realpath: dirReal,
33
+ manifestPath,
34
+ diagnostics,
35
+ };
36
+ }
37
+
38
+ function discoverPluginCandidates({ roots = [], source = 'user' } = {}) {
39
+ const candidates = [];
40
+ const diagnostics = [];
41
+ for (const root of roots || []) {
42
+ const rootReal = safeRealpath(root);
43
+ if (!rootReal) {
44
+ diagnostics.push({ level: 'warn', message: `cannot read plugin root: ${root}`, source });
45
+ continue;
46
+ }
47
+ let entries = [];
48
+ try {
49
+ entries = fs.readdirSync(rootReal, { withFileTypes: true });
50
+ } catch (err) {
51
+ diagnostics.push({ level: 'warn', message: `cannot scan plugin root ${root}: ${err.message}`, source });
52
+ continue;
53
+ }
54
+ for (const entry of entries) {
55
+ if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name.startsWith('_')) continue;
56
+ const dir = path.join(rootReal, entry.name);
57
+ const inspected = inspectPluginCandidate(dir, { rootDir: rootReal });
58
+ diagnostics.push(...inspected.diagnostics.map(d => ({ ...d, source })));
59
+ if (inspected.ok) candidates.push({ dir: inspected.realpath, manifestPath: inspected.manifestPath, source });
60
+ }
61
+ }
62
+ return { candidates, diagnostics };
63
+ }
64
+
65
+ function safeRealpath(target) {
66
+ try {
67
+ return fs.realpathSync(target);
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ function isInside(root, target) {
74
+ const rel = path.relative(root, target);
75
+ return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel));
76
+ }
77
+
78
+ module.exports = {
79
+ MAX_PLUGIN_MANIFEST_BYTES,
80
+ discoverPluginCandidates,
81
+ inspectPluginCandidate,
82
+ isInside,
83
+ };
@@ -2,7 +2,8 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
- const { validateManifest } = require('./manifest-validator');
5
+ const { normalizePluginManifest } = require('./manifest-schema');
6
+ const { MAX_PLUGIN_MANIFEST_BYTES, isInside } = require('./discovery');
6
7
 
7
8
  const MANIFEST_FILENAME = 'walle.plugin.json';
8
9
 
@@ -14,7 +15,7 @@ const MANIFEST_FILENAME = 'walle.plugin.json';
14
15
  * @param {string} pluginDir - Directory to look in
15
16
  * @param {string} [source] - Plugin source for diagnostics (bundled/user/...)
16
17
  */
17
- function loadManifestFromDir(pluginDir, source) {
18
+ function loadManifestFromDir(pluginDir, source, options = {}) {
18
19
  const manifestPath = path.join(pluginDir, MANIFEST_FILENAME);
19
20
  if (!fs.existsSync(manifestPath)) {
20
21
  return { manifest: null, diagnostics: [] };
@@ -22,6 +23,32 @@ function loadManifestFromDir(pluginDir, source) {
22
23
 
23
24
  let raw;
24
25
  try {
26
+ const stat = fs.statSync(manifestPath);
27
+ if (stat.size > (options.maxManifestBytes || MAX_PLUGIN_MANIFEST_BYTES)) {
28
+ return {
29
+ manifest: null,
30
+ diagnostics: [{
31
+ level: 'error',
32
+ message: `Manifest too large: ${manifestPath}`,
33
+ source: source || 'unknown',
34
+ }],
35
+ };
36
+ }
37
+ if (options.rootDir) {
38
+ const rootReal = fs.realpathSync(options.rootDir);
39
+ const manifestReal = fs.realpathSync(manifestPath);
40
+ const dirReal = fs.realpathSync(pluginDir);
41
+ if (!isInside(rootReal, manifestReal) || !isInside(rootReal, dirReal)) {
42
+ return {
43
+ manifest: null,
44
+ diagnostics: [{
45
+ level: 'error',
46
+ message: `Manifest escapes plugin root: ${manifestPath}`,
47
+ source: source || 'unknown',
48
+ }],
49
+ };
50
+ }
51
+ }
25
52
  raw = fs.readFileSync(manifestPath, 'utf8');
26
53
  } catch (e) {
27
54
  return {
@@ -52,7 +79,7 @@ function loadManifestFromDir(pluginDir, source) {
52
79
  };
53
80
  }
54
81
 
55
- const result = validateManifest(parsed, { source });
82
+ const result = normalizePluginManifest(parsed, { source });
56
83
  if (result.valid) {
57
84
  return {
58
85
  manifest: { ...parsed, dir: pluginDir, manifestPath, source: source || 'unknown' },
@@ -67,10 +94,11 @@ function loadManifestFromDir(pluginDir, source) {
67
94
  * Returns { manifests: [...], diagnostics: [...] }.
68
95
  *
69
96
  * Inspired by OpenClaw's `src/plugins/discovery.ts:discoverOpenClawPlugins`,
70
- * but simpler: no caching, no boundary-file safety, no ownership UID check
71
- * (we trust the local filesystem since Wall-E is single-user).
97
+ * but scoped to Wall-E's local filesystem: manifest size and realpath
98
+ * containment are enforced here; ownership/world-writable checks live in
99
+ * plugins/discovery.js for callers that need full candidate hardening.
72
100
  */
73
- function loadManifestsFromDir(rootDir, source) {
101
+ function loadManifestsFromDir(rootDir, source, options = {}) {
74
102
  const manifests = [];
75
103
  const diagnostics = [];
76
104
 
@@ -93,7 +121,7 @@ function loadManifestsFromDir(rootDir, source) {
93
121
  for (const entry of entries) {
94
122
  if (!entry.isDirectory()) continue;
95
123
  if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
96
- const result = loadManifestFromDir(path.join(rootDir, entry.name), source);
124
+ const result = loadManifestFromDir(path.join(rootDir, entry.name), source, { ...options, rootDir });
97
125
  diagnostics.push(...result.diagnostics);
98
126
  if (result.manifest) manifests.push(result.manifest);
99
127
  }
@@ -110,7 +138,7 @@ function loadManifestsFromDir(rootDir, source) {
110
138
  * @param {string} entryFile - Path to the JS module (sibling .plugin.json is read)
111
139
  * @param {string} [source]
112
140
  */
113
- function loadSiblingManifest(entryFile, source) {
141
+ function loadSiblingManifest(entryFile, source, options = {}) {
114
142
  const dir = path.dirname(entryFile);
115
143
  const base = path.basename(entryFile, path.extname(entryFile));
116
144
  const manifestPath = path.join(dir, `${base}.plugin.json`);
@@ -121,6 +149,17 @@ function loadSiblingManifest(entryFile, source) {
121
149
 
122
150
  let raw;
123
151
  try {
152
+ const stat = fs.statSync(manifestPath);
153
+ if (stat.size > (options.maxManifestBytes || MAX_PLUGIN_MANIFEST_BYTES)) {
154
+ return {
155
+ manifest: null,
156
+ diagnostics: [{
157
+ level: 'error',
158
+ message: `Manifest too large: ${manifestPath}`,
159
+ source: source || 'unknown',
160
+ }],
161
+ };
162
+ }
124
163
  raw = fs.readFileSync(manifestPath, 'utf8');
125
164
  } catch (e) {
126
165
  return {
@@ -151,7 +190,7 @@ function loadSiblingManifest(entryFile, source) {
151
190
  };
152
191
  }
153
192
 
154
- const result = validateManifest(parsed, { source });
193
+ const result = normalizePluginManifest(parsed, { source });
155
194
  if (result.valid) {
156
195
  return {
157
196
  manifest: {
@@ -198,7 +237,7 @@ function loadSiblingManifestsFromDir(rootDir, source, options = {}) {
198
237
  if (exclude.has(entry.name)) continue;
199
238
  const stem = entry.name.replace(/\.plugin\.json$/, '');
200
239
  const entryFile = path.join(rootDir, `${stem}.js`);
201
- const result = loadSiblingManifest(entryFile, source);
240
+ const result = loadSiblingManifest(entryFile, source, options);
202
241
  diagnostics.push(...result.diagnostics);
203
242
  if (result.manifest) manifests.push(result.manifest);
204
243
  }
@@ -212,4 +251,5 @@ module.exports = {
212
251
  loadManifestsFromDir,
213
252
  loadSiblingManifest,
214
253
  loadSiblingManifestsFromDir,
254
+ normalizePluginManifest,
215
255
  };
@@ -0,0 +1,69 @@
1
+ 'use strict';
2
+
3
+ const { validateManifest } = require('./manifest-validator');
4
+
5
+ const ACTIVATION_TRIGGERS = new Set([
6
+ 'startup',
7
+ 'provider',
8
+ 'agentHarness',
9
+ 'command',
10
+ 'channel',
11
+ 'route',
12
+ 'configPath',
13
+ 'capability',
14
+ ]);
15
+
16
+ function normalizePluginManifest(manifest = {}, opts = {}) {
17
+ const base = validateManifest(manifest, opts);
18
+ const diagnostics = [...base.diagnostics];
19
+ const pluginId = typeof manifest.id === 'string' ? manifest.id : undefined;
20
+ const source = opts.source || 'unknown';
21
+
22
+ if (manifest.activation !== undefined) {
23
+ const activation = Array.isArray(manifest.activation) ? manifest.activation : [];
24
+ if (!Array.isArray(manifest.activation)) {
25
+ diagnostics.push({ level: 'error', message: 'activation must be an array', fieldPath: 'activation', pluginId, source });
26
+ }
27
+ for (const item of activation) {
28
+ if (!item || typeof item !== 'object' || !ACTIVATION_TRIGGERS.has(item.type)) {
29
+ diagnostics.push({ level: 'error', message: `unsupported activation trigger: ${item?.type}`, fieldPath: 'activation', pluginId, source });
30
+ }
31
+ }
32
+ }
33
+
34
+ if (manifest.modelSupport !== undefined) {
35
+ const support = manifest.modelSupport;
36
+ const valid = support && typeof support === 'object' && (
37
+ Array.isArray(support.prefixes) || Array.isArray(support.patterns) || Array.isArray(support.models)
38
+ );
39
+ if (!valid) diagnostics.push({ level: 'error', message: 'modelSupport must declare prefixes, patterns, or models', fieldPath: 'modelSupport', pluginId, source });
40
+ }
41
+
42
+ if (manifest.modelCatalog !== undefined && !Array.isArray(manifest.modelCatalog)) {
43
+ diagnostics.push({ level: 'error', message: 'modelCatalog must be an array', fieldPath: 'modelCatalog', pluginId, source });
44
+ }
45
+
46
+ if (manifest.configSchema !== undefined && (!manifest.configSchema || typeof manifest.configSchema !== 'object')) {
47
+ diagnostics.push({ level: 'error', message: 'configSchema must be an object', fieldPath: 'configSchema', pluginId, source });
48
+ }
49
+
50
+ if (manifest.permissions !== undefined) {
51
+ for (const [key, value] of Object.entries(manifest.permissions || {})) {
52
+ if (typeof value !== 'boolean') {
53
+ diagnostics.push({ level: 'error', message: `permission ${key} must be boolean`, fieldPath: `permissions.${key}`, pluginId, source });
54
+ }
55
+ }
56
+ }
57
+
58
+ const errorCount = diagnostics.filter(d => d.level === 'error').length;
59
+ return {
60
+ valid: errorCount === 0,
61
+ manifest: errorCount === 0 ? manifest : undefined,
62
+ diagnostics,
63
+ };
64
+ }
65
+
66
+ module.exports = {
67
+ ACTIVATION_TRIGGERS,
68
+ normalizePluginManifest,
69
+ };
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ function normalizeModelId(providerId, modelId) {
4
+ const provider = String(providerId || '').trim().toLowerCase();
5
+ const model = String(modelId || '').trim();
6
+ if (!model) return '';
7
+ if (model.includes('/')) return model;
8
+ return provider ? `${provider}/${model}` : model;
9
+ }
10
+
11
+ function buildModelCatalog(manifests = []) {
12
+ const models = new Map();
13
+ const aliases = new Map();
14
+ for (const manifest of manifests || []) {
15
+ if (manifest.kind !== 'provider') continue;
16
+ const providerId = manifest.id;
17
+ for (const item of manifest.modelCatalog || []) {
18
+ const normalizedId = normalizeModelId(providerId, item.id || item.model);
19
+ if (!normalizedId) continue;
20
+ const record = {
21
+ id: normalizedId,
22
+ providerId,
23
+ model: normalizedId.split('/').slice(1).join('/'),
24
+ displayName: item.displayName || item.name || normalizedId,
25
+ contextWindow: item.contextWindow || item.context_window || null,
26
+ inputCostPer1M: item.inputCostPer1M ?? item.input_cost_per_1m ?? null,
27
+ outputCostPer1M: item.outputCostPer1M ?? item.output_cost_per_1m ?? null,
28
+ capabilities: { ...(item.capabilities || {}) },
29
+ };
30
+ models.set(normalizedId, record);
31
+ for (const alias of item.aliases || []) aliases.set(String(alias), normalizedId);
32
+ }
33
+ for (const prefix of manifest.modelPrefixes || []) aliases.set(prefix, `${providerId}/${prefix}*`);
34
+ }
35
+ return {
36
+ list: () => [...models.values()],
37
+ resolve: (id) => {
38
+ const raw = String(id || '');
39
+ if (models.has(raw)) return models.get(raw);
40
+ if (aliases.has(raw) && models.has(aliases.get(raw))) return models.get(aliases.get(raw));
41
+ for (const [alias, target] of aliases.entries()) {
42
+ if (target.endsWith('*') && raw.startsWith(alias)) {
43
+ return { id: normalizeModelId(target.slice(0, target.indexOf('/')), raw), providerId: target.slice(0, target.indexOf('/')), model: raw, inferred: true };
44
+ }
45
+ }
46
+ return null;
47
+ },
48
+ aliases: () => new Map(aliases),
49
+ };
50
+ }
51
+
52
+ module.exports = {
53
+ buildModelCatalog,
54
+ normalizeModelId,
55
+ };
@@ -0,0 +1,2 @@
1
+ Wall-E coding prompt bundle v1.
2
+ Use the selected coding tools to inspect, edit, and verify the repository. Keep tool descriptions in the registry, not in this prompt file.
@@ -0,0 +1 @@
1
+ DeepSeek mode reminder: keep tool-loop turns concise and avoid carrying reasoning blocks back into later tool-result requests.
@@ -0,0 +1,9 @@
1
+ Use Wall-E session memory as part of the coding workflow.
2
+
3
+ - At session start or before acting on old context, call `walle_memory_status` when available so you know whether memory is connected.
4
+ - Before answering or changing code based on "last time", "previously", "this branch", "that bug", "my preference", a named project/person, or a remembered decision, search first with `walle_search_sessions` or `search_memories`.
5
+ - Batch searches by angle: project/session id, file path, error text, decision keyword. Do not run one narrow query and give up.
6
+ - When you use retrieved memory, cite the source session id, timestamp, file, or diary entry returned by the tool.
7
+ - For long retrieved sessions, use `walle_get_session` to inspect the specific source/session instead of relying on a short snippet.
8
+ - At stop, handoff, or before compaction, call `walle_diary_write` with decisions, changed files, commands, blockers, and next steps.
9
+ - Diary entries are operational state, not prose. Keep them concise and specific enough for the next agent to resume.
@@ -0,0 +1 @@
1
+ Planning mode reminder: edits, writes, patches, and shell mutations are forbidden. Produce analysis and a concrete implementation plan only.