create-walle 0.9.21 → 0.9.22
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 +5 -5
- package/package.json +2 -2
- package/template/claude-task-manager/api-prompts.js +13 -0
- package/template/claude-task-manager/api-reviews.js +5 -2
- package/template/claude-task-manager/db.js +348 -15
- package/template/claude-task-manager/docs/app-update-refresh-protocol.md +69 -0
- package/template/claude-task-manager/docs/image-paste-ux.md +3 -0
- package/template/claude-task-manager/docs/ipad-web-preview.md +88 -0
- package/template/claude-task-manager/git-utils.js +146 -17
- package/template/claude-task-manager/lib/auth-rate-limit.js +23 -3
- package/template/claude-task-manager/lib/auth-rules.js +3 -0
- package/template/claude-task-manager/lib/document-review.js +33 -2
- package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +83 -0
- package/template/claude-task-manager/lib/mobile-auth-api.js +14 -0
- package/template/claude-task-manager/lib/restart-guard.js +68 -0
- package/template/claude-task-manager/lib/session-standup.js +36 -13
- package/template/claude-task-manager/lib/session-stream.js +11 -4
- package/template/claude-task-manager/lib/transport-security.js +50 -0
- package/template/claude-task-manager/lib/walle-transcript.js +16 -0
- package/template/claude-task-manager/lib/worktree-active-sync.js +6 -3
- package/template/claude-task-manager/public/css/reviews.css +10 -0
- package/template/claude-task-manager/public/css/setup.css +13 -0
- package/template/claude-task-manager/public/css/walle.css +145 -0
- package/template/claude-task-manager/public/index.html +539 -44
- package/template/claude-task-manager/public/ipad.html +363 -0
- package/template/claude-task-manager/public/js/document-review-links.js +196 -0
- package/template/claude-task-manager/public/js/message-renderer.js +14 -3
- package/template/claude-task-manager/public/js/reviews.js +30 -6
- package/template/claude-task-manager/public/js/setup.js +42 -2
- package/template/claude-task-manager/public/js/stream-view.js +20 -1
- package/template/claude-task-manager/public/js/walle.js +314 -18
- package/template/claude-task-manager/public/m/app.css +789 -11
- package/template/claude-task-manager/public/m/app.js +1070 -67
- package/template/claude-task-manager/public/m/claim.html +9 -2
- package/template/claude-task-manager/public/m/index.html +17 -10
- package/template/claude-task-manager/public/m/sw.js +1 -1
- package/template/claude-task-manager/server.js +365 -95
- package/template/claude-task-manager/session-integrity.js +4 -0
- package/template/docs/designs/2026-05-17-portkey-gateway-provider-ux.md +86 -35
- package/template/package.json +1 -1
- package/template/wall-e/api-walle.js +19 -1
- package/template/wall-e/brain.js +152 -6
- package/template/wall-e/chat.js +85 -0
- package/template/wall-e/coding-orchestrator.js +106 -12
- package/template/wall-e/http/model-admin.js +131 -0
- package/template/wall-e/lib/service-health.js +194 -0
- package/template/wall-e/llm/anthropic.js +7 -0
- package/template/wall-e/llm/client.js +46 -12
- package/template/wall-e/llm/openai.js +17 -2
- package/template/wall-e/llm/portkey-sync.js +201 -0
- package/template/wall-e/server.js +13 -0
- package/template/website/index.html +10 -10
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createClient } = require('./client');
|
|
4
|
+
const {
|
|
5
|
+
isPortkeyProviderConfig,
|
|
6
|
+
parseCustomHeaders,
|
|
7
|
+
} = require('./portkey');
|
|
8
|
+
const { findSupportedModel } = require('./supported-models');
|
|
9
|
+
|
|
10
|
+
const PORTKEY_SYNC_INTERVAL_MS = 60 * 60 * 1000;
|
|
11
|
+
const PORTKEY_SYNC_INITIAL_DELAY_MS = 15 * 1000;
|
|
12
|
+
|
|
13
|
+
function isPortkeyProviderRow(row = {}) {
|
|
14
|
+
return isPortkeyProviderConfig({
|
|
15
|
+
baseUrl: row.base_url || row.baseUrl,
|
|
16
|
+
customHeaders: row.custom_headers || row.customHeaders,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function modelLookupId(modelId) {
|
|
21
|
+
const id = String(modelId || '').trim();
|
|
22
|
+
const slash = id.lastIndexOf('/');
|
|
23
|
+
return slash >= 0 ? id.slice(slash + 1) : id;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function prettyGatewayModelName(model = {}) {
|
|
27
|
+
const id = String(model.id || model.model_id || model.modelId || '').trim();
|
|
28
|
+
if (model.name && model.name !== id) return model.name;
|
|
29
|
+
const base = modelLookupId(id);
|
|
30
|
+
return base || id;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function inferGatewayCapabilities(providerType, model = {}) {
|
|
34
|
+
if (Array.isArray(model.capabilities) || (model.capabilities && typeof model.capabilities === 'object')) {
|
|
35
|
+
return model.capabilities;
|
|
36
|
+
}
|
|
37
|
+
const id = String(model.id || model.model_id || model.modelId || '').trim();
|
|
38
|
+
const supported = findSupportedModel(providerType, id) || findSupportedModel(providerType, modelLookupId(id));
|
|
39
|
+
return supported?.capabilities || ['chat'];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function gatewayModelVerificationStatus(providerType, model = {}) {
|
|
43
|
+
const id = String(model.id || model.model_id || model.modelId || '').trim();
|
|
44
|
+
return findSupportedModel(providerType, id) || findSupportedModel(providerType, modelLookupId(id))
|
|
45
|
+
? 'verified'
|
|
46
|
+
: 'unverified';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function providerConfigFromRow(row = {}) {
|
|
50
|
+
const config = {};
|
|
51
|
+
if (row.api_key_encrypted) config.apiKey = row.api_key_encrypted;
|
|
52
|
+
if (row.base_url) config.baseUrl = row.base_url;
|
|
53
|
+
const customHeaders = parseCustomHeaders(row.custom_headers || row.customHeaders);
|
|
54
|
+
if (Object.keys(customHeaders).length > 0) config.customHeaders = customHeaders;
|
|
55
|
+
return config;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function syncPortkeyGatewayModels({
|
|
59
|
+
brainApi,
|
|
60
|
+
createClientFn = createClient,
|
|
61
|
+
providerId,
|
|
62
|
+
now = new Date().toISOString(),
|
|
63
|
+
} = {}) {
|
|
64
|
+
const brain = brainApi || require('../brain');
|
|
65
|
+
const providers = (brain.listModelProviders?.() || [])
|
|
66
|
+
.filter((provider) => provider.enabled !== 0 && provider.enabled !== false)
|
|
67
|
+
.filter((provider) => !providerId || provider.id === providerId)
|
|
68
|
+
.map((provider) => brain.getModelProviderWithKey?.(provider.id) || provider)
|
|
69
|
+
.filter(isPortkeyProviderRow);
|
|
70
|
+
|
|
71
|
+
const result = {
|
|
72
|
+
gateway: 'portkey',
|
|
73
|
+
synced_at: now,
|
|
74
|
+
routes: [],
|
|
75
|
+
imported: 0,
|
|
76
|
+
updated: 0,
|
|
77
|
+
errors: [],
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
for (const provider of providers) {
|
|
81
|
+
const route = {
|
|
82
|
+
provider_id: provider.id,
|
|
83
|
+
provider_type: provider.type,
|
|
84
|
+
models_seen: 0,
|
|
85
|
+
imported: 0,
|
|
86
|
+
updated: 0,
|
|
87
|
+
};
|
|
88
|
+
try {
|
|
89
|
+
const client = createClientFn(provider.type, providerConfigFromRow(provider));
|
|
90
|
+
if (!client || typeof client.listModels !== 'function') {
|
|
91
|
+
throw new Error('Provider does not expose listModels()');
|
|
92
|
+
}
|
|
93
|
+
const listed = await client.listModels({
|
|
94
|
+
exact: true,
|
|
95
|
+
liveOnly: true,
|
|
96
|
+
includeUnknown: true,
|
|
97
|
+
allowUnknown: true,
|
|
98
|
+
source: 'portkey',
|
|
99
|
+
});
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
for (const model of Array.isArray(listed) ? listed : []) {
|
|
102
|
+
const modelId = String(model && (model.id || model.model_id || model.modelId) || '').trim();
|
|
103
|
+
if (!modelId || seen.has(modelId)) continue;
|
|
104
|
+
seen.add(modelId);
|
|
105
|
+
const registryId = `${provider.id}:${modelId}`;
|
|
106
|
+
const existing = brain.getModelRegistryEntry?.(registryId);
|
|
107
|
+
brain.upsertModelRegistryEntry({
|
|
108
|
+
id: registryId,
|
|
109
|
+
providerId: provider.id,
|
|
110
|
+
modelId,
|
|
111
|
+
displayName: prettyGatewayModelName({ ...model, id: modelId }),
|
|
112
|
+
capabilities: inferGatewayCapabilities(provider.type, { ...model, id: modelId }),
|
|
113
|
+
costPer1mInput: model.costPer1mInput || model.cost_per_1m_input || null,
|
|
114
|
+
costPer1mOutput: model.costPer1mOutput || model.cost_per_1m_output || null,
|
|
115
|
+
maxContextTokens: model.maxContextTokens || model.max_context_tokens || null,
|
|
116
|
+
maxOutputTokens: model.maxOutputTokens || model.max_output_tokens || null,
|
|
117
|
+
speedTier: model.speedTier || model.speed_tier || 3,
|
|
118
|
+
enabled: true,
|
|
119
|
+
source: 'portkey',
|
|
120
|
+
gatewayType: 'portkey',
|
|
121
|
+
verificationStatus: gatewayModelVerificationStatus(provider.type, { ...model, id: modelId }),
|
|
122
|
+
lastSeenAt: now,
|
|
123
|
+
});
|
|
124
|
+
if (existing) route.updated += 1;
|
|
125
|
+
else route.imported += 1;
|
|
126
|
+
}
|
|
127
|
+
route.models_seen = seen.size;
|
|
128
|
+
result.imported += route.imported;
|
|
129
|
+
result.updated += route.updated;
|
|
130
|
+
result.routes.push(route);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
route.error = err && err.message ? err.message : String(err);
|
|
133
|
+
result.errors.push({ provider_id: provider.id, error: route.error });
|
|
134
|
+
result.routes.push(route);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (typeof brain.setKv === 'function') {
|
|
139
|
+
brain.setKv('model_gateway_sync:portkey:last_run_at', now);
|
|
140
|
+
if (result.errors.length === 0) {
|
|
141
|
+
brain.setKv('model_gateway_sync:portkey:last_success_at', now);
|
|
142
|
+
brain.setKv('model_gateway_sync:portkey:last_error', '');
|
|
143
|
+
} else {
|
|
144
|
+
brain.setKv('model_gateway_sync:portkey:last_error', result.errors.map((item) => `${item.provider_id}: ${item.error}`).join('; '));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function startPortkeyModelSyncLoop(options = {}) {
|
|
152
|
+
const intervalMs = Number.isFinite(options.intervalMs) ? options.intervalMs : PORTKEY_SYNC_INTERVAL_MS;
|
|
153
|
+
const initialDelayMs = Number.isFinite(options.initialDelayMs) ? options.initialDelayMs : PORTKEY_SYNC_INITIAL_DELAY_MS;
|
|
154
|
+
const log = options.log || console;
|
|
155
|
+
let stopped = false;
|
|
156
|
+
let timer = null;
|
|
157
|
+
let running = false;
|
|
158
|
+
|
|
159
|
+
const schedule = (delay) => {
|
|
160
|
+
if (stopped) return;
|
|
161
|
+
timer = setTimeout(run, delay);
|
|
162
|
+
if (timer && typeof timer.unref === 'function') timer.unref();
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const run = async () => {
|
|
166
|
+
if (stopped) return;
|
|
167
|
+
if (running) {
|
|
168
|
+
schedule(intervalMs);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
running = true;
|
|
172
|
+
try {
|
|
173
|
+
const result = await syncPortkeyGatewayModels(options);
|
|
174
|
+
if ((result.imported || result.updated) && log && typeof log.log === 'function') {
|
|
175
|
+
log.log(`[portkey-sync] synced ${result.imported} new and ${result.updated} existing model(s)`);
|
|
176
|
+
}
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (log && typeof log.warn === 'function') log.warn('[portkey-sync] failed:', err.message);
|
|
179
|
+
} finally {
|
|
180
|
+
running = false;
|
|
181
|
+
schedule(intervalMs);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
schedule(initialDelayMs);
|
|
186
|
+
return () => {
|
|
187
|
+
stopped = true;
|
|
188
|
+
if (timer) clearTimeout(timer);
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = {
|
|
193
|
+
PORTKEY_SYNC_INTERVAL_MS,
|
|
194
|
+
isPortkeyProviderRow,
|
|
195
|
+
modelLookupId,
|
|
196
|
+
inferGatewayCapabilities,
|
|
197
|
+
gatewayModelVerificationStatus,
|
|
198
|
+
providerConfigFromRow,
|
|
199
|
+
syncPortkeyGatewayModels,
|
|
200
|
+
startPortkeyModelSyncLoop,
|
|
201
|
+
};
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
const http = require('http');
|
|
3
3
|
const { handleWalleApi } = require('./api-walle');
|
|
4
4
|
const { getAllowedOrigin, isAuthorizedRequest } = require('./http/auth');
|
|
5
|
+
const { startPortkeyModelSyncLoop } = require('./llm/portkey-sync');
|
|
5
6
|
|
|
6
7
|
const PORT = parseInt(process.env.WALL_E_PORT) || 3457;
|
|
7
8
|
const HOST = process.env.WALL_E_HOST || '127.0.0.1';
|
|
@@ -155,6 +156,13 @@ function startServer(options = {}) {
|
|
|
155
156
|
readyResolve();
|
|
156
157
|
if (typeof setImmediate === 'function') setImmediate(autoConfigureWallEMcp);
|
|
157
158
|
else setTimeout(autoConfigureWallEMcp, 0);
|
|
159
|
+
if (process.env.WALLE_DISABLE_PORTKEY_SYNC !== '1') {
|
|
160
|
+
try {
|
|
161
|
+
server.stopPortkeyModelSync = startPortkeyModelSyncLoop();
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.warn(`[wall-e] Portkey model sync loop failed to start: ${err.message}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
158
166
|
});
|
|
159
167
|
}
|
|
160
168
|
|
|
@@ -177,6 +185,11 @@ function startServer(options = {}) {
|
|
|
177
185
|
readyReject(err);
|
|
178
186
|
}
|
|
179
187
|
});
|
|
188
|
+
server.on('close', () => {
|
|
189
|
+
try {
|
|
190
|
+
if (typeof server.stopPortkeyModelSync === 'function') server.stopPortkeyModelSync();
|
|
191
|
+
} catch {}
|
|
192
|
+
});
|
|
180
193
|
|
|
181
194
|
tryListen();
|
|
182
195
|
return server;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Wall-E — AI Coding Dashboard + Personal Agent</title>
|
|
7
|
-
<meta name="description" content="Run Claude Code, Codex, Gemini, and Aider sessions side by side. Manage prompts, queue tasks, review code and docs, use remote phone access, and let an AI agent build a second brain from your work life. Runs locally.">
|
|
7
|
+
<meta name="description" content="Run Claude Code, Codex, Gemini, and Aider sessions side by side. Manage prompts, queue tasks, review code and docs, use remote phone and tablet access, and let an AI agent build a second brain from your work life. Runs locally.">
|
|
8
8
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🤖</text></svg>">
|
|
9
9
|
<style>
|
|
10
10
|
:root {
|
|
@@ -241,7 +241,7 @@
|
|
|
241
241
|
<h1>Your AI Coding <span class="accent">Command Center</span> +<br>Personal <span class="accent">Agent</span></h1>
|
|
242
242
|
<p class="sub">
|
|
243
243
|
Run Claude Code, Codex, Gemini, Aider, OpenCode, and Cursor Agent sessions side by side. Manage prompts,
|
|
244
|
-
queue tasks, review code and docs, open a
|
|
244
|
+
queue tasks, review code and docs, open a touch-friendly remote UI for phone and tablet with live prompts and model controls, and let an AI agent build a second brain from your work life.
|
|
245
245
|
</p>
|
|
246
246
|
<div class="install-box" onclick="navigator.clipboard.writeText('npx create-walle install ./walle');this.querySelector('.copy-hint').textContent='Copied!'">
|
|
247
247
|
<code>npx create-walle install ./walle</code>
|
|
@@ -250,7 +250,7 @@
|
|
|
250
250
|
<div class="badge-row">
|
|
251
251
|
<span class="badge">MIT License</span>
|
|
252
252
|
<span class="badge">Local-first</span>
|
|
253
|
-
<span class="badge">
|
|
253
|
+
<span class="badge">Phone & Tablet Access</span>
|
|
254
254
|
<span class="badge">Code & Doc Review</span>
|
|
255
255
|
<span class="badge">SQLite</span>
|
|
256
256
|
<span class="badge">No Cloud Required</span>
|
|
@@ -281,7 +281,7 @@
|
|
|
281
281
|
<h2>One dashboard for every AI coding agent</h2>
|
|
282
282
|
<p class="desc">
|
|
283
283
|
Run Claude Code, Codex, Gemini CLI, and Aider sessions side by side.
|
|
284
|
-
Manage prompts, queue tasks, review code and docs, and respond from your phone.
|
|
284
|
+
Manage prompts, queue tasks, review code and docs, and respond from your phone or tablet.
|
|
285
285
|
</p>
|
|
286
286
|
<div class="features">
|
|
287
287
|
<div class="feature-card">
|
|
@@ -306,8 +306,8 @@
|
|
|
306
306
|
</div>
|
|
307
307
|
<div class="feature-card">
|
|
308
308
|
<span class="icon">📱</span>
|
|
309
|
-
<h3>Remote Phone Access</h3>
|
|
310
|
-
<p>Pair your phone with a QR code and use a
|
|
309
|
+
<h3>Remote Phone & Tablet Access</h3>
|
|
310
|
+
<p>Pair your phone or tablet with a QR code and use a responsive CTM UI through Microsoft Dev Tunnels, Tailscale, Cloudflare Tunnel, or Walle Remote, including live prompts and model controls.</p>
|
|
311
311
|
</div>
|
|
312
312
|
<div class="feature-card">
|
|
313
313
|
<span class="icon">Δ</span>
|
|
@@ -354,7 +354,7 @@
|
|
|
354
354
|
<div class="feature-card">
|
|
355
355
|
<span class="icon">📱</span>
|
|
356
356
|
<h3>Multi-Device</h3>
|
|
357
|
-
<p>Share your brain across machines via Dropbox, iCloud, or any file sync. Pair your phone when you need to monitor or respond away from the Mac.</p>
|
|
357
|
+
<p>Share your brain across machines via Dropbox, iCloud, or any file sync. Pair your phone or tablet when you need to monitor or respond away from the Mac.</p>
|
|
358
358
|
</div>
|
|
359
359
|
</div>
|
|
360
360
|
</section>
|
|
@@ -378,8 +378,8 @@
|
|
|
378
378
|
<p>Click through to connect Slack (OAuth), email, and calendar. All optional — Wall-E works without them.</p>
|
|
379
379
|
</div>
|
|
380
380
|
<div class="step">
|
|
381
|
-
<h3>Pair your
|
|
382
|
-
<p>Choose Microsoft Dev Tunnels, Tailscale, Cloudflare Tunnel, or Walle Remote from Setup and scan the QR code.</p>
|
|
381
|
+
<h3>Pair your device</h3>
|
|
382
|
+
<p>Choose Microsoft Dev Tunnels, Tailscale, Cloudflare Tunnel, or Walle Remote from Setup and scan the QR code on a phone or tablet.</p>
|
|
383
383
|
</div>
|
|
384
384
|
<div class="step">
|
|
385
385
|
<h3>Start working</h3>
|
|
@@ -405,7 +405,7 @@
|
|
|
405
405
|
<li>Multi-agent terminal multiplexer</li>
|
|
406
406
|
<li>Prompt store & task queue</li>
|
|
407
407
|
<li>Approval engine & model registry</li>
|
|
408
|
-
<li>Remote phone pairing &
|
|
408
|
+
<li>Remote phone/tablet pairing & responsive UI</li>
|
|
409
409
|
<li>Code and document review workspace</li>
|
|
410
410
|
</ul>
|
|
411
411
|
</div>
|