@galda/cli 0.10.8 → 0.10.9
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/bin/manager-for-ai.mjs +7 -0
- package/engine/server.mjs +54 -15
- package/package.json +1 -1
package/bin/manager-for-ai.mjs
CHANGED
|
@@ -86,6 +86,13 @@ process.env.MANAGER_OPEN_BROWSER = process.env.MANAGER_OPEN_BROWSER ?? '1';
|
|
|
86
86
|
// and serves the whole API (/connect, /checkout, /api/*). See [[no-a2c-tech]].
|
|
87
87
|
process.env.MANAGER_BILLING_API_URL = process.env.MANAGER_BILLING_API_URL ?? 'https://galda.app';
|
|
88
88
|
process.env.RELAY_URL = process.env.RELAY_URL ?? 'wss://app.galda.app/agent';
|
|
89
|
+
// The named app the user actually works in (over the relay). The CLI opens THIS,
|
|
90
|
+
// not the localhost board, in the hosted flow. Override for a self-hosted app.
|
|
91
|
+
process.env.MANAGER_APP_URL = process.env.MANAGER_APP_URL ?? 'https://app.galda.app';
|
|
92
|
+
// `npx @galda/cli --signin` forces the Google sign-in even if a license already
|
|
93
|
+
// exists — the way to switch to a different account (a stale token otherwise
|
|
94
|
+
// binds you to the previous account).
|
|
95
|
+
if (process.argv.includes('--signin') || process.argv.includes('--login')) process.env.MANAGER_FORCE_SIGNIN = '1';
|
|
89
96
|
// Choose a free port now and pin it for BOTH the server and the relay-client, so
|
|
90
97
|
// a busy 4400 (another manager) no longer stops onboarding — no MANAGER_PORT by hand.
|
|
91
98
|
const wantedPort = Number(process.env.MANAGER_PORT ?? 4400);
|
package/engine/server.mjs
CHANGED
|
@@ -54,7 +54,7 @@ function maybeEmitSetupCompleted(req) {
|
|
|
54
54
|
_setupClientSeen = true;
|
|
55
55
|
if (!shouldEmitSetupCompleted({ isMcpClient: true, alreadySeen: false, flagExists: existsSync(SETUP_FLAG) })) return;
|
|
56
56
|
try { writeFileSync(SETUP_FLAG, String(Date.now())); } catch { /* best effort — still emit */ }
|
|
57
|
-
emitEvent('signup',
|
|
57
|
+
emitEvent('signup', analyticsUid(), { source: 'mcp-connect' });
|
|
58
58
|
}
|
|
59
59
|
// Per-worker run cap. Env-configurable so it can be tuned operationally and the
|
|
60
60
|
// timeout→'interrupted' path is testable (a test sets a short value + a slow
|
|
@@ -152,6 +152,15 @@ const MANAGER_OWNER_EMAIL = process.env.MANAGER_OWNER_EMAIL ?? '';
|
|
|
152
152
|
// still works standalone (e.g. for Collaborators / dev) with no billing-api
|
|
153
153
|
// configured at all.
|
|
154
154
|
const BILLING_API_URL = (process.env.MANAGER_BILLING_API_URL ?? '').replace(/\/$/, '');
|
|
155
|
+
// The user's real UI in the hosted flow is the NAMED app (app.galda.app),
|
|
156
|
+
// reached over the relay — never the localhost board (Masa 2026-07-15:
|
|
157
|
+
// 「npm→ターミナル→app.galda.appに戻る / localhostでなくアプリ名で開くべき」).
|
|
158
|
+
// The localhost board stays the entry only for local/dev (no billing) runs.
|
|
159
|
+
const APP_URL = (process.env.MANAGER_APP_URL ?? '').replace(/\/$/, '');
|
|
160
|
+
// Force the sign-in flow even when a license already exists — so a user can
|
|
161
|
+
// switch to a different Google account (a stale token from a previous account
|
|
162
|
+
// otherwise silently binds the relay to the wrong account). Set by `--signin`.
|
|
163
|
+
const FORCE_SIGNIN = process.env.MANAGER_FORCE_SIGNIN === '1';
|
|
155
164
|
const entitlementCache = new Map(); // billing email -> { isPaying, checkedAt }
|
|
156
165
|
|
|
157
166
|
// License token (local-first identity, docs/BILLING-LAUNCH-PLAN.md): a signed
|
|
@@ -166,6 +175,14 @@ const entitlementCache = new Map(); // billing email -> { isPaying, checkedAt }
|
|
|
166
175
|
const licenseFile = join(DATA_DIR, 'license.token');
|
|
167
176
|
let licenseState = { email: null, isPaying: false, verifiedAt: 0 }; // in-memory cache of the last successful verify (isPaying is the token's snapshot; the live gate re-checks)
|
|
168
177
|
|
|
178
|
+
// Analytics identity (A+): once signed in, scope usage events to the ACCOUNT
|
|
179
|
+
// (the billing Worker hashes this email → the same account_id the accounts table
|
|
180
|
+
// carries, so per-user app activity joins WITHOUT a cookie). Not signed in →
|
|
181
|
+
// the anonymous per-install id. Raw email never lands in usage_events (the
|
|
182
|
+
// Worker pseudonymizes it server-side).
|
|
183
|
+
function analyticsUid() { return licenseState.email || ANALYTICS_UID; }
|
|
184
|
+
let _freeExhaustedEmitted = false;
|
|
185
|
+
|
|
169
186
|
// One-time nonces for the "Sign in with Google" loopback (GET /api/signin-url →
|
|
170
187
|
// billing /connect → Google → billing → GET /oauth/callback). The engine issues
|
|
171
188
|
// the nonce, round-trips it through the flow, and requires it back — so a random
|
|
@@ -216,12 +233,17 @@ function signinResultPage(outcome) {
|
|
|
216
233
|
const body = outcome.ok
|
|
217
234
|
? `<h1>Signed in</h1><p>You're signed in as <b>${esc(outcome.email)}</b>. This tab will close automatically.</p>`
|
|
218
235
|
: `<h1>Sign-in didn't complete</h1><p>${esc(outcome.error)}</p><p class="muted">Close this tab and click “Sign in with Google” again in the app.</p>`;
|
|
236
|
+
// When the sign-in was opened FROM the app tab (window.opener present), tell
|
|
237
|
+
// it and close. When it was opened by the CLI at startup (no opener — `open`
|
|
238
|
+
// can't script-close it), don't strand the user on a localhost page: send them
|
|
239
|
+
// to the named app so the flow is npm → terminal → app.galda.app.
|
|
240
|
+
const goApp = outcome.ok && APP_URL ? JSON.stringify(APP_URL) : 'null';
|
|
219
241
|
return `<!doctype html><meta charset="utf-8"><title>Galda</title>` +
|
|
220
242
|
`<body style="font:15px/1.6 -apple-system,system-ui,sans-serif;max-width:32rem;margin:12vh auto;padding:0 1.5rem;color:#111">` +
|
|
221
243
|
`<style>h1{font-size:1.35rem;margin:0 0 .5rem}.muted{color:#888;font-size:.9rem}b{font-weight:600}</style>` +
|
|
222
244
|
body +
|
|
223
|
-
`<script>try{if(window.opener)window.opener.postMessage(${payload},'*')}catch(e){}` +
|
|
224
|
-
(outcome.ok ? `setTimeout(function(){try{window.close()}catch(e){}},
|
|
245
|
+
`<script>var opened=false;try{if(window.opener){window.opener.postMessage(${payload},'*');opened=true;}}catch(e){}` +
|
|
246
|
+
(outcome.ok ? `var app=${goApp};setTimeout(function(){if(opened){try{window.close()}catch(e){}}else if(app){location.replace(app)}else{try{window.close()}catch(e){}}},900);` : '') +
|
|
225
247
|
`</script></body>`;
|
|
226
248
|
}
|
|
227
249
|
|
|
@@ -348,7 +370,7 @@ async function requireEntitlementGate(identity) {
|
|
|
348
370
|
const usage = { ...existing, pendingCount: existing.pendingCount + 1, cumulativeCount: existing.cumulativeCount + 1 };
|
|
349
371
|
if (!checkFreeTierLimit(usage).blocked) return { allowed: true, blocked: null };
|
|
350
372
|
const billingEmail = licenseState.email;
|
|
351
|
-
if (!billingEmail)
|
|
373
|
+
if (!billingEmail) { const r = resolveEntitlement({ isOwner: false, isPaying: false, usage }); noteFreeExhausted(r); return r; }
|
|
352
374
|
const cachedState = resolveCachedEntitlement({ cached: entitlementCache.get(billingEmail) ?? null });
|
|
353
375
|
let isPaying = cachedState.isPaying;
|
|
354
376
|
if (cachedState.needsRefresh) {
|
|
@@ -358,7 +380,17 @@ async function requireEntitlementGate(identity) {
|
|
|
358
380
|
entitlementCache.set(billingEmail, { isPaying: fresh, checkedAt: Date.now() });
|
|
359
381
|
}
|
|
360
382
|
}
|
|
361
|
-
|
|
383
|
+
const result = resolveEntitlement({ isOwner: false, isPaying, usage });
|
|
384
|
+
noteFreeExhausted(result);
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
// Emit one free_exhausted funnel event the first time a non-paying install is
|
|
388
|
+
// blocked by the free-tier cap (the funnel dedupes by distinct account/install,
|
|
389
|
+
// so once-per-session is enough).
|
|
390
|
+
function noteFreeExhausted(result) {
|
|
391
|
+
if (_freeExhaustedEmitted || !result?.blocked) return;
|
|
392
|
+
_freeExhaustedEmitted = true;
|
|
393
|
+
emitEvent('free_exhausted', analyticsUid());
|
|
362
394
|
}
|
|
363
395
|
// The acting identity for a request: 'owner' for key-auth or
|
|
364
396
|
// MANAGER_OWNER_EMAIL, else the verified Google email itself. Stamped onto
|
|
@@ -578,12 +610,12 @@ function emitTaskAnalytics(t) {
|
|
|
578
610
|
if (!t || t.id == null) return;
|
|
579
611
|
if (!_analyticsEmitted.has(`created:${t.id}`)) {
|
|
580
612
|
_analyticsEmitted.add(`created:${t.id}`);
|
|
581
|
-
emitEvent('task_created',
|
|
613
|
+
emitEvent('task_created', analyticsUid(), { source: 'engine' });
|
|
582
614
|
}
|
|
583
615
|
const ev = _TERMINAL_EVENT[t.status];
|
|
584
616
|
if (ev && !_analyticsEmitted.has(`${t.status}:${t.id}`)) {
|
|
585
617
|
_analyticsEmitted.add(`${t.status}:${t.id}`);
|
|
586
|
-
emitEvent(ev,
|
|
618
|
+
emitEvent(ev, analyticsUid(), { status: t.status, source: 'engine' });
|
|
587
619
|
postHistory(t);
|
|
588
620
|
}
|
|
589
621
|
}
|
|
@@ -3428,15 +3460,22 @@ server.on('error', (e) => {
|
|
|
3428
3460
|
server.listen(PORT, '127.0.0.1', () => {
|
|
3429
3461
|
console.log(`[manager] Manager for AI → http://localhost:${PORT}/?key=${ACCESS_KEY}`);
|
|
3430
3462
|
if (process.env.MANAGER_OPEN_BROWSER === '1' && process.platform === 'darwin') {
|
|
3431
|
-
//
|
|
3432
|
-
//
|
|
3433
|
-
//
|
|
3434
|
-
//
|
|
3435
|
-
//
|
|
3463
|
+
// Hosted flow (billing worker + named app configured): the user lives in
|
|
3464
|
+
// app.galda.app, driven over the relay — they must NEVER be dropped on a
|
|
3465
|
+
// localhost board or an access key (Masa 2026-07-15: 「npm→ターミナル→
|
|
3466
|
+
// app.galda.appに戻る」). Not signed in (or switching accounts via --signin)
|
|
3467
|
+
// → one-click Google sign-in; already signed in → straight to the app (the
|
|
3468
|
+
// board comes alive as relay-client rebinds via its license.token watcher).
|
|
3469
|
+
// The localhost ?key board stays the entry ONLY for local/dev (no billing).
|
|
3436
3470
|
let openUrl = `http://localhost:${PORT}/?key=${ACCESS_KEY}`;
|
|
3437
|
-
if (BILLING_API_URL &&
|
|
3438
|
-
|
|
3439
|
-
|
|
3471
|
+
if (BILLING_API_URL && APP_URL) {
|
|
3472
|
+
const signedIn = existsSync(licenseFile) && !FORCE_SIGNIN;
|
|
3473
|
+
openUrl = signedIn
|
|
3474
|
+
? APP_URL
|
|
3475
|
+
: `${BILLING_API_URL}/connect?port=${PORT}&state=${encodeURIComponent(newSigninNonce())}`;
|
|
3476
|
+
console.log(signedIn
|
|
3477
|
+
? `[manager] opening your app → ${APP_URL}`
|
|
3478
|
+
: `[manager] ${FORCE_SIGNIN ? 'switching account' : 'first run'}: opening one-click sign-in → ${openUrl}`);
|
|
3440
3479
|
}
|
|
3441
3480
|
spawn('open', ['-g', openUrl], { stdio: 'ignore' }).unref();
|
|
3442
3481
|
}
|
package/package.json
CHANGED