@galda/cli 0.10.1 → 0.10.2

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/app/index.html CHANGED
@@ -3,7 +3,8 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Agent Manager</title>
6
+ <title>Galda</title>
7
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%233B6BFF'/%3E%3Cstop offset='1' stop-color='%230E2AA8'/%3E%3C/linearGradient%3E%3C/defs%3E%3Ccircle cx='16' cy='16' r='11' fill='none' stroke='url(%23g)' stroke-width='5'/%3E%3C/svg%3E">
7
8
  <!-- brand fonts FINAL (v45 §1): the 3 adopted faces self-host in /fonts.css — the old
8
9
  Google Fonts Inter/JetBrains request was removed (offline-safe, no stray families) -->
9
10
  <link rel="stylesheet" href="/theme.css">
@@ -1129,8 +1130,8 @@
1129
1130
  #fsRoot .fsproj.on i{background:var(--green)}
1130
1131
  /* running project = solid saturated green dot with a breathing glow — never dims to
1131
1132
  grey, independent of selection (HANDOFF-v45 §11, flagship .proj.run port) */
1132
- #fsRoot .fsproj.run i{background:#22c55e;box-shadow:0 0 0 0 rgba(34,197,94,.55);animation:fsrundot 2s ease-in-out infinite}
1133
- @keyframes fsrundot{0%,100%{box-shadow:0 0 0 0 rgba(34,197,94,.5)}50%{box-shadow:0 0 5px 1.5px rgba(34,197,94,.35)}}
1133
+ #fsRoot .fsproj.run i{background:var(--green);box-shadow:0 0 0 0 color-mix(in srgb,var(--green) 55%,transparent);animation:fsrundot 2s ease-in-out infinite}
1134
+ @keyframes fsrundot{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--green) 50%,transparent)}50%{box-shadow:0 0 5px 1.5px color-mix(in srgb,var(--green) 35%,transparent)}}
1134
1135
  @media (prefers-reduced-motion:reduce){#fsRoot .fsproj.run i{animation:none}}
1135
1136
  #fsRoot .fsproj .pname{min-width:0;overflow:hidden;text-overflow:ellipsis}
1136
1137
  #fsRoot .fsproj .num{margin-left:auto;font-size:10px;color:var(--ink3);flex:0 0 auto}
@@ -1168,7 +1169,7 @@
1168
1169
  .fsctxmenu button svg{width:14px;height:14px;flex:0 0 14px;fill:none;stroke:currentColor;stroke-width:1.8}
1169
1170
  .fsctxmenu .ctxsep{height:1px;margin:4px 6px;background:var(--hair)}
1170
1171
  .fsctxmenu .swix{opacity:.5;margin-right:9px;font:600 10px/1 var(--mono);flex:0 0 auto}
1171
- .fsctxmenu .swrun{margin-left:8px;width:6px;height:6px;border-radius:50%;background:#22c55e;flex:0 0 auto}
1172
+ .fsctxmenu .swrun{margin-left:8px;width:6px;height:6px;border-radius:50%;background:var(--green);flex:0 0 auto}
1172
1173
  #fsRoot .ghostbtn{height:26px;padding:0 8px;border:0;border-radius:7px;background:transparent;color:var(--ink3);font:inherit;font-size:11.5px;text-align:left;cursor:pointer;white-space:nowrap}
1173
1174
  #fsRoot .ghostbtn:hover{background:var(--hover);color:var(--ink)}
1174
1175
  #fsRoot .morebtn{display:grid;place-items:center;width:26px;padding:0}
@@ -1213,7 +1214,7 @@
1213
1214
  color:var(--ink3);background:transparent;border:0;padding:0;margin-left:10px;cursor:pointer}
1214
1215
  #fsRoot .chd .runsw[hidden]{display:none}
1215
1216
  #fsRoot .chd .runsw:hover{color:var(--ink)}
1216
- #fsRoot .chd .runsw i{width:5px;height:5px;border-radius:50%;background:#22c55e;flex:0 0 5px;animation:fsrundot 2s ease-in-out infinite}
1217
+ #fsRoot .chd .runsw i{width:5px;height:5px;border-radius:50%;background:var(--green);flex:0 0 5px;animation:fsrundot 2s ease-in-out infinite}
1217
1218
  #fsRoot .chd .runsw svg{width:9px;height:9px;flex:0 0 9px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
1218
1219
  #fsRoot .runmenu{display:none;flex-direction:column;gap:1px;background:var(--panel);border-radius:10px;
1219
1220
  box-shadow:0 4px 14px rgba(20,23,31,.12),0 0 0 1px var(--hair);padding:4px;position:absolute;top:22px;right:0;min-width:210px;z-index:6}
@@ -1222,7 +1223,7 @@
1222
1223
  color:var(--ink2);font:inherit;font-size:11.5px;text-align:left;cursor:pointer;white-space:nowrap;overflow:hidden}
1223
1224
  #fsRoot .runmenu .rmrow:hover{background:var(--hover);color:var(--ink)}
1224
1225
  #fsRoot .runmenu .rmrow.on{color:var(--ink);font-weight:600}
