create-walle 0.9.13 → 0.9.15
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 +8 -3
- package/bin/create-walle.js +232 -32
- package/bin/mcp-inject.js +18 -53
- package/package.json +3 -1
- package/template/claude-task-manager/api-prompts.js +11 -2
- package/template/claude-task-manager/approval-agent.js +7 -0
- package/template/claude-task-manager/db.js +94 -75
- package/template/claude-task-manager/docs/session-standup-command-center-design.md +242 -0
- package/template/claude-task-manager/docs/session-tooltip-freshness-design.md +224 -0
- package/template/claude-task-manager/docs/session-ux-issue-review-2026-05-01.md +369 -0
- package/template/claude-task-manager/fuzzy-utils.js +10 -2
- package/template/claude-task-manager/git-utils.js +140 -10
- package/template/claude-task-manager/lib/agent-capabilities.js +1 -1
- package/template/claude-task-manager/lib/agent-presets.js +38 -5
- package/template/claude-task-manager/lib/codex-terminal-final.js +53 -0
- package/template/claude-task-manager/lib/ctm-session-context-api.js +222 -0
- package/template/claude-task-manager/lib/session-diagnostics.js +56 -0
- package/template/claude-task-manager/lib/session-history.js +309 -16
- package/template/claude-task-manager/lib/session-standup.js +409 -0
- package/template/claude-task-manager/lib/session-stream.js +253 -20
- 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-ctm-history.js +49 -6
- package/template/claude-task-manager/lib/walle-default-model.js +55 -0
- package/template/claude-task-manager/lib/walle-mcp-auto-config.js +66 -0
- package/template/claude-task-manager/lib/walle-supervisor.js +86 -19
- package/template/claude-task-manager/lib/walle-transcript.js +1 -3
- package/template/claude-task-manager/lib/worktree-cwd.js +82 -0
- package/template/claude-task-manager/package.json +1 -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 +71 -0
- package/template/claude-task-manager/public/index.html +2388 -429
- package/template/claude-task-manager/public/js/message-renderer.js +314 -35
- package/template/claude-task-manager/public/js/session-search-utils.js +185 -3
- package/template/claude-task-manager/public/js/session-status-precedence.js +125 -0
- package/template/claude-task-manager/public/js/setup.js +62 -19
- package/template/claude-task-manager/public/js/stream-view.js +396 -55
- package/template/claude-task-manager/public/js/terminal-restore-state.js +57 -0
- package/template/claude-task-manager/public/js/walle-session.js +234 -26
- package/template/claude-task-manager/public/js/walle.js +143 -2
- package/template/claude-task-manager/server.js +1402 -433
- package/template/claude-task-manager/session-integrity.js +77 -28
- package/template/claude-task-manager/workers/approval-widget-validator.js +15 -5
- package/template/claude-task-manager/workers/scrollback-worker.js +5 -6
- package/template/claude-task-manager/workers/state-detectors/codex.js +6 -0
- package/template/package.json +1 -1
- package/template/wall-e/agent-runners/claude-code.js +2 -0
- package/template/wall-e/agent.js +63 -8
- package/template/wall-e/api-walle.js +330 -52
- package/template/wall-e/brain.js +291 -42
- package/template/wall-e/chat.js +172 -15
- package/template/wall-e/coding/compaction-service.js +19 -5
- package/template/wall-e/coding/stream-processor.js +22 -2
- package/template/wall-e/coding/workspace-replay.js +1 -4
- package/template/wall-e/coding-orchestrator.js +250 -80
- package/template/wall-e/compat.js +0 -28
- package/template/wall-e/context/context-builder.js +3 -1
- package/template/wall-e/embeddings.js +2 -7
- package/template/wall-e/eval/agent-runner.js +30 -9
- package/template/wall-e/eval/benchmark-generator.js +21 -1
- package/template/wall-e/eval/benchmarks/chat-eval.json +66 -6
- package/template/wall-e/eval/benchmarks/coding-agent.json +0 -596
- package/template/wall-e/eval/cc-replay.js +1 -0
- package/template/wall-e/eval/codex-cli-baseline.js +633 -0
- package/template/wall-e/eval/debug-agent003.js +1 -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/eval/run-model-comparison.js +1 -0
- package/template/wall-e/eval/swebench-adapter.js +1 -0
- package/template/wall-e/evaluation/quorum-evaluator.js +0 -1
- package/template/wall-e/extraction/knowledge-extractor.js +1 -2
- package/template/wall-e/lib/mcp-integration.js +336 -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/loops/initiative.js +87 -2
- package/template/wall-e/mcp-server.js +872 -19
- package/template/wall-e/memory/ctm-context-client.js +230 -0
- package/template/wall-e/memory/ctm-session-context.js +1376 -0
- package/template/wall-e/prompts/coding/memory-protocol.md +6 -0
- package/template/wall-e/server.js +30 -1
- package/template/wall-e/skills/_bundled/memory-search/SKILL.md +8 -0
- 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/_bundled/slack-mentions/run.js +471 -188
- package/template/wall-e/skills/skill-planner.js +86 -4
- package/template/wall-e/slack/socket-mode-listener.js +276 -0
- package/template/wall-e/telemetry.js +70 -2
- 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 +4 -4
- package/template/builder-journal.md +0 -17
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const SERVER_EVENTS = new Set([
|
|
4
|
+
'check_started',
|
|
5
|
+
'check_completed',
|
|
6
|
+
'check_failed',
|
|
7
|
+
'update_available',
|
|
8
|
+
'api_check',
|
|
9
|
+
'apply_requested',
|
|
10
|
+
'apply_ignored',
|
|
11
|
+
'apply_started',
|
|
12
|
+
'apply_spawn_error',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const CLIENT_EVENTS = new Set([
|
|
16
|
+
'ui_prompt_shown',
|
|
17
|
+
'ui_action',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const SURFACES = new Set(['banner', 'wizard', 'api', 'startup', 'scheduled', 'manual', 'unknown']);
|
|
21
|
+
const ACTIONS = new Set(['later', 'skip', 'apply', 'apply_started', 'apply_failed', 'already_up_to_date']);
|
|
22
|
+
const RESULTS = new Set(['available', 'up_to_date', 'error', 'started', 'ignored']);
|
|
23
|
+
|
|
24
|
+
function cleanEnum(value, allowed, fallback = 'unknown') {
|
|
25
|
+
const normalized = String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]/g, '_');
|
|
26
|
+
return allowed.has(normalized) ? normalized : fallback;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function cleanVersion(value) {
|
|
30
|
+
const raw = String(value || '').trim().replace(/^v/i, '');
|
|
31
|
+
if (!raw || raw.length > 64) return '';
|
|
32
|
+
return /^[0-9A-Za-z.+_-]+$/.test(raw) ? raw : '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function cleanBool(value) {
|
|
36
|
+
return value === true ? true : value === false ? false : undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function errorKind(error) {
|
|
40
|
+
const message = String(error?.message || error || '').toLowerCase();
|
|
41
|
+
if (!message) return 'unknown';
|
|
42
|
+
if (/timeout|timed out|abort/.test(message)) return 'timeout';
|
|
43
|
+
if (/network|fetch|enotfound|eai_again|econn|socket/.test(message)) return 'network';
|
|
44
|
+
if (/npm registry returned 4\d\d/.test(message)) return 'registry_client_error';
|
|
45
|
+
if (/npm registry returned 5\d\d/.test(message)) return 'registry_server_error';
|
|
46
|
+
if (/spawn|enoent/.test(message)) return 'spawn_error';
|
|
47
|
+
return 'error';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sanitizePayload(details = {}) {
|
|
51
|
+
const payload = {};
|
|
52
|
+
const source = cleanEnum(details.source, SURFACES, '');
|
|
53
|
+
const surface = cleanEnum(details.surface, SURFACES, '');
|
|
54
|
+
const action = cleanEnum(details.action, ACTIONS, '');
|
|
55
|
+
const result = cleanEnum(details.result, RESULTS, '');
|
|
56
|
+
const currentVersion = cleanVersion(details.currentVersion || details.current || details.current_version);
|
|
57
|
+
const latestVersion = cleanVersion(details.latestVersion || details.latest || details.latest_version);
|
|
58
|
+
const updateAvailable = cleanBool(details.updateAvailable);
|
|
59
|
+
|
|
60
|
+
if (source) payload.source = source;
|
|
61
|
+
if (surface) payload.surface = surface;
|
|
62
|
+
if (action) payload.action = action;
|
|
63
|
+
if (result) payload.result = result;
|
|
64
|
+
if (currentVersion) payload.current_version = currentVersion;
|
|
65
|
+
if (latestVersion) payload.latest_version = latestVersion;
|
|
66
|
+
if (updateAvailable !== undefined) payload.update_available = updateAvailable;
|
|
67
|
+
if (details.error || details.errorKind) payload.error_kind = details.errorKind || errorKind(details.error);
|
|
68
|
+
|
|
69
|
+
return payload;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function trackUpdateTelemetry(telemetry, event, details = {}) {
|
|
73
|
+
const normalized = cleanEnum(event, new Set([...SERVER_EVENTS, ...CLIENT_EVENTS]), '');
|
|
74
|
+
if (!normalized) return false;
|
|
75
|
+
try {
|
|
76
|
+
telemetry?.track?.(`ctm_update_${normalized}`, sanitizePayload(details));
|
|
77
|
+
return true;
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function clientUpdateTelemetryEvent(body = {}) {
|
|
84
|
+
const rawEvent = String(body.event || '').trim().toLowerCase();
|
|
85
|
+
if (rawEvent === 'prompt_shown') {
|
|
86
|
+
return {
|
|
87
|
+
event: 'ui_prompt_shown',
|
|
88
|
+
details: sanitizePayload({
|
|
89
|
+
surface: body.surface,
|
|
90
|
+
currentVersion: body.currentVersion,
|
|
91
|
+
latestVersion: body.latestVersion,
|
|
92
|
+
}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (rawEvent === 'action') {
|
|
96
|
+
return {
|
|
97
|
+
event: 'ui_action',
|
|
98
|
+
details: sanitizePayload({
|
|
99
|
+
surface: body.surface,
|
|
100
|
+
action: body.action,
|
|
101
|
+
currentVersion: body.currentVersion,
|
|
102
|
+
latestVersion: body.latestVersion,
|
|
103
|
+
}),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
clientUpdateTelemetryEvent,
|
|
111
|
+
errorKind,
|
|
112
|
+
sanitizePayload,
|
|
113
|
+
trackUpdateTelemetry,
|
|
114
|
+
};
|
|
@@ -12,7 +12,7 @@ function applyWalleToolEvent(toolCalls, event) {
|
|
|
12
12
|
toolCalls.push({
|
|
13
13
|
name: event.tool || event.name || 'tool',
|
|
14
14
|
summary: event.summary || '',
|
|
15
|
-
args: event.args || null,
|
|
15
|
+
args: event.args || event.input || null,
|
|
16
16
|
status: 'working',
|
|
17
17
|
output: '',
|
|
18
18
|
});
|
|
@@ -29,6 +29,47 @@ function applyWalleToolEvent(toolCalls, event) {
|
|
|
29
29
|
return toolCalls;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function extractText(content) {
|
|
33
|
+
if (!content) return '';
|
|
34
|
+
if (typeof content === 'string') return content;
|
|
35
|
+
if (Array.isArray(content)) {
|
|
36
|
+
return content
|
|
37
|
+
.filter((block) => block && block.type === 'text' && block.text)
|
|
38
|
+
.map((block) => block.text)
|
|
39
|
+
.join('\n');
|
|
40
|
+
}
|
|
41
|
+
if (typeof content.text === 'string') return content.text;
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function messageText(entry) {
|
|
46
|
+
if (!entry || typeof entry !== 'object') return '';
|
|
47
|
+
if (entry.message && entry.message.content !== undefined) return extractText(entry.message.content);
|
|
48
|
+
if (entry.content !== undefined) return extractText(entry.content);
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeToolEvent(entry) {
|
|
53
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
54
|
+
if (entry.type === 'tool_call' || entry.type === 'tool_result' || entry.type === 'tool_done') {
|
|
55
|
+
return entry;
|
|
56
|
+
}
|
|
57
|
+
if (entry.type !== 'walle_part') return null;
|
|
58
|
+
const data = entry.data && typeof entry.data === 'object' ? entry.data : {};
|
|
59
|
+
if (entry.partType !== 'tool_call' && entry.partType !== 'tool_result' && entry.partType !== 'tool_done') return null;
|
|
60
|
+
return {
|
|
61
|
+
type: entry.partType,
|
|
62
|
+
tool: data.tool || data.name || 'tool',
|
|
63
|
+
name: data.name || data.tool || 'tool',
|
|
64
|
+
summary: data.summary || '',
|
|
65
|
+
args: data.args || data.input || null,
|
|
66
|
+
input: data.input || data.args || null,
|
|
67
|
+
output: data.output || data.result || '',
|
|
68
|
+
result: data.result || data.output || '',
|
|
69
|
+
error: data.error || null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
32
73
|
function readWalleCtmHistory(filePath) {
|
|
33
74
|
let raw = '';
|
|
34
75
|
try { raw = fs.readFileSync(filePath, 'utf8'); } catch { return []; }
|
|
@@ -41,20 +82,21 @@ function readWalleCtmHistory(filePath) {
|
|
|
41
82
|
if (entry.type === 'user') {
|
|
42
83
|
messages.push({
|
|
43
84
|
role: 'user',
|
|
44
|
-
content: entry
|
|
85
|
+
content: messageText(entry),
|
|
45
86
|
timestamp: entry.timestamp || 0,
|
|
46
87
|
});
|
|
47
88
|
continue;
|
|
48
89
|
}
|
|
49
|
-
|
|
50
|
-
|
|
90
|
+
const toolEvent = normalizeToolEvent(entry);
|
|
91
|
+
if (toolEvent) {
|
|
92
|
+
applyWalleToolEvent(pendingToolCalls, toolEvent);
|
|
51
93
|
continue;
|
|
52
94
|
}
|
|
53
95
|
if (entry.type === 'assistant') {
|
|
54
96
|
messages.push({
|
|
55
97
|
role: 'assistant',
|
|
56
|
-
content: entry
|
|
57
|
-
model: entry.model || '',
|
|
98
|
+
content: messageText(entry),
|
|
99
|
+
model: entry.message?.model || entry.model || entry.modelId || '',
|
|
58
100
|
latency_ms: entry.latencyMs || entry.latency_ms || 0,
|
|
59
101
|
timestamp: entry.timestamp || 0,
|
|
60
102
|
toolCalls: entry.toolCalls || cloneToolCalls(pendingToolCalls),
|
|
@@ -68,5 +110,6 @@ function readWalleCtmHistory(filePath) {
|
|
|
68
110
|
module.exports = {
|
|
69
111
|
applyWalleToolEvent,
|
|
70
112
|
cloneToolCalls,
|
|
113
|
+
extractText,
|
|
71
114
|
readWalleCtmHistory,
|
|
72
115
|
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { sanitizeSetupProviderType } = require('./setup-provider-config');
|
|
4
|
+
|
|
5
|
+
function _kv(brain, key) {
|
|
6
|
+
try {
|
|
7
|
+
if (brain && typeof brain.getKv === 'function') return brain.getKv(key) || '';
|
|
8
|
+
if (brain && typeof brain.getDb === 'function') {
|
|
9
|
+
return brain.getDb().prepare('SELECT value FROM brain_metadata WHERE key = ?').get(key)?.value || '';
|
|
10
|
+
}
|
|
11
|
+
} catch (e) {
|
|
12
|
+
if (process.env.CTM_DEBUG_WALLE_DEFAULTS === '1') {
|
|
13
|
+
console.warn('[walle-default-model] metadata lookup failed:', e.message);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function _providerDefault(provider, providerRegistry) {
|
|
20
|
+
if (!provider) return '';
|
|
21
|
+
try {
|
|
22
|
+
const registry = providerRegistry || require('../../wall-e/llm/registry');
|
|
23
|
+
if (typeof registry.ensureBootstrapped === 'function') registry.ensureBootstrapped();
|
|
24
|
+
return registry.getDefaultModel(provider) || '';
|
|
25
|
+
} catch (e) {
|
|
26
|
+
if (process.env.CTM_DEBUG_WALLE_DEFAULTS === '1') {
|
|
27
|
+
console.warn('[walle-default-model] provider default lookup failed:', e.message);
|
|
28
|
+
}
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveWalleDefaultModelSelection(opts = {}) {
|
|
34
|
+
const env = opts.env || process.env;
|
|
35
|
+
const brain = opts.brain || null;
|
|
36
|
+
const provider = sanitizeSetupProviderType(
|
|
37
|
+
_kv(brain, 'walle_provider')
|
|
38
|
+
|| env.WALLE_PROVIDER
|
|
39
|
+
|| 'anthropic'
|
|
40
|
+
) || 'anthropic';
|
|
41
|
+
const model = _kv(brain, 'walle_model_' + provider)
|
|
42
|
+
|| _kv(brain, 'walle_model')
|
|
43
|
+
|| env.WALLE_MODEL
|
|
44
|
+
|| _providerDefault(provider, opts.providerRegistry)
|
|
45
|
+
|| '';
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
model_id: model,
|
|
49
|
+
model_provider: provider,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
resolveWalleDefaultModelSelection,
|
|
55
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
let lastEnsureAt = 0;
|
|
6
|
+
|
|
7
|
+
function isMcpAutoConfigDisabled(env = process.env) {
|
|
8
|
+
const value = String(env.WALLE_MCP_AUTO_CONFIG || '').toLowerCase();
|
|
9
|
+
return value === '0' || value === 'false' || value === 'off';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isMcpCapableAgentCommand(cmd) {
|
|
13
|
+
const base = path.basename(String(cmd || '')).toLowerCase();
|
|
14
|
+
return base === 'claude' || base === 'codex';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function loadMcpIntegration() {
|
|
18
|
+
return require(path.resolve(__dirname, '..', '..', 'wall-e', 'lib', 'mcp-integration'));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function ensureWalleMcpForAgentSession({
|
|
22
|
+
cmd,
|
|
23
|
+
wallePort,
|
|
24
|
+
homeDir,
|
|
25
|
+
env = process.env,
|
|
26
|
+
logger = console,
|
|
27
|
+
now = Date.now(),
|
|
28
|
+
minIntervalMs = 30000,
|
|
29
|
+
} = {}) {
|
|
30
|
+
if (!isMcpCapableAgentCommand(cmd)) return { skipped: 'not_mcp_capable_agent' };
|
|
31
|
+
if (isMcpAutoConfigDisabled(env)) return { skipped: 'disabled' };
|
|
32
|
+
if (lastEnsureAt && now - lastEnsureAt < minIntervalMs) return { skipped: 'throttled' };
|
|
33
|
+
lastEnsureAt = now;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const { ensureMcpIntegrations } = loadMcpIntegration();
|
|
37
|
+
const results = ensureMcpIntegrations(wallePort, { homeDir: homeDir || env.HOME || process.env.HOME });
|
|
38
|
+
const changed = results.filter(r => r.kind === 'mcp_config' && (r.action === 'added' || r.action === 'updated'));
|
|
39
|
+
const instructionChanged = results.filter(r => r.kind === 'agent_instructions' && (r.action === 'added' || r.action === 'updated'));
|
|
40
|
+
const errors = results.filter(r => r.action === 'error');
|
|
41
|
+
if (changed.length) {
|
|
42
|
+
logger.log(`[ctm] Auto-configured Wall-E MCP for ${changed.length} AI tool(s) before agent launch`);
|
|
43
|
+
}
|
|
44
|
+
if (instructionChanged.length) {
|
|
45
|
+
logger.log(`[ctm] Updated Wall-E memory-routing instructions for ${instructionChanged.length} AI agent(s) before launch`);
|
|
46
|
+
}
|
|
47
|
+
if (errors.length) {
|
|
48
|
+
logger.warn(`[ctm] Wall-E MCP auto-config skipped ${errors.length} invalid config file(s): ${errors.map(r => r.tool).join(', ')}`);
|
|
49
|
+
}
|
|
50
|
+
return { results, changed: changed.length, instructionChanged: instructionChanged.length, errors: errors.length };
|
|
51
|
+
} catch (err) {
|
|
52
|
+
logger.warn(`[ctm] Wall-E MCP auto-config failed: ${err.message}`);
|
|
53
|
+
return { error: err.message };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function _resetWalleMcpAutoConfigState() {
|
|
58
|
+
lastEnsureAt = 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
ensureWalleMcpForAgentSession,
|
|
63
|
+
isMcpAutoConfigDisabled,
|
|
64
|
+
isMcpCapableAgentCommand,
|
|
65
|
+
_resetWalleMcpAutoConfigState,
|
|
66
|
+
};
|
|
@@ -66,6 +66,7 @@ const defaultProcessOps = {
|
|
|
66
66
|
function createWalleSupervisor({
|
|
67
67
|
ctmDir,
|
|
68
68
|
configDir,
|
|
69
|
+
ctmPort = process.env.CTM_PORT || 3456,
|
|
69
70
|
wallePort,
|
|
70
71
|
instanceTag,
|
|
71
72
|
walleToken = null,
|
|
@@ -124,6 +125,44 @@ function createWalleSupervisor({
|
|
|
124
125
|
return pidSet;
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
function readPidFile() {
|
|
129
|
+
try {
|
|
130
|
+
const pid = parseInt(fs.readFileSync(pidFile(), 'utf8').trim(), 10);
|
|
131
|
+
return pid || null;
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isAlive(pid) {
|
|
138
|
+
if (!pid || pid === (processOps.pid || process.pid)) return false;
|
|
139
|
+
try {
|
|
140
|
+
processOps.kill(pid, 0);
|
|
141
|
+
return true;
|
|
142
|
+
} catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function writePidFile(pid) {
|
|
148
|
+
try {
|
|
149
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
150
|
+
fs.writeFileSync(pidFile(), String(pid));
|
|
151
|
+
return true;
|
|
152
|
+
} catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function removePidFile() {
|
|
158
|
+
try {
|
|
159
|
+
fs.unlinkSync(pidFile());
|
|
160
|
+
return true;
|
|
161
|
+
} catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
127
166
|
async function killAll() {
|
|
128
167
|
const pidSet = await collectManagedPids();
|
|
129
168
|
if (pidSet.size === 0) return waitForPortFree();
|
|
@@ -175,6 +214,8 @@ function createWalleSupervisor({
|
|
|
175
214
|
|
|
176
215
|
const childEnv = {
|
|
177
216
|
...process.env,
|
|
217
|
+
CTM_PORT: String(ctmPort),
|
|
218
|
+
CTM_API_BASE_URL: `http://127.0.0.1:${ctmPort}`,
|
|
178
219
|
WALL_E_PORT: String(wallePort),
|
|
179
220
|
WALLE_INSTANCE_TAG: instanceTag,
|
|
180
221
|
};
|
|
@@ -225,27 +266,53 @@ function createWalleSupervisor({
|
|
|
225
266
|
return nextChild.pid;
|
|
226
267
|
}
|
|
227
268
|
|
|
228
|
-
function getStatus() {
|
|
229
|
-
|
|
230
|
-
let
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
running
|
|
235
|
-
pid
|
|
236
|
-
|
|
269
|
+
async function getStatus() {
|
|
270
|
+
const savedPid = readPidFile();
|
|
271
|
+
let stalePid = null;
|
|
272
|
+
|
|
273
|
+
if (childPid && isAlive(childPid)) {
|
|
274
|
+
return {
|
|
275
|
+
running: true,
|
|
276
|
+
pid: childPid,
|
|
277
|
+
pidSource: 'child',
|
|
278
|
+
stalePid: savedPid && savedPid !== childPid ? savedPid : null,
|
|
279
|
+
repairedPidFile: savedPid !== childPid ? writePidFile(childPid) : false,
|
|
280
|
+
};
|
|
237
281
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if (orphanPid && orphanPid !== process.pid) {
|
|
242
|
-
process.kill(orphanPid, 0);
|
|
243
|
-
running = true;
|
|
244
|
-
pid = orphanPid;
|
|
245
|
-
}
|
|
246
|
-
} catch {}
|
|
282
|
+
|
|
283
|
+
if (savedPid && isAlive(savedPid)) {
|
|
284
|
+
return { running: true, pid: savedPid, pidSource: 'pid_file' };
|
|
247
285
|
}
|
|
248
|
-
|
|
286
|
+
if (savedPid) stalePid = savedPid;
|
|
287
|
+
|
|
288
|
+
const title = processTitle();
|
|
289
|
+
let discovered = [];
|
|
290
|
+
try {
|
|
291
|
+
const results = await Promise.all([
|
|
292
|
+
processOps.findPidsByExactCommand ? processOps.findPidsByExactCommand(title) : [],
|
|
293
|
+
processOps.findListeningPidsByExactCommand ? processOps.findListeningPidsByExactCommand(wallePort, title) : [],
|
|
294
|
+
]);
|
|
295
|
+
discovered = [...new Set(results.flat().filter(pid => pid && isAlive(pid)))];
|
|
296
|
+
} catch {}
|
|
297
|
+
|
|
298
|
+
if (discovered.length > 0) {
|
|
299
|
+
const pid = discovered[0];
|
|
300
|
+
return {
|
|
301
|
+
running: true,
|
|
302
|
+
pid,
|
|
303
|
+
pidSource: 'discovered',
|
|
304
|
+
stalePid,
|
|
305
|
+
repairedPidFile: writePidFile(pid),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
running: false,
|
|
311
|
+
pid: null,
|
|
312
|
+
pidSource: stalePid ? 'stale_pid_file' : 'none',
|
|
313
|
+
stalePid,
|
|
314
|
+
removedStalePidFile: stalePid ? removePidFile() : false,
|
|
315
|
+
};
|
|
249
316
|
}
|
|
250
317
|
|
|
251
318
|
function apiStop(req, res) {
|
|
@@ -15,9 +15,7 @@ function defaultSessionsDir(env = process.env) {
|
|
|
15
15
|
if (env.WALL_E_DATA_DIR && env.WALL_E_DATA_DIR !== path.join(os.homedir(), '.walle', 'data')) {
|
|
16
16
|
return path.join(env.WALL_E_DATA_DIR, 'sessions');
|
|
17
17
|
}
|
|
18
|
-
|
|
19
|
-
return path.join(env.CTM_DATA_DIR, 'sessions');
|
|
20
|
-
}
|
|
18
|
+
// CTM_DATA_DIR belongs to the task manager; Wall-E transcripts stay in Wall-E-owned storage.
|
|
21
19
|
return path.join(os.homedir(), '.walle', 'sessions');
|
|
22
20
|
}
|
|
23
21
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execFileSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
function fail(error, status) {
|
|
8
|
+
return { ok: false, status: status || 400, error };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function gitText(cwd, args) {
|
|
12
|
+
return execFileSync('git', args, {
|
|
13
|
+
cwd,
|
|
14
|
+
encoding: 'utf8',
|
|
15
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
16
|
+
timeout: 5000,
|
|
17
|
+
}).trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function firstWorktreePath(porcelain) {
|
|
21
|
+
for (const line of String(porcelain || '').split('\n')) {
|
|
22
|
+
if (line.startsWith('worktree ')) return line.slice('worktree '.length).trim();
|
|
23
|
+
}
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function realpathOrResolve(p) {
|
|
28
|
+
try {
|
|
29
|
+
return fs.realpathSync(p);
|
|
30
|
+
} catch (_) {
|
|
31
|
+
return path.resolve(p);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveWorktreeRepoRoot(cwd) {
|
|
36
|
+
if (typeof cwd !== 'string' || !cwd.trim()) return fail('cwd is required');
|
|
37
|
+
const raw = cwd.trim();
|
|
38
|
+
if (raw.includes('~')) {
|
|
39
|
+
return fail('cwd must be an absolute path with no `~` (the literal-tilde is the source of ghost worktrees).');
|
|
40
|
+
}
|
|
41
|
+
if (!path.isAbsolute(raw)) return fail('cwd must be an absolute path');
|
|
42
|
+
|
|
43
|
+
let realCwd;
|
|
44
|
+
try {
|
|
45
|
+
realCwd = fs.realpathSync(raw);
|
|
46
|
+
} catch (_) {
|
|
47
|
+
return fail('cwd does not exist');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let stat;
|
|
51
|
+
try {
|
|
52
|
+
stat = fs.statSync(realCwd);
|
|
53
|
+
} catch (_) {
|
|
54
|
+
return fail('cwd does not exist');
|
|
55
|
+
}
|
|
56
|
+
if (!stat.isDirectory()) return fail('cwd must be a directory');
|
|
57
|
+
|
|
58
|
+
let topLevel;
|
|
59
|
+
try {
|
|
60
|
+
topLevel = gitText(realCwd, ['rev-parse', '--show-toplevel']);
|
|
61
|
+
} catch (_) {
|
|
62
|
+
return fail('cwd must be inside a Git work tree');
|
|
63
|
+
}
|
|
64
|
+
if (!topLevel) return fail('cwd must be inside a Git work tree');
|
|
65
|
+
|
|
66
|
+
let primaryRoot = '';
|
|
67
|
+
try {
|
|
68
|
+
primaryRoot = firstWorktreePath(gitText(realCwd, ['worktree', 'list', '--porcelain']));
|
|
69
|
+
} catch (_) {
|
|
70
|
+
primaryRoot = '';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
ok: true,
|
|
75
|
+
cwd: realpathOrResolve(primaryRoot || topLevel),
|
|
76
|
+
requestedCwd: realCwd,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
resolveWorktreeRepoRoot,
|
|
82
|
+
};
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"dev": "node server.js --dev",
|
|
23
23
|
"test": "node --test --test-force-exit \"tests/*.test.js\" \"tests/**/*.test.js\"",
|
|
24
24
|
"test:render": "npx playwright test --config tests/rendering/playwright.config.js --grep-invert 'soak'",
|
|
25
|
+
"test:session-ux-smoke": "npx playwright test --config tests/rendering/playwright.config.js scenarios/blank-tab-retry-recovery.spec.js scenarios/blank-tab-worker-saturation.spec.js scenarios/codex-panel-restore-gap.spec.js scenarios/codex-clear-scrollback.spec.js scenarios/session-status-typing.spec.js scenarios/session-search-dedupe.spec.js scenarios/walle-session-default-model.spec.js --retries=0",
|
|
25
26
|
"test:render:blank": "npx playwright test --config tests/rendering/playwright.config.js scenarios/blank-tab-real-click.spec.js --retries=0",
|
|
26
27
|
"test:render:blank:native": "CTM_RENDER_NATIVE_GPU=1 npx playwright test --config tests/rendering/playwright.config.js scenarios/blank-tab-real-click.spec.js --headed --retries=0",
|
|
27
28
|
"test:soak": "SOAK_DURATION_MINUTES=5 npx playwright test --config tests/rendering/playwright.config.js --grep 'soak'"
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Codex MCP permission form:
|
|
4
|
+
//
|
|
5
|
+
// Calling wall-e.walle_memory_status({})
|
|
6
|
+
// Field 1/1 ...
|
|
7
|
+
// Allow the wall-e MCP server to run tool "walle_memory_status"?
|
|
8
|
+
// 1. Allow
|
|
9
|
+
// 2. Allow for this session
|
|
10
|
+
// 3. Always allow
|
|
11
|
+
// 4. Cancel
|
|
12
|
+
// enter to submit | esc to cancel
|
|
13
|
+
|
|
14
|
+
const CALLING_RE = /\bCalling\s+([A-Za-z0-9_.-]+)\.([A-Za-z0-9_.-]+)\s*\(/i;
|
|
15
|
+
const QUESTION_RE = /Allow the ([A-Za-z0-9_.-]+) MCP server to run tool ["']?([A-Za-z0-9_.-]+)["']?\?/i;
|
|
16
|
+
const ANCHOR_RE = /enter to submit\s*\|\s*esc to cancel/i;
|
|
17
|
+
const ALLOW_RE = /^\s*[❯›> ]?\s*1\.\s*Allow\b/i;
|
|
18
|
+
const OPTION_RE = /^\s*[❯›> ]?\s*(\d+)\.\s*(Allow for this session|Always allow|Allow|Cancel)\b/i;
|
|
19
|
+
|
|
20
|
+
function extractMcpTool(lines) {
|
|
21
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
22
|
+
const q = lines[i].match(QUESTION_RE);
|
|
23
|
+
if (q) return { server: q[1], tool: q[2], questionIdx: i };
|
|
24
|
+
}
|
|
25
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
26
|
+
const c = lines[i].match(CALLING_RE);
|
|
27
|
+
if (c) return { server: c[1], tool: c[2], questionIdx: -1 };
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseOptions(lines) {
|
|
33
|
+
const out = {
|
|
34
|
+
allowShortcut: '1',
|
|
35
|
+
sessionShortcut: null,
|
|
36
|
+
alwaysShortcut: null,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
const match = line.match(OPTION_RE);
|
|
41
|
+
if (!match) continue;
|
|
42
|
+
const index = match[1];
|
|
43
|
+
const label = match[2].toLowerCase();
|
|
44
|
+
if (label === 'allow for this session') out.sessionShortcut = index;
|
|
45
|
+
else if (label === 'always allow') out.alwaysShortcut = index;
|
|
46
|
+
else if (label === 'allow') out.allowShortcut = index;
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = {
|
|
52
|
+
id: 'codex-mcp',
|
|
53
|
+
name: 'Codex MCP Permission',
|
|
54
|
+
|
|
55
|
+
detect(text) {
|
|
56
|
+
const lines = String(text || '').split('\n').map(l => l.trim()).filter(Boolean);
|
|
57
|
+
if (lines.length < 5) return false;
|
|
58
|
+
if (!lines.slice(-5).some(l => ANCHOR_RE.test(l))) return false;
|
|
59
|
+
if (!lines.some(l => ALLOW_RE.test(l))) return false;
|
|
60
|
+
return lines.some(l => QUESTION_RE.test(l)) || lines.some(l => CALLING_RE.test(l));
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
parse(text) {
|
|
64
|
+
const lines = String(text || '').split('\n').map(l => l.trim()).filter(Boolean);
|
|
65
|
+
const mcp = extractMcpTool(lines);
|
|
66
|
+
if (!mcp) return null;
|
|
67
|
+
|
|
68
|
+
let allowIdx = -1;
|
|
69
|
+
for (let i = 0; i < lines.length; i++) {
|
|
70
|
+
if (ALLOW_RE.test(lines[i])) { allowIdx = i; break; }
|
|
71
|
+
}
|
|
72
|
+
if (allowIdx < 0) return null;
|
|
73
|
+
|
|
74
|
+
const options = parseOptions(lines.slice(allowIdx, allowIdx + 8));
|
|
75
|
+
const ctxAnchor = mcp.questionIdx >= 0 ? mcp.questionIdx : allowIdx;
|
|
76
|
+
const ctxStart = Math.max(0, ctxAnchor - 5);
|
|
77
|
+
const ctxEnd = Math.min(lines.length, allowIdx + 8);
|
|
78
|
+
const command = `${mcp.server}.${mcp.tool}({})`;
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
toolName: 'MCP',
|
|
82
|
+
command: command.slice(0, 2000),
|
|
83
|
+
warning: '',
|
|
84
|
+
fullContext: lines.slice(ctxStart, ctxEnd).join('\n').slice(0, 2000),
|
|
85
|
+
hasAllowAll: !!options.sessionShortcut,
|
|
86
|
+
approveShortcut: options.allowShortcut,
|
|
87
|
+
approveAllShortcut: options.sessionShortcut,
|
|
88
|
+
alwaysAllowShortcut: options.alwaysShortcut,
|
|
89
|
+
mcpServer: mcp.server,
|
|
90
|
+
mcpTool: mcp.tool,
|
|
91
|
+
providerId: 'codex-mcp',
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
approveKeystroke(ctx) {
|
|
96
|
+
return ctx.approveAllShortcut || ctx.approveShortcut || '1';
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
approveAllKeystroke(ctx) {
|
|
100
|
+
return ctx.approveAllShortcut || null;
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
requiresEnter: true,
|
|
104
|
+
};
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// Detection order: most distinctive signals first to minimize false positives.
|
|
5
5
|
|
|
6
6
|
const claudeCode = require('./claude-code');
|
|
7
|
+
const codexMcp = require('./codex-mcp');
|
|
7
8
|
const codex = require('./codex');
|
|
8
9
|
const gemini = require('./gemini');
|
|
9
10
|
const amazonQ = require('./amazon-q');
|
|
@@ -19,6 +20,7 @@ const opencode = require('./opencode');
|
|
|
19
20
|
// Numbered-widget providers (Claude Code, Codex, Cursor, OpenCode, Kimi)
|
|
20
21
|
// run before inline [Y/n] providers (Gemini, Aider, Amazon Q, Cline, Copilot).
|
|
21
22
|
const providers = [
|
|
23
|
+
codexMcp,
|
|
22
24
|
claudeCode,
|
|
23
25
|
codex,
|
|
24
26
|
cursor,
|