create-walle 0.9.13 → 0.9.14
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 +6 -1
- package/bin/create-walle.js +195 -30
- package/bin/mcp-inject.js +18 -53
- package/package.json +3 -1
- package/template/claude-task-manager/approval-agent.js +7 -0
- package/template/claude-task-manager/docs/session-standup-command-center-design.md +242 -0
- package/template/claude-task-manager/git-utils.js +111 -3
- package/template/claude-task-manager/lib/session-history.js +144 -16
- package/template/claude-task-manager/lib/session-standup.js +409 -0
- package/template/claude-task-manager/lib/standup-attention.js +200 -0
- package/template/claude-task-manager/lib/status-hooks.js +8 -2
- package/template/claude-task-manager/lib/update-telemetry.js +114 -0
- package/template/claude-task-manager/lib/walle-default-model.js +55 -0
- package/template/claude-task-manager/lib/walle-mcp-auto-config.js +62 -0
- package/template/claude-task-manager/lib/walle-supervisor.js +83 -19
- package/template/claude-task-manager/lib/worktree-cwd.js +82 -0
- package/template/claude-task-manager/providers/codex-mcp.js +104 -0
- package/template/claude-task-manager/providers/index.js +2 -0
- package/template/claude-task-manager/public/css/setup.css +2 -1
- package/template/claude-task-manager/public/css/walle.css +5 -0
- package/template/claude-task-manager/public/index.html +1596 -283
- package/template/claude-task-manager/public/js/session-search-utils.js +171 -1
- package/template/claude-task-manager/public/js/setup.js +62 -19
- package/template/claude-task-manager/public/js/stream-view.js +55 -6
- package/template/claude-task-manager/public/js/walle-session.js +73 -16
- package/template/claude-task-manager/public/js/walle.js +34 -2
- package/template/claude-task-manager/server.js +780 -177
- package/template/claude-task-manager/session-integrity.js +58 -15
- package/template/claude-task-manager/workers/approval-widget-validator.js +15 -5
- package/template/claude-task-manager/workers/state-detectors/codex.js +6 -0
- package/template/package.json +1 -1
- package/template/wall-e/agent.js +36 -7
- package/template/wall-e/api-walle.js +72 -20
- package/template/wall-e/coding/stream-processor.js +22 -2
- package/template/wall-e/coding-orchestrator.js +26 -6
- package/template/wall-e/eval/agent-runner.js +16 -4
- package/template/wall-e/eval/benchmark-generator.js +21 -1
- package/template/wall-e/eval/benchmarks/coding-agent.json +0 -596
- package/template/wall-e/eval/codex-cli-baseline.js +633 -0
- package/template/wall-e/eval/eval-orchestrator.js +3 -3
- package/template/wall-e/eval/run-agent-benchmarks.js +11 -3
- package/template/wall-e/eval/run-codex-cli-baseline.js +177 -0
- package/template/wall-e/lib/mcp-integration.js +220 -0
- package/template/wall-e/llm/ollama.js +47 -8
- package/template/wall-e/llm/ollama.plugin.json +1 -1
- package/template/wall-e/llm/tool-adapter.js +1 -0
- package/template/wall-e/loops/ingest.js +42 -8
- package/template/wall-e/mcp-server.js +272 -10
- package/template/wall-e/memory/ctm-session-context.js +910 -0
- package/template/wall-e/server.js +26 -1
- package/template/wall-e/skills/_bundled/scan-ctm-sessions/SKILL.md +20 -0
- package/template/wall-e/skills/_bundled/scan-ctm-sessions/run.js +43 -0
- package/template/wall-e/skills/skill-planner.js +52 -3
- package/template/wall-e/tools/builtin-middleware.js +55 -2
- package/template/wall-e/tools/shell-policy.js +1 -1
- package/template/wall-e/tools/slack-owner.js +104 -0
- package/template/website/index.html +2 -2
- package/template/builder-journal.md +0 -17
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
try {
|
|
5
|
+
const envPath = require('path').resolve(__dirname, '..', '..', '.env');
|
|
6
|
+
const lines = require('fs').readFileSync(envPath, 'utf8').split('\n');
|
|
7
|
+
for (const line of lines) {
|
|
8
|
+
const match = line.match(/^([A-Z_]+)=(.*)$/);
|
|
9
|
+
if (match && !process.env[match[1]]) process.env[match[1]] = match[2];
|
|
10
|
+
}
|
|
11
|
+
} catch {}
|
|
12
|
+
|
|
13
|
+
const crypto = require('crypto');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
process.chdir(path.join(__dirname, '..'));
|
|
16
|
+
|
|
17
|
+
const benchmarks = require('./benchmarks/coding-agent.json');
|
|
18
|
+
const {
|
|
19
|
+
DEFAULT_RESULTS_DIR,
|
|
20
|
+
runCodexCliBaselineBenchmark,
|
|
21
|
+
storeBaselineResult,
|
|
22
|
+
summarizeBaselineResults,
|
|
23
|
+
writeBaselineArtifact,
|
|
24
|
+
} = require('./codex-cli-baseline');
|
|
25
|
+
|
|
26
|
+
async function main() {
|
|
27
|
+
const args = parseArgs(process.argv.slice(2));
|
|
28
|
+
if (args.help) {
|
|
29
|
+
printHelp();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const selected = selectBenchmarks({
|
|
34
|
+
id: args.id,
|
|
35
|
+
limit: args.limit ? Number(args.limit) : null,
|
|
36
|
+
difficulty: args.difficulty,
|
|
37
|
+
});
|
|
38
|
+
if (!selected.length) {
|
|
39
|
+
console.error('No benchmarks selected');
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const timeoutMs = args.timeout ? Number(args.timeout) : 600_000;
|
|
45
|
+
const runId = crypto.randomUUID();
|
|
46
|
+
const dryRun = !!args['dry-run'];
|
|
47
|
+
const model = args.model || null;
|
|
48
|
+
const resultsDir = args['results-dir'] || DEFAULT_RESULTS_DIR;
|
|
49
|
+
const brain = initBrain();
|
|
50
|
+
|
|
51
|
+
console.log('=== Codex CLI Baseline Runner ===');
|
|
52
|
+
console.log(`Run: ${runId}`);
|
|
53
|
+
console.log(`Benchmarks: ${selected.length}/${benchmarks.length}`);
|
|
54
|
+
console.log(`Mode: ${dryRun ? 'dry-run' : 'codex exec baseline'}`);
|
|
55
|
+
console.log(`Model: ${model || 'codex default'}`);
|
|
56
|
+
console.log(`Auth: ${args['use-env-openai-key'] ? 'env OPENAI_API_KEY allowed' : 'ChatGPT/Codex auth preferred (OPENAI_API_KEY stripped)'}`);
|
|
57
|
+
console.log('');
|
|
58
|
+
|
|
59
|
+
const results = [];
|
|
60
|
+
for (const benchmark of selected) {
|
|
61
|
+
console.log(`--- ${benchmark.id} (${benchmark.difficulty || 'unknown'}) ---`);
|
|
62
|
+
console.log(` Prompt: ${String(benchmark.prompt || '').replace(/\s+/g, ' ').slice(0, 120)}`);
|
|
63
|
+
console.log(` Fixture: ${benchmark.agentExpectations?.projectFixture || 'express-basic'}`);
|
|
64
|
+
|
|
65
|
+
const started = Date.now();
|
|
66
|
+
const result = await runCodexCliBaselineBenchmark(benchmark, {
|
|
67
|
+
dryRun,
|
|
68
|
+
model,
|
|
69
|
+
timeoutMs,
|
|
70
|
+
keepFailures: !!args['keep-failures'],
|
|
71
|
+
useEnvOpenAIKey: !!args['use-env-openai-key'],
|
|
72
|
+
dangerouslyBypassSandbox: !!args['dangerously-bypass-sandbox'],
|
|
73
|
+
allowMcp: !!args['allow-mcp'],
|
|
74
|
+
disableMcpServers: normalizeListArg(args['disable-mcp']),
|
|
75
|
+
});
|
|
76
|
+
result.runId = runId;
|
|
77
|
+
result.timestamp = new Date().toISOString();
|
|
78
|
+
results.push(result);
|
|
79
|
+
|
|
80
|
+
if (args.record) {
|
|
81
|
+
result.artifactPath = writeBaselineArtifact(result, { resultsDir });
|
|
82
|
+
}
|
|
83
|
+
storeBaselineResult({ brain, runId, benchmark, result, model, timeoutMs });
|
|
84
|
+
|
|
85
|
+
console.log(` Success: ${result.success}`);
|
|
86
|
+
if (result.status) console.log(` Status: ${result.status}`);
|
|
87
|
+
if (result.score) console.log(` Score: ${(result.score.composite || 0).toFixed(3)}`);
|
|
88
|
+
console.log(` Files: ${(result.actualFileChanges || []).join(', ') || 'none'}`);
|
|
89
|
+
console.log(` Tests: ${result.testsPassed == null ? 'N/A' : result.testsPassed}`);
|
|
90
|
+
console.log(` Time: ${((Date.now() - started) / 1000).toFixed(1)}s`);
|
|
91
|
+
if (result.error) console.log(` Error: ${String(result.error).split('\n')[0]}`);
|
|
92
|
+
if (result.sandboxDir) console.log(` Kept sandbox: ${result.sandboxDir}`);
|
|
93
|
+
if (result.artifactPath) console.log(` Artifact: ${result.artifactPath}`);
|
|
94
|
+
console.log('');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const summary = summarizeBaselineResults(results);
|
|
98
|
+
console.log('=== SUMMARY ===');
|
|
99
|
+
console.log(`Passed: ${summary.passed}/${summary.total}`);
|
|
100
|
+
console.log(`Average composite score: ${summary.avgComposite.toFixed(3)}`);
|
|
101
|
+
console.log(`Failures: ${JSON.stringify(summary.failures)}`);
|
|
102
|
+
if (!dryRun && summary.failed > 0) process.exitCode = 1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function initBrain() {
|
|
106
|
+
try {
|
|
107
|
+
const brain = require('../brain');
|
|
108
|
+
brain.initDb();
|
|
109
|
+
return brain;
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.warn(`Brain not available: ${err.message}`);
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function selectBenchmarks({ id, limit, difficulty } = {}) {
|
|
117
|
+
let selected = benchmarks;
|
|
118
|
+
if (id) selected = selected.filter((benchmark) => benchmark.id === id);
|
|
119
|
+
if (difficulty) selected = selected.filter((benchmark) => benchmark.difficulty === difficulty);
|
|
120
|
+
if (limit) selected = selected.slice(0, limit);
|
|
121
|
+
return selected;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function parseArgs(argv) {
|
|
125
|
+
const out = {};
|
|
126
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
127
|
+
const arg = argv[i];
|
|
128
|
+
if (!arg.startsWith('--')) continue;
|
|
129
|
+
const key = arg.slice(2);
|
|
130
|
+
if (['dry-run', 'record', 'keep-failures', 'use-env-openai-key', 'dangerously-bypass-sandbox', 'allow-mcp', 'help'].includes(key)) {
|
|
131
|
+
out[key] = true;
|
|
132
|
+
} else {
|
|
133
|
+
if (out[key] === undefined) out[key] = argv[i + 1];
|
|
134
|
+
else if (Array.isArray(out[key])) out[key].push(argv[i + 1]);
|
|
135
|
+
else out[key] = [out[key], argv[i + 1]];
|
|
136
|
+
i += 1;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function normalizeListArg(value) {
|
|
143
|
+
if (value == null) return null;
|
|
144
|
+
const values = Array.isArray(value) ? value : [value];
|
|
145
|
+
return values
|
|
146
|
+
.flatMap((entry) => String(entry || '').split(','))
|
|
147
|
+
.map((entry) => entry.trim())
|
|
148
|
+
.filter(Boolean);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function printHelp() {
|
|
152
|
+
console.log(`Usage:
|
|
153
|
+
node eval/run-codex-cli-baseline.js --id agent-001 --model gpt-5.5
|
|
154
|
+
node eval/run-codex-cli-baseline.js --limit 5 --model gpt-5-codex --record
|
|
155
|
+
node eval/run-codex-cli-baseline.js --dry-run --difficulty easy
|
|
156
|
+
|
|
157
|
+
Options:
|
|
158
|
+
--id <id> Run one benchmark id
|
|
159
|
+
--limit <n> Limit selected benchmarks
|
|
160
|
+
--difficulty <easy|medium|hard>
|
|
161
|
+
--model <id> Forwarded verbatim to: codex exec -m <id>
|
|
162
|
+
--timeout <ms> Per-benchmark timeout (default 600000)
|
|
163
|
+
--record Write result JSON artifacts
|
|
164
|
+
--results-dir <path> Artifact directory
|
|
165
|
+
--keep-failures Keep failed sandbox directories
|
|
166
|
+
--use-env-openai-key Let codex inherit OPENAI_API_KEY instead of preferring ChatGPT auth
|
|
167
|
+
--allow-mcp Let codex load enabled MCP servers from local config
|
|
168
|
+
--disable-mcp <name[,name]> Explicit MCP server names to disable; defaults to auto-discover enabled servers
|
|
169
|
+
--dangerously-bypass-sandbox Pass codex's bypass flag. Use only inside an external sandbox.
|
|
170
|
+
--dry-run Verify fixture setup only; does not invoke codex
|
|
171
|
+
`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
main().catch((err) => {
|
|
175
|
+
console.error(err.stack || err.message);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
});
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const WALLE_SERVER_NAME = 'wall-e';
|
|
8
|
+
|
|
9
|
+
const MCP_TARGETS = Object.freeze([
|
|
10
|
+
{ id: 'claude-code', tool: 'Claude Code', configPath: '.claude/mcp.json', detectPath: '.claude', kind: 'json' },
|
|
11
|
+
{ id: 'claude-code-global', tool: 'Claude Code Global', configPath: '.claude.json', detectPath: '.claude.json', kind: 'json' },
|
|
12
|
+
{ id: 'codex', tool: 'Codex', configPath: '.codex/config.toml', detectPath: '.codex', kind: 'codex-toml' },
|
|
13
|
+
{ id: 'cursor', tool: 'Cursor', configPath: '.cursor/mcp.json', detectPath: '.cursor', kind: 'json' },
|
|
14
|
+
{ id: 'windsurf', tool: 'Windsurf', configPath: '.codeium/windsurf/mcp_config.json', detectPath: '.codeium/windsurf', kind: 'json' },
|
|
15
|
+
{ id: 'claude-desktop', tool: 'Claude Desktop', configPath: 'Library/Application Support/Claude/claude_desktop_config.json', detectPath: 'Library/Application Support/Claude', kind: 'json' },
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function wallEMcpUrl(wallePort) {
|
|
19
|
+
return `http://localhost:${Number(wallePort) || 3457}/mcp`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function wallEMcpConfig(wallePort) {
|
|
23
|
+
return { type: 'http', url: wallEMcpUrl(wallePort) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function _home(homeDir) {
|
|
27
|
+
return homeDir || process.env.HOME || process.env.USERPROFILE || '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function _targetPaths(target, homeDir) {
|
|
31
|
+
const home = _home(homeDir);
|
|
32
|
+
return {
|
|
33
|
+
detectPath: path.join(home, target.detectPath),
|
|
34
|
+
configPath: path.join(home, target.configPath),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function _isDetected(target, homeDir) {
|
|
39
|
+
const paths = _targetPaths(target, homeDir);
|
|
40
|
+
return fs.existsSync(paths.detectPath);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function _readJsonConfig(configPath) {
|
|
44
|
+
if (!fs.existsSync(configPath)) return { config: {}, exists: false };
|
|
45
|
+
try {
|
|
46
|
+
return { config: JSON.parse(fs.readFileSync(configPath, 'utf8')), exists: true };
|
|
47
|
+
} catch (err) {
|
|
48
|
+
return { config: null, exists: true, error: err };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function _writeJsonConfig(configPath, config) {
|
|
53
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
54
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function _jsonStatus(target, configPath, wallePort) {
|
|
58
|
+
const loaded = _readJsonConfig(configPath);
|
|
59
|
+
if (loaded.error) {
|
|
60
|
+
return { tool: target.tool, status: 'invalid_config', configPath, error: loaded.error.message };
|
|
61
|
+
}
|
|
62
|
+
const entry = loaded.config?.mcpServers?.[WALLE_SERVER_NAME];
|
|
63
|
+
if (entry && entry.url === wallEMcpUrl(wallePort)) {
|
|
64
|
+
return { tool: target.tool, status: 'configured', configPath };
|
|
65
|
+
}
|
|
66
|
+
if (entry) return { tool: target.tool, status: 'wrong_port', configPath, url: entry.url || '' };
|
|
67
|
+
return { tool: target.tool, status: 'not_configured', configPath };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function _ensureJsonTarget(target, configPath, wallePort) {
|
|
71
|
+
const loaded = _readJsonConfig(configPath);
|
|
72
|
+
if (loaded.error) {
|
|
73
|
+
return { tool: target.tool, action: 'error', configPath, error: loaded.error.message };
|
|
74
|
+
}
|
|
75
|
+
const config = loaded.config || {};
|
|
76
|
+
if (!config.mcpServers || typeof config.mcpServers !== 'object' || Array.isArray(config.mcpServers)) {
|
|
77
|
+
config.mcpServers = {};
|
|
78
|
+
}
|
|
79
|
+
const existing = config.mcpServers[WALLE_SERVER_NAME];
|
|
80
|
+
if (existing && existing.url === wallEMcpUrl(wallePort)) {
|
|
81
|
+
return { tool: target.tool, action: 'already_configured', configPath };
|
|
82
|
+
}
|
|
83
|
+
const action = existing ? 'updated' : 'added';
|
|
84
|
+
config.mcpServers[WALLE_SERVER_NAME] = wallEMcpConfig(wallePort);
|
|
85
|
+
_writeJsonConfig(configPath, config);
|
|
86
|
+
return { tool: target.tool, action, configPath };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function _codexBlockRegex() {
|
|
90
|
+
return /(^|\n)\[mcp_servers\.(?:"wall-e"|wall-e)\]\n[\s\S]*?(?=\n\[[^\n]+\]|\s*$)/m;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function _extractCodexWallEBlock(text) {
|
|
94
|
+
const match = String(text || '').match(_codexBlockRegex());
|
|
95
|
+
return match ? match[0].replace(/^\n/, '') : '';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function _extractTomlStringValue(block, key) {
|
|
99
|
+
const re = new RegExp(`^\\s*${key}\\s*=\\s*"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"\\s*$`, 'm');
|
|
100
|
+
const match = String(block || '').match(re);
|
|
101
|
+
if (!match) return '';
|
|
102
|
+
return match[1].replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function _codexStatus(target, configPath, wallePort) {
|
|
106
|
+
const text = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : '';
|
|
107
|
+
const block = _extractCodexWallEBlock(text);
|
|
108
|
+
if (!block) return { tool: target.tool, status: 'not_configured', configPath };
|
|
109
|
+
const url = _extractTomlStringValue(block, 'url');
|
|
110
|
+
if (url === wallEMcpUrl(wallePort)) return { tool: target.tool, status: 'configured', configPath };
|
|
111
|
+
return { tool: target.tool, status: 'wrong_port', configPath, url };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function _upsertCodexWallEBlock(text, wallePort) {
|
|
115
|
+
const body = String(text || '').replace(_codexBlockRegex(), '').replace(/\s+$/g, '');
|
|
116
|
+
const block = `[mcp_servers."wall-e"]\nurl = "${wallEMcpUrl(wallePort)}"\n`;
|
|
117
|
+
return (body ? `${body}\n\n` : '') + block;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function _ensureCodexTarget(target, configPath, wallePort) {
|
|
121
|
+
const existing = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : '';
|
|
122
|
+
const status = _codexStatus(target, configPath, wallePort);
|
|
123
|
+
if (status.status === 'configured') return { tool: target.tool, action: 'already_configured', configPath };
|
|
124
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
125
|
+
fs.writeFileSync(configPath, _upsertCodexWallEBlock(existing, wallePort));
|
|
126
|
+
return { tool: target.tool, action: status.status === 'wrong_port' ? 'updated' : 'added', configPath };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function detectMcpIntegrations(wallePort, opts = {}) {
|
|
130
|
+
const homeDir = opts.homeDir;
|
|
131
|
+
return MCP_TARGETS.map((target) => {
|
|
132
|
+
const paths = _targetPaths(target, homeDir);
|
|
133
|
+
if (!_isDetected(target, homeDir)) return { tool: target.tool, status: 'not_installed' };
|
|
134
|
+
if (target.kind === 'codex-toml') return _codexStatus(target, paths.configPath, wallePort);
|
|
135
|
+
return _jsonStatus(target, paths.configPath, wallePort);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function ensureMcpIntegrations(wallePort, opts = {}) {
|
|
140
|
+
const homeDir = opts.homeDir;
|
|
141
|
+
return MCP_TARGETS.map((target) => {
|
|
142
|
+
const paths = _targetPaths(target, homeDir);
|
|
143
|
+
if (!_isDetected(target, homeDir)) return { tool: target.tool, action: 'not_installed' };
|
|
144
|
+
if (target.kind === 'codex-toml') return _ensureCodexTarget(target, paths.configPath, wallePort);
|
|
145
|
+
return _ensureJsonTarget(target, paths.configPath, wallePort);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function injectMcpConfigs(wallePort, homeDir) {
|
|
150
|
+
return ensureMcpIntegrations(wallePort, { homeDir });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function _postMcp(port, payload, timeoutMs) {
|
|
154
|
+
return new Promise((resolve, reject) => {
|
|
155
|
+
const body = JSON.stringify(payload);
|
|
156
|
+
const req = http.request({
|
|
157
|
+
host: '127.0.0.1',
|
|
158
|
+
port: Number(port) || 3457,
|
|
159
|
+
path: '/mcp',
|
|
160
|
+
method: 'POST',
|
|
161
|
+
timeout: timeoutMs,
|
|
162
|
+
headers: {
|
|
163
|
+
'Content-Type': 'application/json',
|
|
164
|
+
'Content-Length': Buffer.byteLength(body),
|
|
165
|
+
},
|
|
166
|
+
}, (res) => {
|
|
167
|
+
const chunks = [];
|
|
168
|
+
res.on('data', chunk => chunks.push(chunk));
|
|
169
|
+
res.on('end', () => {
|
|
170
|
+
const raw = Buffer.concat(chunks).toString();
|
|
171
|
+
let json = null;
|
|
172
|
+
try { json = raw ? JSON.parse(raw) : null; } catch {}
|
|
173
|
+
resolve({ status: res.statusCode || 0, json, body: raw });
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
req.on('timeout', () => req.destroy(new Error('Timed out testing Wall-E MCP')));
|
|
177
|
+
req.on('error', reject);
|
|
178
|
+
req.write(body);
|
|
179
|
+
req.end();
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function testWallEMcpEndpoint(wallePort, opts = {}) {
|
|
184
|
+
const timeoutMs = opts.timeoutMs || 2000;
|
|
185
|
+
try {
|
|
186
|
+
const init = await _postMcp(wallePort, {
|
|
187
|
+
jsonrpc: '2.0',
|
|
188
|
+
id: 1,
|
|
189
|
+
method: 'initialize',
|
|
190
|
+
params: { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'wall-e-self-test', version: '1.0.0' } },
|
|
191
|
+
}, timeoutMs);
|
|
192
|
+
if (init.status !== 200 || !init.json?.result?.serverInfo) {
|
|
193
|
+
return { ok: false, status: init.status, error: 'initialize failed' };
|
|
194
|
+
}
|
|
195
|
+
const tools = await _postMcp(wallePort, {
|
|
196
|
+
jsonrpc: '2.0',
|
|
197
|
+
id: 2,
|
|
198
|
+
method: 'tools/list',
|
|
199
|
+
params: {},
|
|
200
|
+
}, timeoutMs);
|
|
201
|
+
const list = tools.json?.result?.tools;
|
|
202
|
+
if (tools.status !== 200 || !Array.isArray(list)) {
|
|
203
|
+
return { ok: false, status: tools.status, error: 'tools/list failed' };
|
|
204
|
+
}
|
|
205
|
+
return { ok: true, server: init.json.result.serverInfo, toolCount: list.length };
|
|
206
|
+
} catch (err) {
|
|
207
|
+
return { ok: false, error: err.message };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = {
|
|
212
|
+
MCP_TARGETS,
|
|
213
|
+
WALLE_SERVER_NAME,
|
|
214
|
+
detectMcpIntegrations,
|
|
215
|
+
ensureMcpIntegrations,
|
|
216
|
+
injectMcpConfigs,
|
|
217
|
+
testWallEMcpEndpoint,
|
|
218
|
+
wallEMcpConfig,
|
|
219
|
+
wallEMcpUrl,
|
|
220
|
+
};
|
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
const { toOpenAI, messagesToOpenAI, responseFromOpenAI } = require('./tool-adapter');
|
|
4
4
|
|
|
5
|
+
function isGemma4Model(model) {
|
|
6
|
+
return /^gemma4:/i.test(String(model || ''));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function resolveThinkValue({ model, thinking, reasoningEffort } = {}) {
|
|
10
|
+
if (thinking === false || thinking === 'disabled' || thinking === 'off') return false;
|
|
11
|
+
if (thinking === true || thinking === 'enabled' || thinking === 'on') return true;
|
|
12
|
+
if (['low', 'medium', 'high'].includes(thinking)) return thinking;
|
|
13
|
+
|
|
14
|
+
// Ollama's GPT-OSS models accept effort levels. Gemma4's thinking renderer
|
|
15
|
+
// accepts the boolean form, so keep Gemma4 on true when callers ask for any
|
|
16
|
+
// non-disabled reasoning effort.
|
|
17
|
+
if (reasoningEffort && reasoningEffort !== 'disabled') {
|
|
18
|
+
if (/^gpt-oss/i.test(String(model || '')) && ['low', 'medium', 'high'].includes(reasoningEffort)) {
|
|
19
|
+
return reasoningEffort;
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function buildOllamaOptions({ model, maxTokens, temperature, options } = {}) {
|
|
27
|
+
const out = { ...(options || {}) };
|
|
28
|
+
if (maxTokens != null) out.num_predict = maxTokens;
|
|
29
|
+
if (temperature != null) out.temperature = temperature;
|
|
30
|
+
if (isGemma4Model(model) && out.num_ctx == null) {
|
|
31
|
+
out.num_ctx = 16384;
|
|
32
|
+
}
|
|
33
|
+
return Object.keys(out).length ? out : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
5
36
|
/**
|
|
6
37
|
* Create an Ollama local-model LLM provider.
|
|
7
38
|
* Uses Ollama's OpenAI-compatible API — no npm dependency needed.
|
|
@@ -9,11 +40,13 @@ const { toOpenAI, messagesToOpenAI, responseFromOpenAI } = require('./tool-adapt
|
|
|
9
40
|
*/
|
|
10
41
|
function createOllamaProvider(config = {}) {
|
|
11
42
|
const baseUrl = (config.baseUrl || 'http://localhost:11434').replace(/\/+$/, '');
|
|
43
|
+
const defaultThink = config.think ?? false;
|
|
12
44
|
|
|
13
45
|
return {
|
|
14
46
|
type: 'ollama',
|
|
15
47
|
|
|
16
|
-
async chat({ model, messages, tools, system, maxTokens, temperature, signal } = {}) {
|
|
48
|
+
async chat({ model, messages, tools, system, maxTokens, temperature, signal, thinking, reasoningEffort, options } = {}) {
|
|
49
|
+
const modelId = model || 'gemma4:e4b';
|
|
17
50
|
const openaiMessages = [];
|
|
18
51
|
|
|
19
52
|
// System prompt as first message
|
|
@@ -30,13 +63,15 @@ function createOllamaProvider(config = {}) {
|
|
|
30
63
|
const useNativeApi = !tools || tools.length === 0;
|
|
31
64
|
|
|
32
65
|
if (useNativeApi) {
|
|
66
|
+
const resolvedThink = resolveThinkValue({ model: modelId, thinking, reasoningEffort });
|
|
33
67
|
const nativeBody = {
|
|
34
|
-
model:
|
|
68
|
+
model: modelId,
|
|
35
69
|
messages: openaiMessages,
|
|
36
70
|
stream: false,
|
|
71
|
+
think: resolvedThink !== undefined ? resolvedThink : defaultThink,
|
|
37
72
|
};
|
|
38
|
-
|
|
39
|
-
if (
|
|
73
|
+
const nativeOptions = buildOllamaOptions({ model: modelId, maxTokens, temperature, options });
|
|
74
|
+
if (nativeOptions) nativeBody.options = nativeOptions;
|
|
40
75
|
|
|
41
76
|
const start = Date.now();
|
|
42
77
|
const resp = await fetch(`${baseUrl}/api/chat`, {
|
|
@@ -54,6 +89,7 @@ function createOllamaProvider(config = {}) {
|
|
|
54
89
|
const raw = await resp.json();
|
|
55
90
|
const latencyMs = Date.now() - start;
|
|
56
91
|
const msg = raw.message || {};
|
|
92
|
+
const reasoningContent = msg.thinking || msg.reasoning || msg.reasoning_content || null;
|
|
57
93
|
|
|
58
94
|
// Extract accurate timing from native API (durations are in nanoseconds)
|
|
59
95
|
const evalDurationNs = raw.eval_duration || 0;
|
|
@@ -65,6 +101,7 @@ function createOllamaProvider(config = {}) {
|
|
|
65
101
|
|
|
66
102
|
return {
|
|
67
103
|
content: msg.content || null,
|
|
104
|
+
reasoningContent,
|
|
68
105
|
toolCalls: [],
|
|
69
106
|
stopReason: 'end_turn',
|
|
70
107
|
usage: {
|
|
@@ -76,17 +113,19 @@ function createOllamaProvider(config = {}) {
|
|
|
76
113
|
promptEvalDurationMs: promptEvalDurationNs / 1e6,
|
|
77
114
|
},
|
|
78
115
|
latencyMs,
|
|
79
|
-
model:
|
|
116
|
+
model: modelId,
|
|
80
117
|
provider: 'ollama',
|
|
81
118
|
raw,
|
|
82
119
|
};
|
|
83
120
|
}
|
|
84
121
|
|
|
85
122
|
// Fall back to OpenAI-compat API for tool use
|
|
123
|
+
const resolvedThink = resolveThinkValue({ model: modelId, thinking, reasoningEffort });
|
|
86
124
|
const body = {
|
|
87
|
-
model:
|
|
125
|
+
model: modelId,
|
|
88
126
|
messages: openaiMessages,
|
|
89
127
|
stream: false,
|
|
128
|
+
think: resolvedThink !== undefined ? resolvedThink : defaultThink,
|
|
90
129
|
};
|
|
91
130
|
|
|
92
131
|
const openaiTools = toOpenAI(tools);
|
|
@@ -132,7 +171,7 @@ function createOllamaProvider(config = {}) {
|
|
|
132
171
|
if (normalized.stopReason === 'tool_calls') stopReason = 'tool_use';
|
|
133
172
|
else if (normalized.stopReason === 'stop' || !normalized.stopReason) stopReason = 'end_turn';
|
|
134
173
|
|
|
135
|
-
return { ...normalized, stopReason, latencyMs: Date.now() - start, model:
|
|
174
|
+
return { ...normalized, stopReason, latencyMs: Date.now() - start, model: modelId, provider: 'ollama', raw };
|
|
136
175
|
},
|
|
137
176
|
|
|
138
177
|
async listModels() {
|
|
@@ -169,4 +208,4 @@ function createOllamaProvider(config = {}) {
|
|
|
169
208
|
};
|
|
170
209
|
}
|
|
171
210
|
|
|
172
|
-
module.exports = { createOllamaProvider };
|
|
211
|
+
module.exports = { createOllamaProvider, buildOllamaOptions, resolveThinkValue };
|
|
@@ -205,6 +205,7 @@ function responseFromOpenAI(resp) {
|
|
|
205
205
|
|
|
206
206
|
return {
|
|
207
207
|
content: msg.content || null,
|
|
208
|
+
reasoningContent: msg.reasoning_content || msg.reasoning || msg.thinking || null,
|
|
208
209
|
toolCalls: (msg.tool_calls || []).map((tc, index) => ({
|
|
209
210
|
id: tc.id || synthesizeOpenAIToolCallId(index),
|
|
210
211
|
name: tc.function.name,
|
|
@@ -1,22 +1,39 @@
|
|
|
1
1
|
const brain = require('../brain');
|
|
2
2
|
const eventBus = require('../events/event-bus');
|
|
3
3
|
|
|
4
|
+
function checkpointName(adapter) {
|
|
5
|
+
const name = adapter?.name || adapter?.constructor?.name || 'unknown';
|
|
6
|
+
return `ingest:${name}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function newerTimestamp(a, b) {
|
|
10
|
+
if (!a) return b || null;
|
|
11
|
+
if (!b) return a;
|
|
12
|
+
return b > a ? b : a;
|
|
13
|
+
}
|
|
14
|
+
|
|
4
15
|
async function runOnce(adapters) {
|
|
5
|
-
const
|
|
6
|
-
const since = checkpoint?.last_memory_at || null;
|
|
16
|
+
const legacyCheckpoint = brain.getCheckpoint('ingest');
|
|
7
17
|
let memoriesIngested = 0;
|
|
8
|
-
let latestTimestamp =
|
|
18
|
+
let latestTimestamp = legacyCheckpoint?.last_memory_at || null;
|
|
19
|
+
const adapterResults = {};
|
|
9
20
|
|
|
10
21
|
for (const adapter of adapters) {
|
|
22
|
+
const adapterCheckpointName = checkpointName(adapter);
|
|
23
|
+
const adapterCheckpoint = brain.getCheckpoint(adapterCheckpointName);
|
|
24
|
+
const since = adapterCheckpoint?.last_memory_at || null;
|
|
25
|
+
let adapterIngested = 0;
|
|
26
|
+
let adapterLatestTimestamp = since;
|
|
27
|
+
|
|
11
28
|
try {
|
|
12
29
|
const memories = await adapter.poll(since);
|
|
13
30
|
for (const mem of memories) {
|
|
31
|
+
adapterLatestTimestamp = newerTimestamp(adapterLatestTimestamp, mem.timestamp);
|
|
32
|
+
latestTimestamp = newerTimestamp(latestTimestamp, mem.timestamp);
|
|
14
33
|
const result = brain.insertMemory(mem);
|
|
15
34
|
if (result) {
|
|
16
35
|
memoriesIngested++;
|
|
17
|
-
|
|
18
|
-
latestTimestamp = mem.timestamp;
|
|
19
|
-
}
|
|
36
|
+
adapterIngested++;
|
|
20
37
|
if (mem.importance >= 0.8 || mem.memory_type === 'message_received') {
|
|
21
38
|
eventBus.emitHighImportance(result?.id, (mem.content || '').slice(0, 200));
|
|
22
39
|
}
|
|
@@ -59,17 +76,34 @@ async function runOnce(adapters) {
|
|
|
59
76
|
console.error('[ingest] Exchange chunking failed:', e.message);
|
|
60
77
|
}
|
|
61
78
|
}
|
|
79
|
+
brain.upsertCheckpoint(adapterCheckpointName, {
|
|
80
|
+
last_memory_at: adapterLatestTimestamp,
|
|
81
|
+
metadata: JSON.stringify({
|
|
82
|
+
adapter: adapter.name || adapter.constructor?.name || 'unknown',
|
|
83
|
+
memories_ingested: adapterIngested,
|
|
84
|
+
memories_seen: memories.length,
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
adapterResults[adapter.name || adapter.constructor?.name || 'unknown'] = {
|
|
88
|
+
memoriesIngested: adapterIngested,
|
|
89
|
+
memoriesSeen: memories.length,
|
|
90
|
+
lastMemoryAt: adapterLatestTimestamp,
|
|
91
|
+
};
|
|
62
92
|
} catch (err) {
|
|
63
93
|
console.error(`[ingest] Adapter ${adapter.name} failed:`, err.message);
|
|
94
|
+
adapterResults[adapter.name || adapter.constructor?.name || 'unknown'] = {
|
|
95
|
+
error: err.message,
|
|
96
|
+
lastMemoryAt: adapterLatestTimestamp,
|
|
97
|
+
};
|
|
64
98
|
}
|
|
65
99
|
}
|
|
66
100
|
|
|
67
101
|
brain.upsertCheckpoint('ingest', {
|
|
68
102
|
last_memory_at: latestTimestamp,
|
|
69
|
-
metadata: JSON.stringify({ memories_ingested: memoriesIngested }),
|
|
103
|
+
metadata: JSON.stringify({ memories_ingested: memoriesIngested, adapters: adapterResults }),
|
|
70
104
|
});
|
|
71
105
|
|
|
72
106
|
return { memoriesIngested };
|
|
73
107
|
}
|
|
74
108
|
|
|
75
|
-
module.exports = { runOnce };
|
|
109
|
+
module.exports = { checkpointName, runOnce };
|