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.
- package/README.md +3 -3
- package/package.json +2 -2
- package/template/bin/dev.sh +7 -1
- package/template/bin/setup.js +53 -9
- package/template/bin/sync-images.js +53 -0
- package/template/builder-journal.md +17 -0
- package/template/claude-task-manager/api-prompts.js +98 -13
- package/template/claude-task-manager/api-reviews.js +82 -5
- package/template/claude-task-manager/db.js +32 -5
- package/template/claude-task-manager/docs/session-capture-foundation-design.md +1273 -0
- package/template/claude-task-manager/lib/claude-desktop-sessions.js +696 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +49 -1
- package/template/claude-task-manager/lib/session-capture.js +421 -0
- package/template/claude-task-manager/lib/session-history.js +135 -15
- package/template/claude-task-manager/lib/session-jobs.js +10 -5
- package/template/claude-task-manager/lib/session-stream.js +87 -19
- package/template/claude-task-manager/lib/setup-provider-config.js +115 -0
- package/template/claude-task-manager/lib/walle-ctm-history.js +72 -0
- package/template/claude-task-manager/lib/walle-session-context.js +61 -0
- package/template/claude-task-manager/lib/walle-transcript.js +176 -0
- package/template/claude-task-manager/public/css/setup.css +35 -8
- package/template/claude-task-manager/public/css/walle-session.css +56 -0
- package/template/claude-task-manager/public/css/walle.css +120 -0
- package/template/claude-task-manager/public/index.html +814 -181
- package/template/claude-task-manager/public/js/message-renderer.js +148 -19
- package/template/claude-task-manager/public/js/reviews.js +120 -62
- package/template/claude-task-manager/public/js/setup.js +75 -31
- package/template/claude-task-manager/public/js/stream-view.js +115 -55
- package/template/claude-task-manager/public/js/walle-session.js +84 -2
- package/template/claude-task-manager/public/js/walle.js +308 -54
- package/template/claude-task-manager/server.js +1092 -146
- package/template/claude-task-manager/session-integrity.js +181 -54
- package/template/claude-task-manager/session-utils.js +123 -41
- package/template/claude-task-manager/workers/state-detectors/codex.js +5 -2
- package/template/package.json +1 -1
- package/template/wall-e/adapters/ctm.js +39 -18
- package/template/wall-e/agent-runners/contract.js +17 -0
- package/template/wall-e/agent-runners/index.js +22 -0
- package/template/wall-e/agent-runtime/harness.js +212 -0
- package/template/wall-e/agent-runtime/index.js +8 -0
- package/template/wall-e/agent-runtime/registry.js +67 -0
- package/template/wall-e/agent-runtime/session-store.js +179 -0
- package/template/wall-e/agent-runtime/spawn.js +208 -0
- package/template/wall-e/api-walle.js +174 -7
- package/template/wall-e/brain.js +266 -28
- package/template/wall-e/channels/policy.js +88 -0
- package/template/wall-e/channels/registry.js +15 -1
- package/template/wall-e/channels/reply-dispatcher.js +70 -0
- package/template/wall-e/channels/session-bindings.js +51 -0
- package/template/wall-e/chat/code-review-context.js +29 -0
- package/template/wall-e/chat.js +188 -42
- package/template/wall-e/coding/acp-adapter.js +188 -0
- package/template/wall-e/coding/agent-catalog.js +129 -0
- package/template/wall-e/coding/compaction-service.js +247 -0
- package/template/wall-e/coding/execution-trace.js +3 -0
- package/template/wall-e/coding/instruction-service.js +224 -0
- package/template/wall-e/coding/model-message.js +67 -0
- package/template/wall-e/coding/permission-rules-store.js +111 -0
- package/template/wall-e/coding/permission-service.js +266 -0
- package/template/wall-e/coding/prompt-bundle.js +67 -0
- package/template/wall-e/coding/prompt-runtime.js +243 -0
- package/template/wall-e/coding/provider-transform.js +188 -0
- package/template/wall-e/coding/runtime-mode.js +132 -0
- package/template/wall-e/coding/snapshot-service.js +155 -0
- package/template/wall-e/coding/stream-processor.js +268 -0
- package/template/wall-e/coding/task-tool.js +255 -0
- package/template/wall-e/coding/tool-registry.js +361 -0
- package/template/wall-e/coding/transcript-writer.js +143 -0
- package/template/wall-e/coding/workspace-replay.js +324 -0
- package/template/wall-e/coding-context.js +4 -22
- package/template/wall-e/coding-orchestrator.js +307 -18
- package/template/wall-e/coding-prompts.js +44 -3
- package/template/wall-e/context/context-builder.js +43 -1
- package/template/wall-e/context/topic-matcher.js +1 -1
- package/template/wall-e/eval/agent-runner.js +59 -13
- package/template/wall-e/eval/benchmarks/memory-retrieval.json +155 -57
- package/template/wall-e/eval/benchmarks.js +100 -16
- package/template/wall-e/eval/eval-orchestrator.js +218 -8
- package/template/wall-e/eval/harvester.js +62 -5
- package/template/wall-e/eval/head-to-head.js +23 -2
- package/template/wall-e/eval/humaneval-adapter.js +30 -5
- package/template/wall-e/eval/livecodebench-adapter.js +29 -5
- package/template/wall-e/eval/manifest.js +186 -0
- package/template/wall-e/eval/run-agent-benchmarks.js +66 -2
- package/template/wall-e/eval/session-retrieval-benchmark.js +150 -0
- package/template/wall-e/eval/session-transcripts.js +57 -4
- package/template/wall-e/eval/swebench-adapter.js +109 -3
- package/template/wall-e/evaluation/agent-router.js +53 -1
- package/template/wall-e/evaluation/coding-quorum.js +48 -1
- package/template/wall-e/evaluation/router.js +4 -2
- package/template/wall-e/evaluation/tier-selector.js +11 -1
- package/template/wall-e/extraction/contradiction.js +2 -2
- package/template/wall-e/extraction/indexer.js +2 -1
- package/template/wall-e/extraction/knowledge-extractor.js +2 -2
- package/template/wall-e/hooks/cli.js +92 -0
- package/template/wall-e/hooks/discovery.js +119 -0
- package/template/wall-e/hooks/index.js +7 -0
- package/template/wall-e/hooks/manifest.js +55 -0
- package/template/wall-e/hooks/runtime.js +84 -0
- package/template/wall-e/hooks/session-memory.js +225 -0
- package/template/wall-e/http/auth.js +6 -2
- package/template/wall-e/http/chat-api.js +54 -8
- package/template/wall-e/integrations/claude-plugin/hooks/hooks.json +27 -0
- package/template/wall-e/integrations/claude-plugin/hooks/walle-precompact-hook.sh +5 -0
- package/template/wall-e/integrations/claude-plugin/hooks/walle-stop-hook.sh +5 -0
- package/template/wall-e/integrations/codex-plugin/hooks/walle-hook.sh +7 -0
- package/template/wall-e/integrations/codex-plugin/hooks.json +37 -0
- package/template/wall-e/listening/calendar.js +3 -1
- package/template/wall-e/llm/client.js +64 -10
- package/template/wall-e/llm/google.js +39 -5
- package/template/wall-e/llm/ollama.js +1 -1
- package/template/wall-e/llm/ollama.plugin.json +1 -1
- package/template/wall-e/llm/provider-availability.js +10 -0
- package/template/wall-e/llm/provider-error.js +269 -0
- package/template/wall-e/llm/tool-adapter.js +48 -12
- package/template/wall-e/loops/boot.js +2 -1
- package/template/wall-e/loops/initiative.js +2 -2
- package/template/wall-e/loops/tasks.js +8 -47
- package/template/wall-e/loops/workspace-prompts.js +20 -0
- package/template/wall-e/mcp-server.js +442 -1
- package/template/wall-e/memory/session-ingest-service.js +159 -0
- package/template/wall-e/memory/source-indexer.js +289 -0
- package/template/wall-e/plugins/discovery.js +83 -0
- package/template/wall-e/plugins/manifest-loader.js +50 -10
- package/template/wall-e/plugins/manifest-schema.js +69 -0
- package/template/wall-e/plugins/model-catalog.js +55 -0
- package/template/wall-e/prompts/coding/base.txt +2 -0
- package/template/wall-e/prompts/coding/deepseek.txt +1 -0
- package/template/wall-e/prompts/coding/memory-protocol.md +9 -0
- package/template/wall-e/prompts/coding/plan.txt +1 -0
- package/template/wall-e/runtime/execution-trace.js +220 -0
- package/template/wall-e/security/audit.js +266 -0
- package/template/wall-e/security/ssrf.js +236 -0
- package/template/wall-e/session-files.js +303 -0
- package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +3 -0
- package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +3 -0
- package/template/wall-e/skills/internal-skill-registry.js +2 -2
- package/template/wall-e/skills/script-skill-runner.js +143 -0
- package/template/wall-e/skills/skill-executor.js +5 -6
- package/template/wall-e/skills/skill-fallback.js +3 -1
- package/template/wall-e/skills/skill-harness-registry.js +7 -8
- package/template/wall-e/skills/skill-planner.js +52 -4
- package/template/wall-e/skills/slack-ingest.js +11 -3
- package/template/wall-e/sources/base.js +90 -0
- package/template/wall-e/sources/builtin.js +33 -0
- package/template/wall-e/sources/claude-code-jsonl.js +78 -0
- package/template/wall-e/sources/codex-jsonl.js +125 -0
- package/template/wall-e/sources/coding-session-utils.js +117 -0
- package/template/wall-e/sources/contract-suite.js +59 -0
- package/template/wall-e/sources/gemini-jsonl.js +85 -0
- package/template/wall-e/sources/index.js +9 -0
- package/template/wall-e/sources/jsonl-utils.js +181 -0
- package/template/wall-e/sources/record-types.js +252 -0
- package/template/wall-e/sources/registry.js +92 -0
- package/template/wall-e/sources/transforms.js +100 -0
- package/template/wall-e/sources/walle-jsonl.js +108 -0
- package/template/wall-e/tools/coding-middleware.js +31 -1
- package/template/wall-e/tools/file-tracker.js +25 -1
- package/template/wall-e/tools/local-tools.js +75 -47
- package/template/wall-e/tools/session-sharing.js +68 -1
- package/template/wall-e/tools/shell-analyzer.js +1 -1
- package/template/wall-e/tools/shell-policy.js +47 -0
- package/template/wall-e/tools/snapshot.js +42 -0
- package/template/wall-e/training/harvester.js +62 -5
- package/template/wall-e/utils/repair.js +253 -1
- package/template/website/index.html +3 -3
- 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 {
|
|
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 =
|
|
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
|
|
71
|
-
*
|
|
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 =
|
|
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 @@
|
|
|
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.
|