1225
- #fsRoot .runmenu .rmrow i{width:6px;height:6px;border-radius:50%;background:#22c55e;flex:0 0 6px;animation:fsrundot 2s ease-in-out infinite}
1226
+ #fsRoot .runmenu .rmrow i{width:6px;height:6px;border-radius:50%;background:var(--green);flex:0 0 6px;animation:fsrundot 2s ease-in-out infinite}
1226
1227
  #fsRoot .runmenu .rmrow .rmnm{overflow:hidden;text-overflow:ellipsis;flex:1 1 auto}
1227
1228
  #fsRoot .runmenu .rmrow .rmpct{flex:0 0 auto;font:600 10px/1 var(--mono);color:var(--ink3)}
1228
1229
  /* "Now doing" — Underline pattern (HANDOFF-v45 §10 confirmed default, flagship
@@ -1232,7 +1233,7 @@
1232
1233
  max-width:var(--fscenterw,760px);width:100%;margin:-4px auto 14px;position:relative;z-index:1}
1233
1234
  #fsRoot .nowdoing[hidden]{display:none}
1234
1235
  #fsRoot .nowdoing .nd-lb{font:600 8.5px/1 var(--mono);letter-spacing:.12em;color:var(--ink3);text-transform:uppercase}
1235
- #fsRoot .nowdoing .nd-run{width:9px;height:9px;flex:0 0 9px;border-radius:50%;background:#22c55e;animation:fsrundot 2s ease-in-out infinite}
1236
+ #fsRoot .nowdoing .nd-run{width:9px;height:9px;flex:0 0 9px;border-radius:50%;background:var(--green);animation:fsrundot 2s ease-in-out infinite}
1236
1237
  #fsRoot .nowdoing .nd-no{font-size:12.5px;font-weight:600;color:var(--ink3);font-variant-numeric:tabular-nums}
1237
1238
  #fsRoot .nowdoing .nd-nm{font-size:14px;font-weight:600;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:0 1 auto}
1238
1239
  #fsRoot .nowdoing .nd-pct{margin-left:auto;font:600 11px/1 var(--mono);color:var(--ink2);font-variant-numeric:tabular-nums;flex:0 0 auto}
@@ -1673,6 +1674,14 @@
1673
1674
  radial-gradient(80% 72% at 84% 7%,var(--lgA,rgba(216,228,255,.56)),transparent 62%),
1674
1675
  radial-gradient(120% 108% at 95% -5%,var(--lgB,rgba(150,170,220,.42)),transparent 74%),#07080a;
1675
1676
  background-attachment:fixed}
1677
+ /* light OFF (H17, flagship f9c75ad): kill the dominant top-right glow + every light layer →
1678
+ pure flat black (LP #111112). Overrides both the --glowc wash (any layout) and the flagship
1679
+ radial stack. Accent goes LP green #3DDC97 via var(--green) (set in theme.css). */
1680
+ body[data-theme="midnight"][data-light="off"],
1681
+ body[data-theme="midnight"][data-light="off"][data-layout="flagship"]{background:#111112 !important}
1682
+ /* send button on flat black: dark round + white arrow (was invisible white-on-white) */
1683
+ body[data-theme="midnight"][data-light="off"] .send{background:#17181c !important;box-shadow:inset 0 0 0 1px rgba(255,255,255,.16) !important}
1684
+ body[data-theme="midnight"][data-light="off"] .send svg{stroke:#fff !important}
1676
1685
  body[data-theme="midnight"] #fsRoot .fscard{background:rgba(10,11,16,.66);-webkit-backdrop-filter:blur(22px) saturate(1.3);backdrop-filter:blur(22px) saturate(1.3);
1677
1686
  box-shadow:0 1px 0 rgba(255,255,255,.05) inset,0 0 0 1px var(--hair),0 24px 60px -24px rgba(0,0,0,.72)}
1678
1687
 
@@ -1908,6 +1917,9 @@
1908
1917
  #fsClawdHero{display:none;position:absolute;left:50%;top:38%;transform:translate(-50%,-50%);text-align:center;z-index:1;pointer-events:none}
1909
1918
  #fsClawdHero b{display:block;margin-top:14px;font:600 16px/1.3 'Schibsted Grotesk',-apple-system,system-ui,sans-serif;letter-spacing:-.012em;color:var(--ink)} /* hero wordmark = Schibsted (v45 §1) */
1910
1919
  #fsClawdHero span{display:block;margin-top:5px;font:500 10px/1.4 var(--mono);letter-spacing:.08em;color:var(--ink3)}
1920
+ /* Welcome P1 (H17 item 4, flagship f9c75ad): grounded hop + clean-cut eyes on settle + our CSS confetti.
1921
+ Two pixel frames (neutral black-eyes / laughing), no arm-raise, no official confetti. reduce-motion=static. */
1922
+ #fsClawdHero .cstage{position:relative;width:170px;height:132px;display:flex;align-items:flex-end;justify-content:center;margin:0 auto 2px}#fsClawdHero .cw{position:relative;width:150px;transform-origin:50% 100%;will-change:transform;animation:chHop 2.4s linear infinite}#fsClawdHero .cw img{position:absolute;left:0;bottom:0;width:100%;display:block;image-rendering:pixelated}#fsClawdHero .cw .chNeu{animation:chNeu 2.4s linear infinite}#fsClawdHero .cw .chLau{opacity:0;animation:chLau 2.4s linear infinite}#fsClawdHero .chConf{position:absolute;left:50%;top:34px;width:0;height:0;pointer-events:none;z-index:3}#fsClawdHero .chConf i{position:absolute;width:4px;height:7px;border-radius:1.5px;opacity:0;transform:translate(-50%,-50%);animation:chPop 2.4s cubic-bezier(.2,.7,.3,1) infinite}@keyframes chHop{0%,52%{transform:translateY(0) scale(1,1)}55%{transform:translateY(0) scale(1.08,.85)}58%{transform:translateY(-16px) scale(.95,1.14)}61%{transform:translateY(-27px) scale(.98,1.05)}64%{transform:translateY(-31px) scale(1,1)}67%{transform:translateY(-26px) scale(.98,1.05)}70%{transform:translateY(-14px) scale(.95,1.12)}73%{transform:translateY(0) scale(1.1,.82)}77%{transform:translateY(-4px) scale(.99,1.02)}80%,100%{transform:translateY(0) scale(1,1)}}@keyframes chLau{0%,53.8%{opacity:0}54%,85.8%{opacity:1}86%,100%{opacity:0}}@keyframes chNeu{0%,53.8%{opacity:1}54%,85.8%{opacity:0}86%,100%{opacity:1}}@keyframes chPop{0%,54%{opacity:0;transform:translate(0,6px) rotate(0) scale(.5)}60%{opacity:1;transform:translate(calc(var(--dx)*.5),calc(var(--dy)*.5)) rotate(calc(var(--rot)*.5)) scale(1)}72%{opacity:1;transform:translate(var(--dx),var(--dy)) rotate(var(--rot))}100%{opacity:0;transform:translate(calc(var(--dx)*1.1),calc(var(--dy) + 44px)) rotate(calc(var(--rot)*1.4)) scale(.9)}}@media (prefers-reduced-motion:reduce){#fsClawdHero .cw,#fsClawdHero .cw .chNeu,#fsClawdHero .cw .chLau{animation:none}#fsClawdHero .cw .chLau{opacity:0}#fsClawdHero .chConf{display:none}}
1911
1923
  #fsRoot.clawdhero #fsClawdHero{display:block}
1912
1924
  #fsRoot .cc-who{display:flex;align-items:center;gap:9px;padding:2px 0 6px}
1913
1925
  #fsRoot .cc-who .cs-n{font-size:12px;font-weight:600;color:var(--ink)}
@@ -2037,7 +2049,7 @@
2037
2049
  <canvas id="fx"></canvas>
2038
2050
  <div class="top">
2039
2051
  <div class="mark"></div>
2040
- <div class="brand">Agent Manager</div>
2052
+ <div class="brand">Galda</div>
2041
2053
  <div class="spacer"></div>
2042
2054
  <!-- Feedback 7: layout switcher — flip the arrangement live. Increment 1:
2043
2055
  現行 / タスク中央 / 3列カンバン (CSS reflow). 中央フロー・1列 = increment 2. -->
@@ -5187,7 +5199,7 @@ async function deleteProject(p){
5187
5199
  if (!p) return;
5188
5200
  if (!(await confirmDelete({
5189
5201
  title: 'Delete this project?',
5190
- body: 'This removes the project from Agent Manager. This cannot be undone.',
5202
+ body: 'This removes the project from Galda. This cannot be undone.',
5191
5203
  preview: p.name,
5192
5204
  }))) return;
5193
5205
  try {
@@ -5310,7 +5322,8 @@ function applyFsBand(){
5310
5322
  // Clawd HERO (§4.5 初期状態): big Clawd + "Claude Code / CONNECTED" in the
5311
5323
  // implementing column. Visibility is state-driven (renderFsFeed toggles
5312
5324
  // #fsRoot.clawdhero when the column has zero activity — no timers).
5313
- $('fsClawdHero').innerHTML = CLAWD(88) + '<b>Claude Code</b><span>CONNECTED · WAITING FOR GOALS</span>';
5325
+ $('fsClawdHero').innerHTML = '<div class="cstage"><div class="chConf"></div><div class="cw"><img class="chNeu" src="data:image/png;base64,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" alt=""><img class="chLau" src="data:image/png;base64,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" alt=""></div></div>' + '<b>Claude Code</b><span>CONNECTED · WAITING FOR GOALS</span>';
5326
+ (function(){var conf=document.querySelector('#fsClawdHero .chConf');if(!conf)return;if(window.matchMedia&&matchMedia('(prefers-reduced-motion: reduce)').matches)return;var C=["#FF6B6B","#F86A5C","#FFD166","#3DDC97","#8FB0FF","#C77DFF","#ffffff"],N=16;for(var k=0;k<N;k++){var i=document.createElement('i');var ang=(-172+Math.random()*164)*Math.PI/180;var dist=50+Math.random()*94;var dx=Math.cos(ang)*dist,dy=Math.sin(ang)*dist-12;i.style.setProperty('--dx',dx.toFixed(1)+'px');i.style.setProperty('--dy',dy.toFixed(1)+'px');i.style.setProperty('--rot',(Math.random()*360-180|0)+'deg');i.style.animationDelay=(-(Math.random()*0.04)).toFixed(3)+'s';i.style.background=C[k%C.length];conf.appendChild(i);}})(); // Welcome P1 confetti (H17 item 4)
5314
5327
  // fold toggles = パネルアイコン Fill v2 (HANDOFF-v45 §4) — faithful port of flagship
5315
5328
  // tgSvg/tgUpdate at the adopted value (__tgv v=1). OPEN = pane side filled 14% +
5316
5329
  // divider; CLOSED = outward arrow pointing in the opening direction. No rotation.
@@ -5671,7 +5684,90 @@ function connectSSE(){
5671
5684
  };
5672
5685
  }
5673
5686
 
5687
+ // Signed in, but this device's engine isn't running yet. We're the REAL app
5688
+ // (served by the relay), just flagged disconnected: render the board, then gate
5689
+ // the composer so the first attempt to type explains the one-time device setup
5690
+ // (npx @galda/cli). Poll `/`; the moment the engine connects the relay proxies
5691
+ // the live app (flag gone) → reload into it. (Masa 2026-07-15: one product, not a
5692
+ // separate look-alike page.)
5693
+ function bootDisconnected(info){
5694
+ const email = (info && info.email) || '';
5695
+ document.body.classList.add('disconnected');
5696
+ document.body.setAttribute('data-layout', localStorage.getItem('layout') || 'flagship');
5697
+ applyTheme(); applyCust();
5698
+ // seed a minimal board so it renders like the real empty state
5699
+ state.projects = [{ id: '__onboard', name: 'Your project' }];
5700
+ state.active = '__onboard';
5701
+ state.billing = { licensed: false, email };
5702
+ try { render(); } catch (e) { console.warn('disconnected render', e); }
5703
+ const heroSpan = document.querySelector('#fsClawdHero span');
5704
+ if (heroSpan) heroSpan.textContent = 'CONNECT YOUR MACHINE TO BEGIN';
5705
+
5706
+ const style = document.createElement('style');
5707
+ style.textContent = `
5708
+ #connectGate{position:fixed;inset:0;z-index:1000;display:none;align-items:center;justify-content:center;padding:22px;
5709
+ background:rgba(4,4,6,.72);-webkit-backdrop-filter:blur(7px);backdrop-filter:blur(7px)}
5710
+ #connectGate.on{display:flex}
5711
+ #connectGate .cg-card{position:relative;width:100%;max-width:440px;background:var(--panel,#0d0d0f);border:1px solid var(--hair2,rgba(255,255,255,.12));
5712
+ border-radius:18px;padding:30px 28px;text-align:center;box-shadow:0 30px 80px -30px #000}
5713
+ #connectGate h2{font:700 20px/1.3 var(--ui,system-ui);letter-spacing:-.02em;margin:0 0 8px;color:var(--ink,#e9e7e2)}
5714
+ #connectGate p{font-size:13.5px;line-height:1.7;color:var(--ink2,#a3a097);margin:0 0 18px}
5715
+ #connectGate .cg-cmd{display:flex;align-items:center;justify-content:space-between;gap:12px;background:#0a0a0b;
5716
+ border:1px solid rgba(255,255,255,.14);border-radius:12px;padding:12px 14px;margin:0 0 16px}
5717
+ #connectGate .cg-cmd code{font:500 14px/1 var(--mono,monospace);color:#e9e7e2}
5718
+ #connectGate .cg-cmd button{font:500 11px/1 var(--mono,monospace);color:#a3a097;background:transparent;
5719
+ border:1px solid rgba(255,255,255,.18);border-radius:7px;padding:6px 9px;cursor:pointer}
5720
+ #connectGate .cg-cmd button:hover{color:#fff}
5721
+ #connectGate .cg-who{font-size:12px;color:var(--ink3,#6b6862);margin:0 0 4px}
5722
+ #connectGate .cg-who b{color:var(--ink2,#a3a097)}
5723
+ #connectGate .cg-switch{font-size:12px;color:var(--ink3,#6b6862);margin:14px 0 0}
5724
+ #connectGate .cg-switch a{color:#8fb0ff;text-decoration:none}
5725
+ #connectGate .cg-x{position:absolute;top:14px;right:16px;width:28px;height:28px;border:0;background:transparent;color:var(--ink3,#6b6862);font-size:20px;line-height:1;cursor:pointer}
5726
+ #connectGate .cg-wait{display:inline-flex;align-items:center;gap:7px;font:500 10.5px/1 var(--mono,monospace);letter-spacing:.08em;color:var(--ink3,#6b6862);margin-top:16px}
5727
+ #connectGate .cg-wait i{width:6px;height:6px;border-radius:50%;background:var(--green,#3DDC97);animation:cgpulse 1.8s ease-in-out infinite}
5728
+ @keyframes cgpulse{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--green,#3DDC97) 50%,transparent)}50%{box-shadow:0 0 6px 2px color-mix(in srgb,var(--green,#3DDC97) 35%,transparent)}}
5729
+ body.disconnected .composer{cursor:pointer}`;
5730
+ document.head.appendChild(style);
5731
+
5732
+ const ov = document.createElement('div');
5733
+ ov.id = 'connectGate';
5734
+ ov.innerHTML = '<div class="cg-card"><button class="cg-x" aria-label="Close">×</button>'
5735
+ + '<h2>Connect this device to start</h2>'
5736
+ + '<p>Galda runs on your own machine. Run this where you use Claude Code or Codex — this board comes alive the moment it connects, no refresh needed:</p>'
5737
+ + '<div class="cg-cmd"><code>npx @galda/cli</code><button class="cg-copy">Copy</button></div>'
5738
+ + '<p class="cg-who">Signed in as <b></b></p>'
5739
+ + '<p class="cg-switch">Wrong account? <a href="/logout">Switch account →</a></p>'
5740
+ + '<div class="cg-wait"><i></i>WAITING FOR THIS DEVICE</div></div>';
5741
+ document.body.appendChild(ov);
5742
+ ov.querySelector('.cg-who b').textContent = email || 'your account';
5743
+ const open = () => ov.classList.add('on');
5744
+ const close = () => ov.classList.remove('on');
5745
+ ov.querySelector('.cg-x').onclick = close;
5746
+ ov.addEventListener('mousedown', (e) => { if (e.target === ov) close(); });
5747
+ ov.querySelector('.cg-copy').onclick = function(){ try { navigator.clipboard.writeText('npx @galda/cli'); } catch {} this.textContent = 'Copied'; setTimeout(() => { this.textContent = 'Copy'; }, 1400); };
5748
+
5749
+ // gate the composer: any attempt to type a goal explains the setup first
5750
+ const input = document.getElementById('input');
5751
+ const send = document.getElementById('send');
5752
+ const gate = (e) => { if (e) e.preventDefault(); open(); };
5753
+ if (input) { input.readOnly = true; input.addEventListener('focus', gate); input.addEventListener('mousedown', gate); input.addEventListener('keydown', gate); }
5754
+ if (send) send.addEventListener('click', gate);
5755
+ const cw = document.getElementById('composerWrap'); if (cw) cw.addEventListener('mousedown', gate);
5756
+
5757
+ // come alive automatically: when the relay stops flagging disconnected (the
5758
+ // engine connected → its live app is proxied), reload into the real board.
5759
+ setInterval(() => {
5760
+ fetch('/', { cache: 'no-store', credentials: 'same-origin' }).then((r) => r.text()).then((t) => {
5761
+ if (t.indexOf('__GALDA_DISCONNECTED') === -1) location.reload();
5762
+ }).catch(() => {});
5763
+ }, 4000);
5764
+ }
5765
+
5674
5766
  async function boot(){
5767
+ // Served by the relay when you're signed in but this device's engine isn't
5768
+ // connected yet (window.__GALDA_DISCONNECTED). Show the real board, but gate the
5769
+ // composer with a "connect this device" explainer on the first attempt to type.
5770
+ if (window.__GALDA_DISCONNECTED) return bootDisconnected(window.__GALDA_DISCONNECTED);
5675
5771
  $('input').value = localStorage.getItem('draft') ?? '';
5676
5772
  await refresh();
5677
5773
  flushOutbox();
@@ -6934,7 +7030,7 @@ const cust = {
6934
7030
  ring: Number(localStorage.getItem('ringPal') ?? 1), // prism send-ring palette (0-16) — default = 2nd 全分光 (§6.5 確定)
6935
7031
  tban: Number(localStorage.getItem('tbAnim') ?? 5), // TB縁アニメ (0-9 + 10 none) — default = 5 パルス (§6.5 確定)
6936
7032
  tbc: Number(localStorage.getItem('tbColor') ?? 1), // TB縁アニメの色 (RINGS 0-16) — default = 2nd 全分光 (§6.5 確定)
6937
- glow: Number(localStorage.getItem('glowLight') ?? 0), // Midnight LIGHT preset (0-5, Moon default)
7033
+ glow: Number(localStorage.getItem('glowLight') ?? 6), // Midnight LIGHT preset (0-5, or 6="Off" flat black — Midnight default)
6938
7034
  hue: localStorage.getItem('lightHue') != null ? Number(localStorage.getItem('lightHue')) : null, // Midnight free hue (0-359, null = preset)
6939
7035
  bg3: Number(localStorage.getItem('bg3') ?? 0), // Banff BACKDROP (0 Lake / 1 Peak / 2-6 dark scenes / -1 upload)
6940
7036
  cine: Number(localStorage.getItem('cineText') ?? 3), // Cinema TEXT (0-4) — default 3 Below (v3 §6.5)
@@ -7194,14 +7290,21 @@ function applyCust(){
7194
7290
  // Midnight light: preset (Moon default) or the free hue slider (__light/__lighthue port)
7195
7291
  const setLg = (la, lb, lc, w) => { const s = document.body.style;
7196
7292
  s.setProperty('--lgA', la); s.setProperty('--lgB', lb); s.setProperty('--lgC', lc); s.setProperty('--lgW', w); };
7197
- if (cust.hue != null) {
7198
- const h = ((cust.hue | 0) % 360 + 360) % 360;
7199
- setLg(`hsla(${h},90%,79%,.60)`, `hsla(${(h + 18) % 360},78%,56%,.48)`, `hsla(${h},95%,87%,.40)`, '.44');
7200
- document.body.style.setProperty('--glowc', `hsl(${h},90%,79%)`); // legacy old-layout wash
7293
+ // light OFF (glow index 6) = pure flat black (LP #111112) via [data-light="off"] — a 3rd state
7294
+ // beyond preset/hue (flagship __light('off')). CSS handles the flat black + green accent.
7295
+ if (cust.hue == null && cust.glow === 6) {
7296
+ document.body.setAttribute('data-light', 'off');
7201
7297
  } else {
7202
- const L = LIGHTS[((cust.glow % 6) + 6) % 6];
7203
- setLg(L[1], L[2], L[3], L[4]);
7204
- document.body.style.setProperty('--glowc', LIGHT_SW[((cust.glow % 6) + 6) % 6]);
7298
+ document.body.removeAttribute('data-light');
7299
+ if (cust.hue != null) {
7300
+ const h = ((cust.hue | 0) % 360 + 360) % 360;
7301
+ setLg(`hsla(${h},90%,79%,.60)`, `hsla(${(h + 18) % 360},78%,56%,.48)`, `hsla(${h},95%,87%,.40)`, '.44');
7302
+ document.body.style.setProperty('--glowc', `hsl(${h},90%,79%)`); // legacy old-layout wash
7303
+ } else {
7304
+ const L = LIGHTS[((cust.glow % 6) + 6) % 6];
7305
+ setLg(L[1], L[2], L[3], L[4]);
7306
+ document.body.style.setProperty('--glowc', LIGHT_SW[((cust.glow % 6) + 6) % 6]);
7307
+ }
7205
7308
  }
7206
7309
  // Banff backdrop (v3 §6.5): photo / dark scene / uploaded image (bg3 = -1 → dataURL)
7207
7310
  const tbg = document.getElementById('themebg');
@@ -7244,11 +7347,13 @@ function gearHistHTML(){
7244
7347
  for (const tk of state.tasks) if (tk.goalId === g.id && tk.finishedAt) t = Math.max(t, Date.parse(tk.finishedAt) || 0);
7245
7348
  return t;
7246
7349
  };
7247
- const MARK = { done: ['ok', '✓'], reverted: ['no', '↩'], skipped: ['ar', ''] };
7350
+ // History = everything you've sent, newest first (not only settled goals
7351
+ // that felt empty). Each row gets a status glyph; unknown states fall back.
7352
+ const MARK = { done: ['ok', '✓'], reverted: ['no', '↩'], skipped: ['ar', '—'], failed: ['no', '✕'], interrupted: ['ar', '⏸'], review: ['rv', '●'], running: ['run', '…'], planning: ['run', '…'], stacked: ['ar', '·'], needsInput: ['rv', '?'] };
7248
7353
  const evs = (state.goals || [])
7249
- .filter((g) => g.projectId === state.active && MARK[g.status])
7354
+ .filter((g) => g.projectId === state.active)
7250
7355
  .map((g) => ({ g, ts: goalTs(g) })).sort((a, b) => b.ts - a.ts);
7251
- if (!evs.length) return '<div class="mcpnote">No decisions yet — approved, reverted and archived goals will appear here.</div>';
7356
+ if (!evs.length) return '<div class="mcpnote">No history yet — everything you send shows up here.</div>';
7252
7357
  const todayK = new Date().toDateString(), yestK = new Date(Date.now() - 864e5).toDateString();
7253
7358
  const cap = (ts) => { const k = new Date(ts).toDateString();
7254
7359
  return k === todayK ? 'TODAY' : k === yestK ? 'YESTERDAY'
@@ -7257,7 +7362,7 @@ function gearHistHTML(){
7257
7362
  for (const { g, ts } of evs) {
7258
7363
  const c = cap(ts);
7259
7364
  if (c !== last) { out += `<h4>${c}</h4>`; last = c; }
7260
- const [cls, m] = MARK[g.status];
7365
+ const [cls, m] = MARK[g.status] || ['ar', '·'];
7261
7366
  const hhmm = new Date(ts).toTimeString().slice(0, 5);
7262
7367
  // 1-line ellipsis + a dedicated More/Less button (HANDOFF-v45 §9 — replaces row-tap;
7263
7368
  // the button itself is hidden post-render for rows that don't overflow, see wiring
@@ -7386,7 +7491,8 @@ function buildGearPop(){
7386
7491
  `<button class="sw${cust.bg3 === i ? ' on' : ''}" data-gbg3="${i}" title="${n}" style="background:${i < 2 ? bg3Image(i) + ' center/cover' : bg3Image(i)}"></button>`).join('')
7387
7492
  + `<button class="optchip${cust.bg3 === -1 ? ' on' : ''}" data-gbg3up="1" title="Upload an image">+</button>`;
7388
7493
  // Midnight LIGHT: 6 presets (Moon default) + free HUE slider
7389
- const lightSw = LIGHTS.map((L, i) => `<button class="sw${cust.hue == null && cust.glow === i ? ' on' : ''}" data-gglow="${i}" title="${L[0]}" style="background:${LIGHT_SW[i]}"></button>`).join('');
7494
+ const offSw = `<button class="sw${cust.hue == null && cust.glow === 6 ? ' on' : ''}" data-gglow="6" title="Off" style="background:#111112"></button>`; // flat-black "Off" — Midnight default, leftmost
7495
+ const lightSw = offSw + LIGHTS.map((L, i) => `<button class="sw${cust.hue == null && cust.glow === i ? ' on' : ''}" data-gglow="${i}" title="${L[0]}" style="background:${LIGHT_SW[i]}"></button>`).join('');
7390
7496
  // Cinema SCENE (reuses the band presets + upload) + TEXT modes
7391
7497
  const sceneNames = ['Lake', 'Peak', 'Blue', 'Violet', 'Dusk', 'Jade', 'Midnight', 'Gold'];
7392
7498
  const sceneSw = FS_BANDS.map((v, i) =>
package/app/theme.css CHANGED
@@ -71,6 +71,10 @@ body[data-theme="dark"],body[data-theme="midnight"],body[data-theme="cinema"]{
71
71
  /* flagship surface tokens — flagship internal T5/T6 (dark) values */
72
72
  --hover:rgba(255,255,255,.06); --hover-s:rgba(255,255,255,.05); --surf:rgba(255,255,255,.04);
73
73
  }
74
+ /* Midnight · light OFF = pure flat black (LP #111112). data-light="off" (H17 port, flagship f9c75ad 2026-07-14).
75
+ Black-screen accent = LP green #3DDC97 — unifies PR pill / ✓ / token / LIVE / DOING / progress. Both --green
76
+ (var(--green) users) and --green-rgb (rgba(var(--green-rgb),.x) / color-mix users) are re-pointed. */
77
+ body[data-theme="midnight"][data-light="off"]{--green:#3DDC97;--green-rgb:61,220,151}
74
78
  /* Theme 4 — Glass: ARCHIVED (v3 §6.5 row 4 / §7 decision log 2026-07-07).
75
79
  Not selectable from any UI; the token block + WebGL pipeline stay dormant/intact
76
80
  so the archive can be revived from the e574 loop without re-porting.
@@ -0,0 +1,36 @@
1
+ // analytics-client.mjs — fire-and-forget client for Phase 1 usage analytics
2
+ // (docs/DATA-RETENTION-PLAN.md). Posts a single Tier-A event to the billing-api
3
+ // /events endpoint.
4
+ //
5
+ // PRIVACY: NEVER pass prompt / task text / file paths. Only an event type, a
6
+ // user identifier (the endpoint hashes it into a pseudonymous id), and
7
+ // allow-listed non-sensitive meta (project_hash / plan / status / model /
8
+ // source — see workers/billing-api/src/analytics.mjs sanitizeMeta). Failures
9
+ // are swallowed so analytics can never break a task run or block the caller.
10
+ //
11
+ // Hook points (wiring lives in engine/relay, kept out of this module):
12
+ // signup → relay Google callback, right after the session is minted
13
+ // task_created → engine, when a task/goal is accepted
14
+ // task_completed → engine, task.status = 'done'
15
+ // task_failed → engine, task.status = 'failed'
16
+ // task_interrupted → engine, task.status = 'interrupted'
17
+ // review_approved / review_dismissed → on the review action
18
+
19
+ const BILLING_API_URL = (process.env.MANAGER_BILLING_API_URL ?? '').replace(/\/$/, '');
20
+
21
+ export function analyticsEnabled() {
22
+ return Boolean(BILLING_API_URL);
23
+ }
24
+
25
+ // type: one of the EVENT_TYPES; uid: account email/id (hashed server-side);
26
+ // meta: optional allow-listed non-sensitive fields only.
27
+ export function emitEvent(type, uid, meta) {
28
+ if (!BILLING_API_URL) return; // analytics off when unconfigured — no-op
29
+ try {
30
+ fetch(`${BILLING_API_URL}/events`, {
31
+ method: 'POST',
32
+ headers: { 'content-type': 'application/json' },
33
+ body: JSON.stringify({ type, uid, meta }),
34
+ }).catch(() => {}); // fire-and-forget: never surface network errors
35
+ } catch { /* never throw into the caller */ }
36
+ }
package/engine/server.mjs CHANGED
@@ -14,7 +14,8 @@
14
14
  import { createServer } from 'node:http';
15
15
  import { spawn, execFile, spawnSync } from 'node:child_process';
16
16
  import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, statSync } from 'node:fs';
17
- import { randomBytes } from 'node:crypto';
17
+ import { emitEvent } from './analytics-client.mjs';
18
+ import { randomBytes, createHash } from 'node:crypto';
18
19
  import { resolve, dirname, join, basename } from 'node:path';
19
20
  import { homedir } from 'node:os';
20
21
  import { fileURLToPath } from 'node:url';
@@ -36,6 +37,10 @@ const PORT = Number(process.env.MANAGER_PORT ?? 4400);
36
37
  const keyFile = join(DATA_DIR, 'secret.key');
37
38
  if (!existsSync(keyFile)) writeFileSync(keyFile, randomBytes(18).toString('base64url'));
38
39
  const ACCESS_KEY = readFileSync(keyFile, 'utf8').trim();
40
+ // A stable, anonymous per-install id for usage analytics — a one-way hash of the
41
+ // access key so the raw credential NEVER leaves the machine (the billing Worker
42
+ // hashes this again into its pseudonymous id). Not the access key itself.
43
+ const ANALYTICS_UID = createHash('sha256').update('galda-analytics:' + ACCESS_KEY).digest('hex').slice(0, 24);
39
44
  // Per-worker run cap. Env-configurable so it can be tuned operationally and the
40
45
  // timeout→'interrupted' path is testable (a test sets a short value + a slow
41
46
  // fake worker instead of waiting 10 real minutes).
@@ -541,6 +546,56 @@ function saveTask(t) {
541
546
  appendFileSync(logFile, JSON.stringify({ ...persist, kind: 'task' }) + '\n');
542
547
  const goal = goals.find((g) => g.id === t.goalId);
543
548
  if (goal) sendGoalEvent({ ev: 'task', task: persist }, goal); else send({ ev: 'task', task: persist });
549
+ emitTaskAnalytics(t);
550
+ }
551
+
552
+ // Phase 1 usage analytics (docs/DATA-RETENTION-PLAN.md). Fire-and-forget, no
553
+ // content: a task's creation and its terminal outcome are counted once each,
554
+ // keyed by ANALYTICS_UID (the hashed, non-credential install id). saveTask runs
555
+ // on every status change, so a per-(id,event) Set dedups repeat saves.
556
+ const _analyticsEmitted = new Set();
557
+ const _TERMINAL_EVENT = { done: 'task_completed', failed: 'task_failed', interrupted: 'task_interrupted' };
558
+ function emitTaskAnalytics(t) {
559
+ if (!t || t.id == null) return;
560
+ if (!_analyticsEmitted.has(`created:${t.id}`)) {
561
+ _analyticsEmitted.add(`created:${t.id}`);
562
+ emitEvent('task_created', ANALYTICS_UID, { source: 'engine' });
563
+ }
564
+ const ev = _TERMINAL_EVENT[t.status];
565
+ if (ev && !_analyticsEmitted.has(`${t.status}:${t.id}`)) {
566
+ _analyticsEmitted.add(`${t.status}:${t.id}`);
567
+ emitEvent(ev, ANALYTICS_UID, { status: t.status, source: 'engine' });
568
+ postHistory(t);
569
+ }
570
+ }
571
+
572
+ // Phase 2 cloud history (paid). On a finished task, post an encrypted history
573
+ // record (the Worker gates on the license token and encrypts the content, so
574
+ // free / no-token installs store nothing — their work stays fully local).
575
+ // Fire-and-forget: never blocks or breaks a run.
576
+ function readLicenseToken() {
577
+ try { return existsSync(licenseFile) ? readFileSync(licenseFile, 'utf8').trim() : null; } catch { return null; }
578
+ }
579
+ function postHistory(t) {
580
+ if (!BILLING_API_URL) return;
581
+ const token = readLicenseToken();
582
+ if (!token) return; // no license → nothing stored server-side (stays local)
583
+ const goal = goals.find((g) => g.id === t.goalId);
584
+ const project = projects.find((p) => p.id === t.projectId);
585
+ try {
586
+ fetch(`${BILLING_API_URL}/history`, {
587
+ method: 'POST',
588
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
589
+ body: JSON.stringify({
590
+ id: `t${t.id}`,
591
+ project: project?.name ?? null,
592
+ content: goal?.text ?? null, // the user's request — encrypted server-side
593
+ summary: t.result ?? null, // the task's result — encrypted server-side
594
+ status: t.status,
595
+ createdAt: t.startedAt ? Date.parse(t.startedAt) : Date.now(),
596
+ }),
597
+ }).catch(() => {});
598
+ } catch { /* never throw into the caller */ }
544
599
  }
545
600
  const ACTIVITY_LIMIT = 1000;
546
601
  function activityLine(line) {
@@ -3265,6 +3320,20 @@ const server6 = createServer(server.listeners('request')[0]);
3265
3320
  server6.on('error', (e) => console.log(`[manager] ::1 listen skipped: ${e.code}`));
3266
3321
  try { server6.listen(PORT, '::1'); } catch { /* IPv6 loopback unavailable */ }
3267
3322
 
3323
+ // Guard the primary IPv4 listen the same way the ::1 twin is guarded: without
3324
+ // this, a busy port emits an unhandled 'error' and Node prints a raw stack
3325
+ // trace + crashes. The common cause is a second manager (or `npx @galda/cli`)
3326
+ // started while one is already running on this port — so say that, and how to
3327
+ // pick another port, instead of dumping a stack trace.
3328
+ server.on('error', (e) => {
3329
+ if (e.code === 'EADDRINUSE') {
3330
+ console.error(`[manager] port ${PORT} is already in use — another manager is likely running on it.`);
3331
+ console.error(`[manager] Use a different port: MANAGER_PORT=${PORT + 10} npx @galda/cli`);
3332
+ process.exit(1);
3333
+ }
3334
+ throw e;
3335
+ });
3336
+
3268
3337
  server.listen(PORT, '127.0.0.1', () => {
3269
3338
  console.log(`[manager] Manager for AI → http://localhost:${PORT}/?key=${ACCESS_KEY}`);
3270
3339
  if (process.env.MANAGER_OPEN_BROWSER === '1' && process.platform === 'darwin') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@galda/cli",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "type": "module",
5
5
  "description": "Galda - hand off work to your Claude Code, get proof back. Runs on your existing subscription, no extra API cost.",
6
6
  "scripts": {