@galda/cli 0.10.1 → 0.10.5
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/CLAUDE.md +6 -0
- package/app/index.html +133 -23
- package/app/theme.css +4 -0
- package/bin/manager-for-ai.mjs +11 -1
- package/engine/analytics-client.mjs +38 -0
- package/engine/lib.mjs +10 -0
- package/engine/mcp.mjs +4 -1
- package/engine/relay-client.mjs +52 -6
- package/engine/server.mjs +98 -3
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -33,6 +33,12 @@
|
|
|
33
33
|
- 探索的な読み込み(横断 grep・場所探し・大きいファイル通読)は**既定で subagent に逃がし**、メイン文脈に全文を載せない。ピンポイントな1関数だけ直接読む。
|
|
34
34
|
- 別エージェントへ渡す時は、チャット履歴でなく **1枚の handoff md** を書いて渡す。
|
|
35
35
|
|
|
36
|
+
## デプロイ/インフラの掟(Masa明言 2026-07-15・全レーン必読)
|
|
37
|
+
|
|
38
|
+
- **`a2c-tech.workers.dev` を絶対に使わない・コードやdocsに書かない**。これはクライアント企業 **A2C** の名を冠した Cloudflare workers.dev サブドメインで、Galda の製品・URL・設定に一切登場させてはいけない(見つけたら全て除去)。
|
|
39
|
+
- Worker/API へは**必ず所有ドメイン `https://galda.app`(custom domain)経由**で参照する。`*.workers.dev` サブドメインを製品コードに焼き込まない(`MANAGER_BILLING_API_URL` 等の既定値は `https://galda.app`)。
|
|
40
|
+
- **Cloudflare/wrangler にデプロイする前に `wrangler whoami` でデプロイ先アカウントを確認**し、Galda/Kodo 所有であること・workers.dev サブドメイン名に客先名や別プロジェクト名が出ていないことを確かめる。怪しければ止めて Masa に確認(本番デプロイは Masa 明示許可制)。
|
|
41
|
+
|
|
36
42
|
## 構成
|
|
37
43
|
|
|
38
44
|
- `engine/server.mjs` … チャットアプリのサーバ(:4400・タスク分解・worker実行・SSE・PR作成)
|
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>
|
|
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
|
|
1133
|
-
@keyframes fsrundot{0%,100%{box-shadow:0 0 0 0
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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">
|
|
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
|
|
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,iVBORw0KGgoAAAANSUhEUgAAARgAAAETCAYAAADtSRlPAAAQEElEQVR4nO3dS4wsZ3XA8VOvfs2Mx3duLNkgY3yT6EpgJ4oEmwQJxUJZAMKgKGSFHIkFG4SEvGDFDiQ2CBZZICVZIRArsI0QIotYQVEkgpBBSmyuyTXBQdiWL/c1z+56fOhUdc1Uv2aqqqenvpr+/yzr3Jnpen3ddb7T9dVDBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVOVUngIX5kvP/L15fHQn/fcodqTjmYVRuV5XjkajU1/X5HS9TkeSeFh7ujLtsOx0+XbVma7jevJz05Mvf/t77Fdjfv4P2Ge768vDnYHEcYWJOv3Ky4mSSK5e2HSDLNSdruL6+Z2Lm87zRB52qk93mZFgLDcMI/FdP/3QG8cRx5iJKOJNvN4xUfW/O76Eptr0daer+vel5uvMtlvZ9qg6nUbP82U/isq8rWuDBGO5YnJR01Fksry5qL9f1HKb2t6q0+n7VKnSXBNu0yuAEuW666c9pCLa2Q76PmEWCablFQzRjnbQ9wmzSDCWo4JpTwWjB3kxiQRjOSqYdkTFMZhZJBjLUcG0IyoqmFkkGMtRwdhRoVDB1EOCsRwVjB0VChVMPSQYy1HBtGcUiWMws0gwlqOCsaNCYRSpHhKM5ahg7KhQqGDqIcFYLj9DtOkemsh5MHWQYCyXnyHadA9N5DyYOkgwlqOCaUcFpTgPZhYJxnJUMO2ooBSjSLNIMJZjFMmOCoUKph4STAssutkU0Z52yG8OhklcY26xrcGWbMhh+u/IJOI7LtHydthgl5pAgrGYI4ns9DuS94uxEfEcoo3tEDjZl4EtZ6vRz4xt+IpkMd9zJDGJuCaxYiciLm4HrWBcSdJOASdIMBbT5JLTsQr9MBPtbAdfkon3CxkSjMXccdmt9DCi9pREO9shEnfi/UKGFrFYpF3jWNM9NJEKpg4SjOXHYHJN99BEKpg6SDAWo4JpT+XEMZj5SDAWo4JpT+XEMZj5SDAWo4JpvjKhglkOCcZiVDDNVyZUMMshwViMCqb5yoQKZjkkGItRwTRfmVDBLIcEYzEqmOYrEyqY5ZBgLEYF03xlQgWzHBKMxahgmq9MqGCWQ4KxGBVM85UJFcxySDAWo4JpvjKhglkOCcZiVDDNVyZUMMshwaxZBaM7jQ09/mWLXIs0HwlmjSoY3/Embu9YJTZdIZwVVdfRe+iUi+e9XVyLNB/35G2JZSqWxIjE3YH8/JVfyj/95BWRTvm3fXBwV5760IfkE+/6IxmGYeOVwrztC/xAvnnzLfnPF/9NRptXyjfqKJLPffDP5U8evyad0UHaTnXXI0jvaDd+o3CMBNMSeU9Z/JpTJibj8v3I78q9g0P5ydt3RPRhYfqojRJx0xh5771dcZMr6XxCcWutx6piun2Okd/uhvLSvUj27r1dafvu7B9I5HfFHx4stX2RcaVDcpnBV6SWWObYwIz8caenxXwnzCfR7xYW3llPty82Rjr+eO8urnfJ6EfD43biGMz5IsG0xDLHBmbkjzs9LRojjjuetr+z9HqscvTGcxwJ4/HDXYrrXWY79WC63z1uJ47BnC8SzCUfRVqmgjHJeNrD20uvx6ormMAbf9svrnfZSiY9VkAFswokmEs4ijQ9CpTf7b7bDSpVMMc/93fESczJfFYwClNltCiYWg+tYEbR1Ne7sttZkG9fPv86o0iGXWoCB3kv2XORVDieLotuthfos5OHYRo9I5K4rriJWRjjcZLp6wSHtyVxt2UYZ/MdFpZz0RVLcfuy9XDFM87JMRhljHg6Rfr3xVErn1xYaKd8/nVGkRyHZyMVcdy7ouc//XEzOhrKquguq/3gRuDJlY2BBF65UQ39cPcCT777+m154Yc/ktvB5sR8O3v35Y/fc12eeeJa+vMoiaXjemn0XV+iJJqIStfjzY0defk//l2ee/2ODAJvZn2/8uG/kne945FsFMZx053z3EeJdBSsuyn/++vX5Ivf/7E4g8ltU3/zyAPyng98UHZ23z51u4pRX/eNl27Krddvyl6wMTG/nXBP/vaTfycf2cqerll2dOkoStKRqd1Qn/R4ksBWodPrytP/8pzV+zAVTEXXr2xKGPZk1fTjWaVC0B7USVzZC2N5ZS+RPbk/OUMdch7F8uQjVyUM86ddL+a5rsRJIg9t7shLwyN5Y29/7utGw8M0uuPHp553xaLblTs8Gslrw0RkOLVt6pEH5MmHduRqPz7+VTT+gM+LriYM35O98Ib8Yj8RMZPz1J92j0YS7GzIcDQqvb4935VHt2cT4CoEgf27L8dgKoqMPn3YTctz/aCuKurOWuUYR3oMwMvezvRrzdSxBv1aNNB5G6fU8g/j7FGo8Xh4enp+E1Hy81HO/5hM2VGw4fAoXdeRfv0zyfHXuEUx/xqk9Fyf4vzyUSjX8Y6Tcdn1jVf8uRgWo7G6eEmRYCrSk7pW0VMvOtZQZTQliRPxB+NSf/qksoKlRp/mnWeywgrmzFGw8fp0uz3xElN6NKjoUH9baK98FCoxcVrJlZlfE7ENSDAVaa+xip76PHp6rWCig5OvMmlPXDwvZKxu5ZDPb2EFI0mjFUz6/pQ8n6Wor78tJKtiBaNfE8vMr4nYBiQYSyuYOj29VjAqOyoiWU9c6JGPxn+vWzksPL/kHM4jOWs99GS4kxWZWv64gqm6Hul8I1f2nMUVTH6co+n312lpBWP/USJLK5jzHi1ZNuq1MHqWy2bgyTsDkTe9jpigI044SmN3d1ceGgQSR3Hp+enOql87dOft+64E/Y3j+WnsF7onrRyKFcx5bpfOV0/n7/c6c9dD9TY2Kq+HtsWDfVce9UXuj9srt3O0K/2gM3MMpun3OS7ENmhLIrTGfz/7KaNXFSszbkCbYhgncjDKdrp0HV1PnCQbWdHEuNnrVJqf2j8apVVbUT7f7X5WOax6u2S8bfvRyShRTtdj0OmkQ/pV5ztv23I6z47nWvG+OnNiNwjkia9+0+p9mAqmgs8+/TFjotDKCiaPjufKYDDQL0yFodkgHV2pu94P9ntT8zuZ7/ACt0+HgPX/eeuh2+fWmK8mXL0Kenoou+78qGAmWZ39bGR7BUNcn3botqCC4SBvRXoWzCpGS4i0g8coEk4STfOjCMT1boc2oIKpKL1a1vGpOKg4Gq8824AEc0nOgyGuXzu0AQmmIr3En2MwHDOyoYJrAxJMTTb0YMT1boc2IMHUZEMPRlzvdmgDEkxNNvRgxPVuhzYgwdRkQw9GXO92aAMSTEV6g2m9oM6GHoy43u3QBiSYivTqWhvvB0Ncv3ZoAxJMxYsdHT/gPBhLevB1j21Agqkov5rahh6MuN7t0AYkmAr+8fkXHL3DGWfy2tGDr3tsAxJMRZzJ23zPTRQqmMuKUaTme26ipEm2Dahgauh7bnoXtMEKo+ekDz+lxz7HimXV7Znv870gSN/HfuBNxEW/L/v3YvTHF97ajltmVvSvr/5OwnnP6jln28lIHrt2TXYkosc+h8pF5UlglRXFLePLb27clHvuyQ3Eywj0SZ4VPlf6+jZoSaG1nr717GfME+Gd9N9ln41MbKYd9LlQWl38zHlQ/uHr32C/GmtHGlxTUd71cu2T9cde0udSpfcJWn112yYkGIv5hSN5jJ7YPXqUPlnTYXeaRotYjAqmfRUMJpFgLEYF03xlQgWzHBKMxahgmq9MqGCWQ4KxGBVM85UJFcxySDAt0XQPTeQYTB0kmJZouocmMopUBwnGYhyDaU/lxCjSfCQYi3EMpj2VE+fBzEeCsVjxvIqme2giFUwdXOxoseKZoXlPWeaaGL0qV++857muxEkieptP/dn1PUmieCbq64ZJoueipjfTCpws+o4nsYkXRn2d8nxvYnmrjIdxIl1HZGTkeD3PitPtUTYqXU6+vae1e2SyK+ExiQRjsdu7e3JXovTfuqOcRW/l+Yv7Q/ncd35Q+aP+zx/9S6NXb3eOjipN98Kv/l+++uJPL2TXevav328+9qePVppm1BvIjZdvyGd/+F+V1vHz779urv3F++TP5PD4d8X3wLieOEl8HLXt1X3Zr7R+lx0JxnL7o5H4ri9RkiWaeYzjiGO0S/fl1UOv1nL+563fS//6k3Ll/t2Z+U7H7G/ZR+eikku+rKfe/XC6AvPWazpqu73hDuTmrXuVl/W1n95wvvDOx8z1nUH6c7H9s/lHM1GXtz/uEJAhwVguTy76IT5LHNdfzoOBI5t33zr+OU8gZrzY6dgUE3TECUfH7bEoKm234oHyOjxPZBjOtv/08vR9wiwO8lou3Ulc/7hyWBTznaGuu6ERrzAD7ZFF4oVRuc4SC6ypE2XLLtMeaXIu3PKiDk3aZdr/tApznZFgWlbBnNZjL0MrmLhQAp21PBN4EjfQaY/8LKldZAVTtv31dRs+lUwRCcZyec9YpsdexmwFc/ry9GtKk8pWMNu7ty6kgsmXtx9RyRSRYCyXf7e3oYKZPtBbfP1FK1tR/H5wdanlUMEsh3rOYluDLdkKQwm8noRxIoHnnhoHvi/bGyfDqlVs9vqy/fCj0i0c6J2e/zT9/UW7GngS9DZKtYfGIDmQ/wvqJeGt7Suy3Xck7GTbXmZ5N6uN8l96JBiLBZ6RQfrZTsTxssfVdhbEgScSuPUPvHa7gQRJlM4nXaLjpieRTS8nP3lNuQ2MKGkS1eV3SrSHtltna6v2svRwjz5CRu/2r5cClFneA4MHznFr24+vSBYrjoCUuRZGJabe15ajw/EJfeP55GeoTi8nNEmaWPRnPWv4oiVysvwy7ZGqmXR1wCrUXOr4XItUEwmmJcpczXseQ8f5fM5anmogv6SXM5S9uvlYzaSbLS8RMRFXU9dEgrkkV1MvW8HEEknknsznrOWpJUeAazHipDt72fYwehRgiaSbX3DK1dT1kGAsF3h+etGjXuDnmmQyysnv9UI719NHZ9TbmfRhpH6SPTxMzV1OYXl6LEb/u2i9wEmv+5nbHoWYtsc5PEbED4I0zm2HQtTl6UWSmMRBXssvdnzDHKQ71DBK0iFTHRl2A73AbnR8oZ3S32/1O3Jnr975KfvhUA72d+Xo8ChdXnY1dbZcXV4SxtL1T36/K7H0GqhgfrubjZKdXGg4Whh1PUNzR+6O6p2bchiO5NZumG5z3u7aDvNifsHjfR6WCgAAAAAAAAAAAAAAAAAAAAAAAABS1R8AHmjUK201McoAAAAASUVORK5CYII=" 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,94 @@ 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
|
+
// Keep the agent / model / effort pickers live so Codex users can switch to
|
|
5756
|
+
// Codex (and see it's a first-class option) even before this device connects;
|
|
5757
|
+
// only typing a goal or hitting send opens the connect explainer. (Masa 2026-07-15)
|
|
5758
|
+
const cw = document.getElementById('composerWrap');
|
|
5759
|
+
if (cw) cw.addEventListener('mousedown', (e) => { if (e.target.closest('#appsel, #modelsel, #effortsel')) return; gate(e); });
|
|
5760
|
+
|
|
5761
|
+
// come alive automatically: when the relay stops flagging disconnected (the
|
|
5762
|
+
// engine connected → its live app is proxied), reload into the real board.
|
|
5763
|
+
setInterval(() => {
|
|
5764
|
+
fetch('/', { cache: 'no-store', credentials: 'same-origin' }).then((r) => r.text()).then((t) => {
|
|
5765
|
+
if (t.indexOf('__GALDA_DISCONNECTED') === -1) location.reload();
|
|
5766
|
+
}).catch(() => {});
|
|
5767
|
+
}, 4000);
|
|
5768
|
+
}
|
|
5769
|
+
|
|
5674
5770
|
async function boot(){
|
|
5771
|
+
// Served by the relay when you're signed in but this device's engine isn't
|
|
5772
|
+
// connected yet (window.__GALDA_DISCONNECTED). Show the real board, but gate the
|
|
5773
|
+
// composer with a "connect this device" explainer on the first attempt to type.
|
|
5774
|
+
if (window.__GALDA_DISCONNECTED) return bootDisconnected(window.__GALDA_DISCONNECTED);
|
|
5675
5775
|
$('input').value = localStorage.getItem('draft') ?? '';
|
|
5676
5776
|
await refresh();
|
|
5677
5777
|
flushOutbox();
|
|
@@ -6934,7 +7034,7 @@ const cust = {
|
|
|
6934
7034
|
ring: Number(localStorage.getItem('ringPal') ?? 1), // prism send-ring palette (0-16) — default = 2nd 全分光 (§6.5 確定)
|
|
6935
7035
|
tban: Number(localStorage.getItem('tbAnim') ?? 5), // TB縁アニメ (0-9 + 10 none) — default = 5 パルス (§6.5 確定)
|
|
6936
7036
|
tbc: Number(localStorage.getItem('tbColor') ?? 1), // TB縁アニメの色 (RINGS 0-16) — default = 2nd 全分光 (§6.5 確定)
|
|
6937
|
-
glow: Number(localStorage.getItem('glowLight') ??
|
|
7037
|
+
glow: Number(localStorage.getItem('glowLight') ?? 6), // Midnight LIGHT preset (0-5, or 6="Off" flat black — Midnight default)
|
|
6938
7038
|
hue: localStorage.getItem('lightHue') != null ? Number(localStorage.getItem('lightHue')) : null, // Midnight free hue (0-359, null = preset)
|
|
6939
7039
|
bg3: Number(localStorage.getItem('bg3') ?? 0), // Banff BACKDROP (0 Lake / 1 Peak / 2-6 dark scenes / -1 upload)
|
|
6940
7040
|
cine: Number(localStorage.getItem('cineText') ?? 3), // Cinema TEXT (0-4) — default 3 Below (v3 §6.5)
|
|
@@ -7194,14 +7294,21 @@ function applyCust(){
|
|
|
7194
7294
|
// Midnight light: preset (Moon default) or the free hue slider (__light/__lighthue port)
|
|
7195
7295
|
const setLg = (la, lb, lc, w) => { const s = document.body.style;
|
|
7196
7296
|
s.setProperty('--lgA', la); s.setProperty('--lgB', lb); s.setProperty('--lgC', lc); s.setProperty('--lgW', w); };
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
document.body.
|
|
7297
|
+
// light OFF (glow index 6) = pure flat black (LP #111112) via [data-light="off"] — a 3rd state
|
|
7298
|
+
// beyond preset/hue (flagship __light('off')). CSS handles the flat black + green accent.
|
|
7299
|
+
if (cust.hue == null && cust.glow === 6) {
|
|
7300
|
+
document.body.setAttribute('data-light', 'off');
|
|
7201
7301
|
} else {
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7302
|
+
document.body.removeAttribute('data-light');
|
|
7303
|
+
if (cust.hue != null) {
|
|
7304
|
+
const h = ((cust.hue | 0) % 360 + 360) % 360;
|
|
7305
|
+
setLg(`hsla(${h},90%,79%,.60)`, `hsla(${(h + 18) % 360},78%,56%,.48)`, `hsla(${h},95%,87%,.40)`, '.44');
|
|
7306
|
+
document.body.style.setProperty('--glowc', `hsl(${h},90%,79%)`); // legacy old-layout wash
|
|
7307
|
+
} else {
|
|
7308
|
+
const L = LIGHTS[((cust.glow % 6) + 6) % 6];
|
|
7309
|
+
setLg(L[1], L[2], L[3], L[4]);
|
|
7310
|
+
document.body.style.setProperty('--glowc', LIGHT_SW[((cust.glow % 6) + 6) % 6]);
|
|
7311
|
+
}
|
|
7205
7312
|
}
|
|
7206
7313
|
// Banff backdrop (v3 §6.5): photo / dark scene / uploaded image (bg3 = -1 → dataURL)
|
|
7207
7314
|
const tbg = document.getElementById('themebg');
|
|
@@ -7244,11 +7351,13 @@ function gearHistHTML(){
|
|
|
7244
7351
|
for (const tk of state.tasks) if (tk.goalId === g.id && tk.finishedAt) t = Math.max(t, Date.parse(tk.finishedAt) || 0);
|
|
7245
7352
|
return t;
|
|
7246
7353
|
};
|
|
7247
|
-
|
|
7354
|
+
// History = everything you've sent, newest first (not only settled goals —
|
|
7355
|
+
// that felt empty). Each row gets a status glyph; unknown states fall back.
|
|
7356
|
+
const MARK = { done: ['ok', '✓'], reverted: ['no', '↩'], skipped: ['ar', '—'], failed: ['no', '✕'], interrupted: ['ar', '⏸'], review: ['rv', '●'], running: ['run', '…'], planning: ['run', '…'], stacked: ['ar', '·'], needsInput: ['rv', '?'] };
|
|
7248
7357
|
const evs = (state.goals || [])
|
|
7249
|
-
.filter((g) => g.projectId === state.active
|
|
7358
|
+
.filter((g) => g.projectId === state.active)
|
|
7250
7359
|
.map((g) => ({ g, ts: goalTs(g) })).sort((a, b) => b.ts - a.ts);
|
|
7251
|
-
if (!evs.length) return '<div class="mcpnote">No
|
|
7360
|
+
if (!evs.length) return '<div class="mcpnote">No history yet — everything you send shows up here.</div>';
|
|
7252
7361
|
const todayK = new Date().toDateString(), yestK = new Date(Date.now() - 864e5).toDateString();
|
|
7253
7362
|
const cap = (ts) => { const k = new Date(ts).toDateString();
|
|
7254
7363
|
return k === todayK ? 'TODAY' : k === yestK ? 'YESTERDAY'
|
|
@@ -7257,7 +7366,7 @@ function gearHistHTML(){
|
|
|
7257
7366
|
for (const { g, ts } of evs) {
|
|
7258
7367
|
const c = cap(ts);
|
|
7259
7368
|
if (c !== last) { out += `<h4>${c}</h4>`; last = c; }
|
|
7260
|
-
const [cls, m] = MARK[g.status];
|
|
7369
|
+
const [cls, m] = MARK[g.status] || ['ar', '·'];
|
|
7261
7370
|
const hhmm = new Date(ts).toTimeString().slice(0, 5);
|
|
7262
7371
|
// 1-line ellipsis + a dedicated More/Less button (HANDOFF-v45 §9 — replaces row-tap;
|
|
7263
7372
|
// the button itself is hidden post-render for rows that don't overflow, see wiring
|
|
@@ -7386,7 +7495,8 @@ function buildGearPop(){
|
|
|
7386
7495
|
`<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
7496
|
+ `<button class="optchip${cust.bg3 === -1 ? ' on' : ''}" data-gbg3up="1" title="Upload an image">+</button>`;
|
|
7388
7497
|
// Midnight LIGHT: 6 presets (Moon default) + free HUE slider
|
|
7389
|
-
const
|
|
7498
|
+
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
|
|
7499
|
+
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
7500
|
// Cinema SCENE (reuses the band presets + upload) + TEXT modes
|
|
7391
7501
|
const sceneNames = ['Lake', 'Peak', 'Blue', 'Violet', 'Dusk', 'Jade', 'Midnight', 'Gold'];
|
|
7392
7502
|
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.
|
package/bin/manager-for-ai.mjs
CHANGED
|
@@ -29,6 +29,12 @@ if (claude.error || claude.status !== 0) {
|
|
|
29
29
|
}
|
|
30
30
|
say(`claude CLI: ${claude.stdout.trim()}`);
|
|
31
31
|
|
|
32
|
+
// 1b) codex CLI (optional alternate worker). Informational only — so Codex users
|
|
33
|
+
// see it's detected and supported, not just Claude Code. Galda can run workers on
|
|
34
|
+
// either; you pick per task (Claude Code / Codex) in the board's composer.
|
|
35
|
+
const codex = probe('codex', ['--version']);
|
|
36
|
+
if (!codex.error && codex.status === 0) say(`codex CLI: ${codex.stdout.trim()} (Codex)`);
|
|
37
|
+
|
|
32
38
|
// 2) Chrome (for verification proof) — recommended; warn if missing
|
|
33
39
|
const chromeCandidates = [
|
|
34
40
|
process.env.CHROME_PATH,
|
|
@@ -57,7 +63,11 @@ process.env.MANAGER_OPEN_BROWSER = process.env.MANAGER_OPEN_BROWSER ?? '1';
|
|
|
57
63
|
// and `npm start` from the repo stay env-driven and don't hit the live worker.
|
|
58
64
|
// The license public key is already baked in engine/lib.mjs (offline verify).
|
|
59
65
|
// Opt out with an empty value: `MANAGER_BILLING_API_URL= RELAY_URL= npx @galda/cli`.
|
|
60
|
-
|
|
66
|
+
// Use the OWNED custom domain (galda.app), never the Cloudflare workers.dev
|
|
67
|
+
// subdomain — that subdomain is named after an unrelated client (a2c-tech) and
|
|
68
|
+
// must never appear in Galda's product. galda.app is bound to the same Worker
|
|
69
|
+
// and serves the whole API (/connect, /checkout, /api/*). See [[no-a2c-tech]].
|
|
70
|
+
process.env.MANAGER_BILLING_API_URL = process.env.MANAGER_BILLING_API_URL ?? 'https://galda.app';
|
|
61
71
|
process.env.RELAY_URL = process.env.RELAY_URL ?? 'wss://app.galda.app/agent';
|
|
62
72
|
await import(pathToFileURL(join(ROOT, 'engine', 'server.mjs')).href);
|
|
63
73
|
|
|
@@ -0,0 +1,38 @@
|
|
|
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 → engine, once per install when the MCP client (Claude
|
|
13
|
+
// Code / Codex) first connects = "setup completed"
|
|
14
|
+
// (maybeEmitSetupCompleted in server.mjs)
|
|
15
|
+
// task_created → engine, when a task/goal is accepted
|
|
16
|
+
// task_completed → engine, task.status = 'done'
|
|
17
|
+
// task_failed → engine, task.status = 'failed'
|
|
18
|
+
// task_interrupted → engine, task.status = 'interrupted'
|
|
19
|
+
// review_approved / review_dismissed → on the review action
|
|
20
|
+
|
|
21
|
+
const BILLING_API_URL = (process.env.MANAGER_BILLING_API_URL ?? '').replace(/\/$/, '');
|
|
22
|
+
|
|
23
|
+
export function analyticsEnabled() {
|
|
24
|
+
return Boolean(BILLING_API_URL);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// type: one of the EVENT_TYPES; uid: account email/id (hashed server-side);
|
|
28
|
+
// meta: optional allow-listed non-sensitive fields only.
|
|
29
|
+
export function emitEvent(type, uid, meta) {
|
|
30
|
+
if (!BILLING_API_URL) return; // analytics off when unconfigured — no-op
|
|
31
|
+
try {
|
|
32
|
+
fetch(`${BILLING_API_URL}/events`, {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: { 'content-type': 'application/json' },
|
|
35
|
+
body: JSON.stringify({ type, uid, meta }),
|
|
36
|
+
}).catch(() => {}); // fire-and-forget: never surface network errors
|
|
37
|
+
} catch { /* never throw into the caller */ }
|
|
38
|
+
}
|
package/engine/lib.mjs
CHANGED
|
@@ -2376,3 +2376,13 @@ export async function verifyLicenseToken({ token, publicJwk = LICENSE_PUBLIC_JWK
|
|
|
2376
2376
|
}
|
|
2377
2377
|
return { valid: true, reason: null, payload };
|
|
2378
2378
|
}
|
|
2379
|
+
|
|
2380
|
+
// Setup-completion analytics: the `signup` event fires exactly ONCE per install,
|
|
2381
|
+
// the first time the MCP client (Claude Code / Codex) connects to the Manager.
|
|
2382
|
+
// Pure decision so it can be unit-tested without a live server or filesystem —
|
|
2383
|
+
// the caller supplies whether the request is from the MCP client, whether this
|
|
2384
|
+
// process already saw it this session, and whether the persisted once-ever flag
|
|
2385
|
+
// already exists on disk.
|
|
2386
|
+
export function shouldEmitSetupCompleted({ isMcpClient, alreadySeen, flagExists }) {
|
|
2387
|
+
return Boolean(isMcpClient) && !alreadySeen && !flagExists;
|
|
2388
|
+
}
|
package/engine/mcp.mjs
CHANGED
|
@@ -24,7 +24,10 @@ const KEY = process.env.MANAGER_KEY
|
|
|
24
24
|
|
|
25
25
|
async function api(path, opts = {}) {
|
|
26
26
|
const sep = path.includes('?') ? '&' : '?';
|
|
27
|
-
|
|
27
|
+
// Tag every MCP→Manager call so the server can detect "connected to Claude
|
|
28
|
+
// Code / Codex" (the setup-completed signal) — see maybeEmitSetupCompleted.
|
|
29
|
+
const headers = { ...(opts.headers || {}), 'x-manager-client': 'mcp' };
|
|
30
|
+
const r = await fetch(`${BASE}${path}${sep}key=${KEY}`, { ...opts, headers });
|
|
28
31
|
if (!r.ok) throw new Error(`Manager server ${r.status}: ${(await r.text()).slice(0, 200)}`);
|
|
29
32
|
return r.json();
|
|
30
33
|
}
|
package/engine/relay-client.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
//
|
|
9
9
|
// RELAY_URL=wss://relay.example/agent RELAY_AGENT_TOKEN=… node engine/relay-client.mjs
|
|
10
10
|
|
|
11
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
11
|
+
import { readFileSync, existsSync, watch } from 'node:fs';
|
|
12
12
|
import { resolve, dirname, join } from 'node:path';
|
|
13
13
|
import { homedir } from 'node:os';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
@@ -19,19 +19,40 @@ const DATA_DIR = process.env.MANAGER_HOME
|
|
|
19
19
|
const LOCAL = process.env.MANAGER_LOCAL ?? `http://localhost:${process.env.MANAGER_PORT ?? 4400}`;
|
|
20
20
|
const KEY = existsSync(join(DATA_DIR, 'secret.key')) ? readFileSync(join(DATA_DIR, 'secret.key'), 'utf8').trim() : '';
|
|
21
21
|
const RELAY = process.env.RELAY_URL ?? 'ws://localhost:5500/agent';
|
|
22
|
+
const DEV_EMAIL = process.env.RELAY_DEV_EMAIL ?? '';
|
|
23
|
+
const LICENSE_FILE = join(DATA_DIR, 'license.token');
|
|
22
24
|
// The relay (P3.1) authenticates the agent by its signed LICENSE TOKEN — only
|
|
23
25
|
// the billing Worker can mint one for a verified email, so the relay binds this
|
|
24
26
|
// socket to that email's id (no shared token, unspoofable). Falls back to an
|
|
25
27
|
// explicit RELAY_AGENT_TOKEN, and to a dev email for local testing.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
// Read FRESH each connect (not once at load): on first run the agent starts with
|
|
29
|
+
// no token (bound to nobody); when the user signs in, license.token appears and
|
|
30
|
+
// the watcher below reconnects, so the relay re-binds this socket to the real
|
|
31
|
+
// identity and the user's hosted board comes alive — no CLI restart needed.
|
|
32
|
+
function readToken() {
|
|
33
|
+
const lic = existsSync(LICENSE_FILE) ? readFileSync(LICENSE_FILE, 'utf8').trim() : '';
|
|
34
|
+
return process.env.RELAY_AGENT_TOKEN ?? lic;
|
|
35
|
+
}
|
|
29
36
|
|
|
30
37
|
const live = new Map(); // reqId -> AbortController
|
|
38
|
+
let currentWs = null;
|
|
39
|
+
let reconnectTimer = null;
|
|
40
|
+
let lastToken = readToken();
|
|
41
|
+
|
|
42
|
+
function scheduleReconnect(retryMs) {
|
|
43
|
+
if (reconnectTimer) return; // exactly one pending reconnect — never stack timers
|
|
44
|
+
reconnectTimer = setTimeout(() => { reconnectTimer = null; connect(Math.min(retryMs * 2, 15000)); }, retryMs);
|
|
45
|
+
}
|
|
31
46
|
|
|
32
47
|
function connect(retryMs = 1000) {
|
|
33
|
-
|
|
48
|
+
// Single-socket invariant: never open a second socket while one is CONNECTING
|
|
49
|
+
// (0) or OPEN (1). Two live sockets with the same identity make the relay evict
|
|
50
|
+
// each other (server.mjs "replaced by a newer connection") → an infinite flap.
|
|
51
|
+
if (currentWs && (currentWs.readyState === 0 || currentWs.readyState === 1)) return;
|
|
52
|
+
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
53
|
+
const auth = DEV_EMAIL ? `devEmail=${encodeURIComponent(DEV_EMAIL)}` : `token=${encodeURIComponent(readToken())}`;
|
|
34
54
|
const ws = new WebSocket(`${RELAY}?${auth}`);
|
|
55
|
+
currentWs = ws;
|
|
35
56
|
ws.onopen = () => { retryMs = 1000; console.log(`[agent] connected to relay ${RELAY}`); };
|
|
36
57
|
ws.onmessage = async (ev) => {
|
|
37
58
|
let m; try { m = JSON.parse(ev.data); } catch { return; }
|
|
@@ -73,10 +94,35 @@ function connect(retryMs = 1000) {
|
|
|
73
94
|
}
|
|
74
95
|
};
|
|
75
96
|
ws.onclose = () => {
|
|
97
|
+
if (ws !== currentWs) return; // a superseded socket (we already moved on) — don't reconnect
|
|
76
98
|
console.log(`[agent] relay connection lost — retrying in ${retryMs}ms`);
|
|
77
|
-
|
|
99
|
+
scheduleReconnect(retryMs);
|
|
78
100
|
};
|
|
79
101
|
ws.onerror = () => { /* onclose follows */ };
|
|
80
102
|
}
|
|
81
103
|
|
|
82
104
|
connect();
|
|
105
|
+
|
|
106
|
+
// Sign-in reconnect: when license.token appears/changes (the user just linked
|
|
107
|
+
// this device), drop the current socket so connect() re-dials with the new
|
|
108
|
+
// identity. Without this the first-run socket stays bound to nobody and the
|
|
109
|
+
// hosted board never comes alive until the CLI is restarted. Watch the DIR (the
|
|
110
|
+
// file may not exist yet on first run); best-effort (fs.watch unsupported → the
|
|
111
|
+
// onclose retry loop still re-reads the token on the next natural reconnect).
|
|
112
|
+
try {
|
|
113
|
+
watch(DATA_DIR, (_ev, f) => {
|
|
114
|
+
if (f && f !== 'license.token') return;
|
|
115
|
+
const t = readToken();
|
|
116
|
+
if (t && t !== lastToken) {
|
|
117
|
+
lastToken = t;
|
|
118
|
+
console.log('[agent] signed in — reconnecting to relay with your identity');
|
|
119
|
+
// Detach the old socket FIRST so its onclose is ignored (ws !== currentWs),
|
|
120
|
+
// then reconnect once, immediately, with a fresh backoff — no racing chains.
|
|
121
|
+
const old = currentWs;
|
|
122
|
+
currentWs = null;
|
|
123
|
+
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
124
|
+
try { old?.close(); } catch { /* already gone */ }
|
|
125
|
+
connect(1000);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
} catch { /* fs.watch unavailable on this platform */ }
|
package/engine/server.mjs
CHANGED
|
@@ -14,12 +14,13 @@
|
|
|
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 {
|
|
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';
|
|
21
22
|
import { pathToFileURL } from 'node:url';
|
|
22
|
-
import { parseStreamEvents, parsePlan, refinePlanTasks, findOverlappingGoal, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, isForcedStop, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildGoalProofMd, buildReviewRuleSection, nextGoalStatus, isPrMerged, isPrApproved, reviewToDoneStatus, advanceReviewGoal, revertReviewGoal, approveGoal, dismissGoal, permissionModeFor, isPlanReview, nextAfterPlanApprove, GOAL_MODES, parseSkillFrontmatter, collectSkills, retestGoal, cancelRetestGoal, computeRetestOutcome, revertGoal, planRevertActions, archiveGoal, unarchiveGoal, undismissGoal, parseNumstat, truncateDiffText, sumUsage, resolveEntryUrl, parseGitLog, diffNewCommits, replayQueueLog, reconcileOrphanGoals, reorderQueue, sortQueueByPriority, TASK_PRIORITIES, validateWorkflowColumns, DEFAULT_WORKFLOW_COLUMNS, validateReviewDefinition, DEFAULT_REVIEW_DEFINITION, buildEphemeralSeedLog, buildEphemeralSeedProjects, trimTaskActivityForState, buildAskContext, WORKER_TOOLS, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, nextRunnableTasks, goalsConflict, detectConflicts, isUiChange, shouldCaptureProof, shouldCaptureBaseline, hasTestRelevantChanges, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, verifyLicenseToken } from './lib.mjs';
|
|
23
|
+
import { parseStreamEvents, parsePlan, refinePlanTasks, findOverlappingGoal, canEditGoal, canDeleteGoal, shouldAskClarification, workerExitReason, isForcedStop, workerResultText, taskCountsAsComplete, resolveWantsPR, resolveGoalSource, buildGoalSummary, buildGoalProofMd, buildReviewRuleSection, nextGoalStatus, isPrMerged, isPrApproved, reviewToDoneStatus, advanceReviewGoal, revertReviewGoal, approveGoal, dismissGoal, permissionModeFor, isPlanReview, nextAfterPlanApprove, GOAL_MODES, parseSkillFrontmatter, collectSkills, retestGoal, cancelRetestGoal, computeRetestOutcome, revertGoal, planRevertActions, archiveGoal, unarchiveGoal, undismissGoal, parseNumstat, truncateDiffText, sumUsage, resolveEntryUrl, parseGitLog, diffNewCommits, replayQueueLog, reconcileOrphanGoals, reorderQueue, sortQueueByPriority, TASK_PRIORITIES, validateWorkflowColumns, DEFAULT_WORKFLOW_COLUMNS, validateReviewDefinition, DEFAULT_REVIEW_DEFINITION, buildEphemeralSeedLog, buildEphemeralSeedProjects, trimTaskActivityForState, buildAskContext, WORKER_TOOLS, verifyCfAccessJwt, resolveIdentity, goalVisibleTo, clampParallelLimit, canStartMore, nextRunnableTasks, goalsConflict, detectConflicts, isUiChange, shouldCaptureProof, shouldCaptureBaseline, hasTestRelevantChanges, buildExecutionPlan, buildContextHandoffSummary, buildFailurePostmortem, appendFailureMemory, latestFailurePolicy, classifyChangeRisk, checkRunBudget, usageBudgetTokens, isRateLimited, nextResumeDelay, checkFreeTierLimit, resolveEntitlement, resolveCachedEntitlement, FREE_TIER_LIMITS, verifyLicenseToken, shouldEmitSetupCompleted } from './lib.mjs';
|
|
23
24
|
import { openPR } from './pr.mjs';
|
|
24
25
|
import { runVerification, exerciseUi } from './verify.mjs';
|
|
25
26
|
|
|
@@ -36,6 +37,24 @@ 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);
|
|
44
|
+
// "Setup completed" = npx-installed AND connected to Claude Code / Codex. That
|
|
45
|
+
// connection is the MCP client (engine/mcp.mjs) first reaching this server, so
|
|
46
|
+
// we fire the `signup` event once, the first time an MCP-tagged request arrives,
|
|
47
|
+
// and persist a flag so it counts once per install ever (not once per restart).
|
|
48
|
+
const SETUP_FLAG = join(DATA_DIR, 'setup-completed.flag');
|
|
49
|
+
let _setupClientSeen = false;
|
|
50
|
+
function maybeEmitSetupCompleted(req) {
|
|
51
|
+
if (_setupClientSeen) return; // already decided this session — no repeat disk hit
|
|
52
|
+
if ((req.headers['x-manager-client'] || '') !== 'mcp') return;
|
|
53
|
+
_setupClientSeen = true;
|
|
54
|
+
if (!shouldEmitSetupCompleted({ isMcpClient: true, alreadySeen: false, flagExists: existsSync(SETUP_FLAG) })) return;
|
|
55
|
+
try { writeFileSync(SETUP_FLAG, String(Date.now())); } catch { /* best effort — still emit */ }
|
|
56
|
+
emitEvent('signup', ANALYTICS_UID, { source: 'mcp-connect' });
|
|
57
|
+
}
|
|
39
58
|
// Per-worker run cap. Env-configurable so it can be tuned operationally and the
|
|
40
59
|
// timeout→'interrupted' path is testable (a test sets a short value + a slow
|
|
41
60
|
// fake worker instead of waiting 10 real minutes).
|
|
@@ -541,6 +560,56 @@ function saveTask(t) {
|
|
|
541
560
|
appendFileSync(logFile, JSON.stringify({ ...persist, kind: 'task' }) + '\n');
|
|
542
561
|
const goal = goals.find((g) => g.id === t.goalId);
|
|
543
562
|
if (goal) sendGoalEvent({ ev: 'task', task: persist }, goal); else send({ ev: 'task', task: persist });
|
|
563
|
+
emitTaskAnalytics(t);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Phase 1 usage analytics (docs/DATA-RETENTION-PLAN.md). Fire-and-forget, no
|
|
567
|
+
// content: a task's creation and its terminal outcome are counted once each,
|
|
568
|
+
// keyed by ANALYTICS_UID (the hashed, non-credential install id). saveTask runs
|
|
569
|
+
// on every status change, so a per-(id,event) Set dedups repeat saves.
|
|
570
|
+
const _analyticsEmitted = new Set();
|
|
571
|
+
const _TERMINAL_EVENT = { done: 'task_completed', failed: 'task_failed', interrupted: 'task_interrupted' };
|
|
572
|
+
function emitTaskAnalytics(t) {
|
|
573
|
+
if (!t || t.id == null) return;
|
|
574
|
+
if (!_analyticsEmitted.has(`created:${t.id}`)) {
|
|
575
|
+
_analyticsEmitted.add(`created:${t.id}`);
|
|
576
|
+
emitEvent('task_created', ANALYTICS_UID, { source: 'engine' });
|
|
577
|
+
}
|
|
578
|
+
const ev = _TERMINAL_EVENT[t.status];
|
|
579
|
+
if (ev && !_analyticsEmitted.has(`${t.status}:${t.id}`)) {
|
|
580
|
+
_analyticsEmitted.add(`${t.status}:${t.id}`);
|
|
581
|
+
emitEvent(ev, ANALYTICS_UID, { status: t.status, source: 'engine' });
|
|
582
|
+
postHistory(t);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Phase 2 cloud history (paid). On a finished task, post an encrypted history
|
|
587
|
+
// record (the Worker gates on the license token and encrypts the content, so
|
|
588
|
+
// free / no-token installs store nothing — their work stays fully local).
|
|
589
|
+
// Fire-and-forget: never blocks or breaks a run.
|
|
590
|
+
function readLicenseToken() {
|
|
591
|
+
try { return existsSync(licenseFile) ? readFileSync(licenseFile, 'utf8').trim() : null; } catch { return null; }
|
|
592
|
+
}
|
|
593
|
+
function postHistory(t) {
|
|
594
|
+
if (!BILLING_API_URL) return;
|
|
595
|
+
const token = readLicenseToken();
|
|
596
|
+
if (!token) return; // no license → nothing stored server-side (stays local)
|
|
597
|
+
const goal = goals.find((g) => g.id === t.goalId);
|
|
598
|
+
const project = projects.find((p) => p.id === t.projectId);
|
|
599
|
+
try {
|
|
600
|
+
fetch(`${BILLING_API_URL}/history`, {
|
|
601
|
+
method: 'POST',
|
|
602
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
|
|
603
|
+
body: JSON.stringify({
|
|
604
|
+
id: `t${t.id}`,
|
|
605
|
+
project: project?.name ?? null,
|
|
606
|
+
content: goal?.text ?? null, // the user's request — encrypted server-side
|
|
607
|
+
summary: t.result ?? null, // the task's result — encrypted server-side
|
|
608
|
+
status: t.status,
|
|
609
|
+
createdAt: t.startedAt ? Date.parse(t.startedAt) : Date.now(),
|
|
610
|
+
}),
|
|
611
|
+
}).catch(() => {});
|
|
612
|
+
} catch { /* never throw into the caller */ }
|
|
544
613
|
}
|
|
545
614
|
const ACTIVITY_LIMIT = 1000;
|
|
546
615
|
function activityLine(line) {
|
|
@@ -2302,6 +2371,8 @@ const server = createServer(async (req, res) => {
|
|
|
2302
2371
|
res.setHeader('set-cookie', `mkey=${ACCESS_KEY}; HttpOnly; SameSite=Lax; Path=/; Max-Age=31536000`);
|
|
2303
2372
|
}
|
|
2304
2373
|
}
|
|
2374
|
+
// Once past auth, the first MCP-client request marks setup as completed.
|
|
2375
|
+
maybeEmitSetupCompleted(req);
|
|
2305
2376
|
|
|
2306
2377
|
if (url.pathname === '/' || url.pathname === '/index.html') {
|
|
2307
2378
|
// no-cache: browsers were keeping a STALE index.html (no cache header before),
|
|
@@ -3265,10 +3336,34 @@ const server6 = createServer(server.listeners('request')[0]);
|
|
|
3265
3336
|
server6.on('error', (e) => console.log(`[manager] ::1 listen skipped: ${e.code}`));
|
|
3266
3337
|
try { server6.listen(PORT, '::1'); } catch { /* IPv6 loopback unavailable */ }
|
|
3267
3338
|
|
|
3339
|
+
// Guard the primary IPv4 listen the same way the ::1 twin is guarded: without
|
|
3340
|
+
// this, a busy port emits an unhandled 'error' and Node prints a raw stack
|
|
3341
|
+
// trace + crashes. The common cause is a second manager (or `npx @galda/cli`)
|
|
3342
|
+
// started while one is already running on this port — so say that, and how to
|
|
3343
|
+
// pick another port, instead of dumping a stack trace.
|
|
3344
|
+
server.on('error', (e) => {
|
|
3345
|
+
if (e.code === 'EADDRINUSE') {
|
|
3346
|
+
console.error(`[manager] port ${PORT} is already in use — another manager is likely running on it.`);
|
|
3347
|
+
console.error(`[manager] Use a different port: MANAGER_PORT=${PORT + 10} npx @galda/cli`);
|
|
3348
|
+
process.exit(1);
|
|
3349
|
+
}
|
|
3350
|
+
throw e;
|
|
3351
|
+
});
|
|
3352
|
+
|
|
3268
3353
|
server.listen(PORT, '127.0.0.1', () => {
|
|
3269
3354
|
console.log(`[manager] Manager for AI → http://localhost:${PORT}/?key=${ACCESS_KEY}`);
|
|
3270
3355
|
if (process.env.MANAGER_OPEN_BROWSER === '1' && process.platform === 'darwin') {
|
|
3271
|
-
|
|
3356
|
+
// First run in the hosted flow (no license yet, billing worker configured):
|
|
3357
|
+
// open the one-click Google device-link straight away instead of the local
|
|
3358
|
+
// ?key board, so a non-engineer never sees a localhost URL or an access key —
|
|
3359
|
+
// they sign in once and their hosted board comes alive (relay-client rebinds
|
|
3360
|
+
// via its license.token watcher). Otherwise open the local board as before.
|
|
3361
|
+
let openUrl = `http://localhost:${PORT}/?key=${ACCESS_KEY}`;
|
|
3362
|
+
if (BILLING_API_URL && !existsSync(licenseFile)) {
|
|
3363
|
+
openUrl = `${BILLING_API_URL}/connect?port=${PORT}&state=${encodeURIComponent(newSigninNonce())}`;
|
|
3364
|
+
console.log('[manager] first run: opening one-click sign-in (no local key needed) → ' + openUrl);
|
|
3365
|
+
}
|
|
3366
|
+
spawn('open', ['-g', openUrl], { stdio: 'ignore' }).unref();
|
|
3272
3367
|
}
|
|
3273
3368
|
console.log(`[manager] projects: ${projects.map((p) => `${p.id}=${p.dir}`).join(' ')}`);
|
|
3274
3369
|
syncAllExternal().catch(() => {});
|
package/package.json
CHANGED