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.
Files changed (52) hide show
  1. package/README.md +5 -5
  2. package/package.json +2 -2
  3. package/template/claude-task-manager/api-prompts.js +13 -0
  4. package/template/claude-task-manager/api-reviews.js +5 -2
  5. package/template/claude-task-manager/db.js +348 -15
  6. package/template/claude-task-manager/docs/app-update-refresh-protocol.md +69 -0
  7. package/template/claude-task-manager/docs/image-paste-ux.md +3 -0
  8. package/template/claude-task-manager/docs/ipad-web-preview.md +88 -0
  9. package/template/claude-task-manager/git-utils.js +146 -17
  10. package/template/claude-task-manager/lib/auth-rate-limit.js +23 -3
  11. package/template/claude-task-manager/lib/auth-rules.js +3 -0
  12. package/template/claude-task-manager/lib/document-review.js +33 -2
  13. package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +83 -0
  14. package/template/claude-task-manager/lib/mobile-auth-api.js +14 -0
  15. package/template/claude-task-manager/lib/restart-guard.js +68 -0
  16. package/template/claude-task-manager/lib/session-standup.js +36 -13
  17. package/template/claude-task-manager/lib/session-stream.js +11 -4
  18. package/template/claude-task-manager/lib/transport-security.js +50 -0
  19. package/template/claude-task-manager/lib/walle-transcript.js +16 -0
  20. package/template/claude-task-manager/lib/worktree-active-sync.js +6 -3
  21. package/template/claude-task-manager/public/css/reviews.css +10 -0
  22. package/template/claude-task-manager/public/css/setup.css +13 -0
  23. package/template/claude-task-manager/public/css/walle.css +145 -0
  24. package/template/claude-task-manager/public/index.html +539 -44
  25. package/template/claude-task-manager/public/ipad.html +363 -0
  26. package/template/claude-task-manager/public/js/document-review-links.js +196 -0
  27. package/template/claude-task-manager/public/js/message-renderer.js +14 -3
  28. package/template/claude-task-manager/public/js/reviews.js +30 -6
  29. package/template/claude-task-manager/public/js/setup.js +42 -2
  30. package/template/claude-task-manager/public/js/stream-view.js +20 -1
  31. package/template/claude-task-manager/public/js/walle.js +314 -18
  32. package/template/claude-task-manager/public/m/app.css +789 -11
  33. package/template/claude-task-manager/public/m/app.js +1070 -67
  34. package/template/claude-task-manager/public/m/claim.html +9 -2
  35. package/template/claude-task-manager/public/m/index.html +17 -10
  36. package/template/claude-task-manager/public/m/sw.js +1 -1
  37. package/template/claude-task-manager/server.js +365 -95
  38. package/template/claude-task-manager/session-integrity.js +4 -0
  39. package/template/docs/designs/2026-05-17-portkey-gateway-provider-ux.md +86 -35
  40. package/template/package.json +1 -1
  41. package/template/wall-e/api-walle.js +19 -1
  42. package/template/wall-e/brain.js +152 -6
  43. package/template/wall-e/chat.js +85 -0
  44. package/template/wall-e/coding-orchestrator.js +106 -12
  45. package/template/wall-e/http/model-admin.js +131 -0
  46. package/template/wall-e/lib/service-health.js +194 -0
  47. package/template/wall-e/llm/anthropic.js +7 -0
  48. package/template/wall-e/llm/client.js +46 -12
  49. package/template/wall-e/llm/openai.js +17 -2
  50. package/template/wall-e/llm/portkey-sync.js +201 -0
  51. package/template/wall-e/server.js +13 -0
  52. package/template/website/index.html +10 -10
@@ -263,21 +263,16 @@ function inferProviderFromAgent(agent) {
263
263
 
264
264
  function isWalleOwnedSession(session) {
265
265
  if (!session || typeof session !== 'object') return false;
266
+ if (session.walle_owned === true || session.walleOwned === true) return true;
267
+ if (session.native_walle_session === true || session.nativeWalleSession === true) return true;
266
268
  const explicitProvider = normalizeProvider(session.provider || session.providerId || session._providerId);
267
269
  if (explicitProvider === 'walle') return true;
268
270
  const explicitAgent = inferProviderFromAgent(session.agentType || session.agent || session.type);
269
271
  if (explicitAgent === 'walle') return true;
270
272
 
271
- const label = String(session.label || session.title || '').trim().toLowerCase();
273
+ // Labels, branches, and worktree names describe the task, not the runtime owner.
272
274
  const cmd = String(session.cmd || '').trim().toLowerCase();
273
- const branch = String(session.branch || session.gitBranch || session.worktreeStatus?.branch || '').trim().toLowerCase();
274
- const worktreePath = String(session.worktree_path || session.worktreePath || session.worktreeStatus?.worktreePath || session.cwd || '').trim().toLowerCase();
275
- const worktreeName = String(session.worktreeStatus?.worktreeName || worktreePath.split('/').filter(Boolean).pop() || '').trim().toLowerCase();
276
-
277
- if (cmd === 'walle coding' || cmd.startsWith('walle coding ')) return true;
278
- if (/^(wall-?e|walle)\s+coding\b/.test(label)) return true;
279
- if (worktreeName === 'walle-coding' || worktreeName.startsWith('walle-coding-')) return true;
280
- if (branch.includes('walle-coding')) return true;
275
+ if (cmd === 'walle' || cmd === 'walle coding' || cmd.startsWith('walle coding ')) return true;
281
276
  return false;
282
277
  }
283
278
 
@@ -406,10 +401,17 @@ function standupStatusForSession(session, status, summary, now = Date.now()) {
406
401
  function worktreeEvidence(worktree) {
407
402
  if (!worktree) return '';
408
403
  const dirtyFiles = Number(worktree.dirtyFiles || 0);
409
- const unmergedCommits = Number(worktree.unmergedCommits || 0);
404
+ const trackedDirtyFiles = worktree.trackedDirtyFiles != null
405
+ ? Number(worktree.trackedDirtyFiles || 0)
406
+ : Math.max(0, dirtyFiles - Number(worktree.untrackedFiles || 0));
407
+ const untrackedFiles = Number(worktree.untrackedFiles || 0);
408
+ const localCommits = worktree.isMain
409
+ ? Number(worktree.unpushedCommits || worktree.ahead || worktree.unmergedCommits || 0)
410
+ : Number(worktree.unmergedCommits || 0);
410
411
  const parts = [];
411
- if (dirtyFiles) parts.push(`${dirtyFiles} dirty`);
412
- if (unmergedCommits) parts.push(`${unmergedCommits} unmerged`);
412
+ if (trackedDirtyFiles) parts.push(`${trackedDirtyFiles} dirty`);
413
+ if (untrackedFiles) parts.push(`${untrackedFiles} untracked`);
414
+ if (localCommits) parts.push(worktree.isMain ? `${localCommits} local` : `${localCommits} unmerged`);
413
415
  if (!parts.length && worktree.summary) parts.push(worktree.summary);
414
416
  return parts.length ? `worktree ${parts.join(', ')}` : '';
415
417
  }
@@ -418,8 +420,12 @@ function hasReviewableWork(session, summaryText, progress) {
418
420
  const worktree = session?.worktreeStatus || session?.worktree || null;
419
421
  if (worktree) {
420
422
  if (worktree.needsAttention) return true;
421
- if (Number(worktree.dirtyFiles || 0) > 0) return true;
423
+ const trackedDirtyFiles = worktree.trackedDirtyFiles != null
424
+ ? Number(worktree.trackedDirtyFiles || 0)
425
+ : Math.max(0, Number(worktree.dirtyFiles || 0) - Number(worktree.untrackedFiles || 0));
426
+ if (trackedDirtyFiles > 0) return true;
422
427
  if (Number(worktree.unmergedCommits || 0) > 0) return true;
428
+ if (Number(worktree.unpushedCommits || 0) > 0) return true;
423
429
  }
424
430
  const text = `${summaryText || ''} ${progressText(progress)}`.toLowerCase();
425
431
  return /\b(done|completed|complete|verified|passed|ready for review|all work done)\b/.test(text);
@@ -664,6 +670,13 @@ function classifySessionStandup(session, signals = {}, now = Date.now()) {
664
670
  );
665
671
  const provider = inferSessionProvider(session);
666
672
  const modelProvider = inferSessionModelProvider(session);
673
+ const nativeWalleSession = session?.native_walle_session === true
674
+ || session?.nativeWalleSession === true
675
+ || session?.type === 'walle';
676
+ const walleOwned = session?.walle_owned === true
677
+ || session?.walleOwned === true
678
+ || isWalleOwnedSession(session);
679
+ const sessionType = session?.session_type || session?.sessionType || session?.runtime_type || session?.runtimeType || session?.type || '';
667
680
 
668
681
  const card = {
669
682
  id: session?.id || session?.sessionId || '',
@@ -671,6 +684,16 @@ function classifySessionStandup(session, signals = {}, now = Date.now()) {
671
684
  title,
672
685
  agent: session?.agentType || session?.agent || session?.type || 'session',
673
686
  provider,
687
+ type: session?.type || sessionType || '',
688
+ session_type: sessionType || '',
689
+ runtime_type: session?.runtime_type || session?.runtimeType || session?.type || '',
690
+ walle_owned: !!walleOwned,
691
+ walleOwned: !!walleOwned,
692
+ native_walle_session: !!nativeWalleSession,
693
+ nativeWalleSession: !!nativeWalleSession,
694
+ agentMode: session?.agentMode || session?.agent_mode || null,
695
+ agentKind: session?.agentKind || session?.agent_kind || (walleOwned && !nativeWalleSession ? 'walle-coding' : null),
696
+ taskType: session?.taskType || session?.task_type || (walleOwned && !nativeWalleSession ? 'coding' : null),
674
697
  modelProvider,
675
698
  model: inferSessionModel(session),
676
699
  cwd: session?.cwd || '',
@@ -136,10 +136,17 @@ class JsonlTailer {
136
136
  function extractText(content) {
137
137
  if (typeof content === 'string') return content;
138
138
  if (!Array.isArray(content)) return '';
139
- return content
140
- .filter(b => b.type === 'text')
141
- .map(b => b.text)
142
- .join('\n');
139
+ const parts = [];
140
+ let imageCount = 0;
141
+ for (const block of content) {
142
+ if (!block || typeof block !== 'object') continue;
143
+ if ((block.type === 'text' || block.type === 'input_text' || block.type === 'output_text') && block.text) {
144
+ parts.push(block.text);
145
+ } else if (block.type === 'image' || block.type === 'input_image' || block.type === 'image_ref') {
146
+ parts.push(`[Image #${++imageCount}]`);
147
+ }
148
+ }
149
+ return parts.join('\n');
143
150
  }
144
151
 
145
152
  function stripBase64Blobs(contentBlocks) {
@@ -94,11 +94,59 @@ function trustedForwardedHost(req) {
94
94
  );
95
95
  }
96
96
 
97
+ function cleanIpHeaderValue(value) {
98
+ let raw = stripHeaderQuotes(firstHeaderValue(value));
99
+ if (!raw || raw.toLowerCase() === 'unknown') return '';
100
+ if (raw.startsWith('[')) {
101
+ const end = raw.indexOf(']');
102
+ raw = end >= 0 ? raw.slice(1, end) : stripIpv6Brackets(raw);
103
+ } else if (net.isIP(raw) === 0) {
104
+ const colonCount = (raw.match(/:/g) || []).length;
105
+ if (colonCount === 1 && /^\d+\.\d+\.\d+\.\d+:\d+$/.test(raw)) {
106
+ raw = raw.replace(/:\d+$/, '');
107
+ }
108
+ }
109
+ raw = stripIpv6Brackets(raw);
110
+ return net.isIP(raw) ? raw : '';
111
+ }
112
+
113
+ function forwardedClientIp(req) {
114
+ const forwarded = parseForwardedHeader(req?.headers?.forwarded || '');
115
+ for (const value of [
116
+ req?.headers?.['x-forwarded-for'],
117
+ req?.headers?.['x-real-ip'],
118
+ req?.headers?.['cf-connecting-ip'],
119
+ forwarded.for,
120
+ ]) {
121
+ const ip = cleanIpHeaderValue(value);
122
+ if (ip) return ip;
123
+ }
124
+ return '';
125
+ }
126
+
127
+ function requestClientIp(req) {
128
+ const direct = stripIpv6Brackets(req?.socket?.remoteAddress || '');
129
+ if (!isLoopbackAddress(direct)) return direct;
130
+ const tunnelHost = trustedForwardedHost(req) || cleanHostHeaderValue(req?.headers?.host);
131
+ if (!isManagedHttpsTunnelHost(tunnelHost)) return direct;
132
+ const forwardedIp = forwardedClientIp(req);
133
+ if (forwardedIp && !isLoopbackAddress(forwardedIp)) return forwardedIp;
134
+ const tunnelName = hostHeaderName(tunnelHost);
135
+ return tunnelName ? `tunnel:${tunnelName}` : direct;
136
+ }
137
+
97
138
  function isLoopbackAddress(address) {
98
139
  const h = stripIpv6Brackets(address).toLowerCase();
99
140
  return LOOPBACK_HOSTS.has(h) || h === '::ffff:127.0.0.1';
100
141
  }
101
142
 
143
+ function primaryProcessIpLockoutExemptions(primaryHost) {
144
+ const out = new Set(['127.0.0.1', '::1']);
145
+ const host = stripIpv6Brackets(primaryHost).trim();
146
+ if (host && !isLoopbackHost(host)) out.add(host);
147
+ return Array.from(out);
148
+ }
149
+
102
150
  function hasNonLoopbackBrowserOrigin(req) {
103
151
  for (const value of [req?.headers?.origin, req?.headers?.referer, req?.headers?.referrer]) {
104
152
  const raw = firstHeaderValue(value);
@@ -296,6 +344,8 @@ module.exports = {
296
344
  normalizeBindHost,
297
345
  originAllowed,
298
346
  parseAllowedOrigins,
347
+ primaryProcessIpLockoutExemptions,
348
+ requestClientIp,
299
349
  requestBrowserOrigin,
300
350
  requestOrigin,
301
351
  requestProtocol,
@@ -165,6 +165,21 @@ function readLastUuid(filePath) {
165
165
  return '';
166
166
  }
167
167
 
168
+ function readSessionMeta(filePath) {
169
+ let raw;
170
+ try { raw = fs.readFileSync(filePath, 'utf8'); } catch { return null; }
171
+ const lines = raw.split('\n').filter(Boolean);
172
+ for (const line of lines) {
173
+ try {
174
+ const row = JSON.parse(line);
175
+ if (row && row.type === 'session_meta') return row;
176
+ } catch {
177
+ // Ignore malformed tail rows; the transcript writer is append-only.
178
+ }
179
+ }
180
+ return null;
181
+ }
182
+
168
183
  module.exports = {
169
184
  VERSION,
170
185
  WALLE_PROJECT_ENTRY,
@@ -177,4 +192,5 @@ module.exports = {
177
192
  appendPart,
178
193
  extractText,
179
194
  readLastUuid,
195
+ readSessionMeta,
180
196
  };
@@ -59,8 +59,11 @@ function activeSyncWorktreePolicy(wt, quiescence, opts = {}) {
59
59
  if (!wt.branch || wt.branch === 'HEAD' || wt.state === 'detached') {
60
60
  return { ok: false, status: quiescence.status, reason: 'Recover this worktree onto a branch before syncing from main.' };
61
61
  }
62
- if ((wt.dirtyFiles || 0) > 0) {
63
- return { ok: false, status: quiescence.status, reason: 'Commit or stash dirty files before syncing from main.' };
62
+ const trackedDirty = wt.trackedDirtyFiles != null
63
+ ? Number(wt.trackedDirtyFiles || 0)
64
+ : Math.max(0, Number(wt.dirtyFiles || 0) - Number(wt.untrackedFiles || 0));
65
+ if (trackedDirty > 0) {
66
+ return { ok: false, status: quiescence.status, reason: 'Commit or stash tracked dirty files before syncing from main.' };
64
67
  }
65
68
  if ((wt.ahead || 0) > 0 || wt.state === 'diverged') {
66
69
  return {
@@ -75,7 +78,7 @@ function activeSyncWorktreePolicy(wt, quiescence, opts = {}) {
75
78
  return {
76
79
  ok: true,
77
80
  status: quiescence.status,
78
- reason: 'Active session is idle and the worktree is clean; sync can proceed with confirmation.',
81
+ reason: 'Active session is idle and tracked files are clean; sync can proceed with confirmation.',
79
82
  };
80
83
  }
81
84
 
@@ -54,6 +54,16 @@
54
54
  .cr-btn.success { background: var(--green, #9ece6a); color: #1a1b26; border-color: var(--green); }
55
55
  .cr-btn.danger { background: var(--red, #f7768e); color: #fff; border-color: var(--red); }
56
56
  .cr-btn:disabled { opacity: 0.4; cursor: default; }
57
+ .ctm-doc-review-link {
58
+ color: var(--accent, #7aa2f7);
59
+ text-decoration: underline;
60
+ text-decoration-style: dotted;
61
+ text-underline-offset: 2px;
62
+ cursor: pointer;
63
+ }
64
+ .ctm-doc-review-link:hover {
65
+ color: var(--fg, #c0caf5);
66
+ }
57
67
 
58
68
  /* Commit summary card (shown at top of diff area when a commit is selected) */
59
69
  .cr-file-item.summary {
@@ -553,12 +553,25 @@
553
553
  font-size: 11px;
554
554
  line-height: 1.35;
555
555
  }
556
+ #setup-panel .setup-tunnel-actions {
557
+ display: flex;
558
+ flex-direction: column;
559
+ gap: 8px;
560
+ align-items: stretch;
561
+ }
556
562
  #setup-panel .setup-tunnel-primary {
557
563
  min-width: 116px;
558
564
  min-height: 38px;
559
565
  padding: 8px 14px;
560
566
  white-space: nowrap;
561
567
  }
568
+ #setup-panel .setup-tunnel-secondary {
569
+ min-width: 116px;
570
+ min-height: 32px;
571
+ padding: 6px 10px;
572
+ font-size: 12px;
573
+ white-space: nowrap;
574
+ }
562
575
  #setup-panel .setup-tunnel-steps {
563
576
  display: grid;
564
577
  grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -410,11 +410,156 @@
410
410
  border: 1px solid rgba(255,255,255,0.10);
411
411
  background: rgba(255,255,255,0.04);
412
412
  }
413
+ .we-health-card {
414
+ padding: 0;
415
+ overflow: hidden;
416
+ border-color: rgba(116,192,252,0.22);
417
+ background: rgba(28,36,58,0.72);
418
+ }
419
+ .we-health-card.error {
420
+ border-color: rgba(247,118,142,0.40);
421
+ background: rgba(247,118,142,0.075);
422
+ }
423
+ .we-health-card.warning {
424
+ border-color: rgba(250,176,5,0.36);
425
+ background: rgba(250,176,5,0.07);
426
+ }
427
+ .we-health-card.info {
428
+ border-color: rgba(116,192,252,0.28);
429
+ }
430
+ .we-health-header {
431
+ display: grid;
432
+ grid-template-columns: 10px minmax(0, 1fr) auto;
433
+ align-items: start;
434
+ gap: 10px;
435
+ padding: 12px;
436
+ }
437
+ .we-health-status-dot {
438
+ width: 8px;
439
+ height: 8px;
440
+ margin-top: 5px;
441
+ border-radius: 999px;
442
+ background: #9ece6a;
443
+ box-shadow: 0 0 0 4px rgba(158,206,106,0.12);
444
+ }
445
+ .we-health-card.error .we-health-status-dot {
446
+ background: #f7768e;
447
+ box-shadow: 0 0 0 4px rgba(247,118,142,0.14);
448
+ }
449
+ .we-health-card.warning .we-health-status-dot {
450
+ background: #fab005;
451
+ box-shadow: 0 0 0 4px rgba(250,176,5,0.14);
452
+ }
453
+ .we-health-card.info .we-health-status-dot {
454
+ background: #74c0fc;
455
+ box-shadow: 0 0 0 4px rgba(116,192,252,0.13);
456
+ }
413
457
  .we-service-alerts-title {
414
458
  font-weight: 700;
415
459
  margin-bottom: 4px;
416
460
  color: #d7dde8;
417
461
  }
462
+ .we-health-title-block {
463
+ min-width: 0;
464
+ }
465
+ .we-health-title {
466
+ color: var(--fg, #d7dde8);
467
+ font-size: 13px;
468
+ font-weight: 800;
469
+ line-height: 1.25;
470
+ }
471
+ .we-health-message {
472
+ margin-top: 3px;
473
+ color: var(--fg-muted, #9aa4b2);
474
+ line-height: 1.45;
475
+ }
476
+ .we-health-meta {
477
+ max-width: 280px;
478
+ color: var(--fg-muted, #9aa4b2);
479
+ font-size: 11px;
480
+ font-weight: 700;
481
+ line-height: 1.35;
482
+ text-align: right;
483
+ }
484
+ .we-health-current {
485
+ display: flex;
486
+ justify-content: space-between;
487
+ gap: 12px;
488
+ margin: 0 12px 12px;
489
+ padding: 11px 12px;
490
+ border: 1px solid rgba(247,118,142,0.30);
491
+ border-radius: 6px;
492
+ background: rgba(10,14,26,0.28);
493
+ }
494
+ .we-health-current-copy {
495
+ min-width: 0;
496
+ }
497
+ .we-health-current-title {
498
+ color: #fff;
499
+ font-weight: 800;
500
+ line-height: 1.3;
501
+ }
502
+ .we-health-current-body {
503
+ margin-top: 4px;
504
+ color: #f2c0c8;
505
+ line-height: 1.45;
506
+ overflow-wrap: anywhere;
507
+ }
508
+ .we-health-current-meta {
509
+ margin-top: 6px;
510
+ color: #9fb0c8;
511
+ font-size: 11px;
512
+ font-weight: 700;
513
+ }
514
+ .we-health-current-actions {
515
+ display: flex;
516
+ align-items: flex-start;
517
+ gap: 6px;
518
+ flex-shrink: 0;
519
+ }
520
+ .we-health-group {
521
+ border-top: 1px solid rgba(255,255,255,0.08);
522
+ }
523
+ .we-health-group summary {
524
+ display: flex;
525
+ align-items: center;
526
+ justify-content: space-between;
527
+ gap: 12px;
528
+ padding: 9px 12px;
529
+ color: #cfd7e6;
530
+ cursor: pointer;
531
+ font-weight: 800;
532
+ list-style: none;
533
+ }
534
+ .we-health-group summary::-webkit-details-marker {
535
+ display: none;
536
+ }
537
+ .we-health-group summary::before {
538
+ content: '›';
539
+ color: var(--fg-muted, #9aa4b2);
540
+ transform: rotate(0deg);
541
+ transition: transform 0.14s ease;
542
+ }
543
+ .we-health-group[open] summary::before {
544
+ transform: rotate(90deg);
545
+ }
546
+ .we-health-group summary span {
547
+ flex: 1;
548
+ min-width: 0;
549
+ }
550
+ .we-health-group summary strong {
551
+ display: inline-grid;
552
+ min-width: 22px;
553
+ height: 22px;
554
+ place-items: center;
555
+ border: 1px solid rgba(255,255,255,0.10);
556
+ border-radius: 999px;
557
+ color: #d7dde8;
558
+ font-size: 11px;
559
+ }
560
+ .we-health-group-body {
561
+ padding: 0 12px 9px;
562
+ }
418
563
  .we-service-alert-item {
419
564
  display: flex;
420
565
  align-items: center;