vibegroup 0.1.3 → 0.1.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/dist/cli.js +114 -234
- package/package.json +1 -1
- package/plugin/commands/init.md +14 -21
package/dist/cli.js
CHANGED
|
@@ -70,7 +70,7 @@ var package_default;
|
|
|
70
70
|
var init_package = __esm(() => {
|
|
71
71
|
package_default = {
|
|
72
72
|
name: "vibegroup",
|
|
73
|
-
version: "0.1.
|
|
73
|
+
version: "0.1.5",
|
|
74
74
|
description: "Talk to your teammates' Claude Code agents — agent-to-agent collaboration for Claude Code over a shared channel.",
|
|
75
75
|
type: "module",
|
|
76
76
|
bin: {
|
|
@@ -230,6 +230,32 @@ var init_install = __esm(() => {
|
|
|
230
230
|
init_pluginInstall();
|
|
231
231
|
});
|
|
232
232
|
|
|
233
|
+
// src/lib/channel.ts
|
|
234
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
235
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
236
|
+
import { dirname as dirname3 } from "node:path";
|
|
237
|
+
function readManagedSettings() {
|
|
238
|
+
try {
|
|
239
|
+
const p = managedSettingsPath();
|
|
240
|
+
if (existsSync3(p))
|
|
241
|
+
return JSON.parse(readFileSync3(p, "utf8"));
|
|
242
|
+
} catch {}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
function channelAllowlisted() {
|
|
246
|
+
return isAllowlisted(readManagedSettings());
|
|
247
|
+
}
|
|
248
|
+
function enableChannelWithSudo() {
|
|
249
|
+
const path = managedSettingsPath();
|
|
250
|
+
const json = JSON.stringify(mergeManagedSettings(readManagedSettings()), null, 2);
|
|
251
|
+
const script = `mkdir -p "${dirname3(path)}" && cat > "${path}"`;
|
|
252
|
+
const r = spawnSync2("sudo", ["sh", "-c", script], { input: json, stdio: ["pipe", "inherit", "inherit"] });
|
|
253
|
+
return r.status === 0;
|
|
254
|
+
}
|
|
255
|
+
var init_channel = __esm(() => {
|
|
256
|
+
init_allowlist();
|
|
257
|
+
});
|
|
258
|
+
|
|
233
259
|
// src/ui/runner.ts
|
|
234
260
|
import { render } from "ink";
|
|
235
261
|
function runInk(make) {
|
|
@@ -249,144 +275,30 @@ function runInk(make) {
|
|
|
249
275
|
}
|
|
250
276
|
var init_runner = () => {};
|
|
251
277
|
|
|
252
|
-
// src/lib/
|
|
253
|
-
async function
|
|
254
|
-
const
|
|
255
|
-
try {
|
|
256
|
-
return text ? JSON.parse(text) : {};
|
|
257
|
-
} catch {
|
|
258
|
-
return {};
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
function parseCeremony(c) {
|
|
262
|
-
if (!c || !c.user_code || !c.verification_uri)
|
|
263
|
-
return;
|
|
264
|
-
return {
|
|
265
|
-
userCode: c.user_code,
|
|
266
|
-
verificationUri: c.verification_uri,
|
|
267
|
-
interval: typeof c.interval === "number" ? c.interval : 5,
|
|
268
|
-
expiresIn: typeof c.expires_in === "number" ? c.expires_in : 600
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
function tokensFromResponse(body, nowMs) {
|
|
272
|
-
const tokens = { accessToken: body.access_token };
|
|
273
|
-
if (typeof body.expires_in === "number") {
|
|
274
|
-
tokens.accessTokenExpiresAt = new Date(nowMs + body.expires_in * 1000).toISOString();
|
|
275
|
-
}
|
|
276
|
-
if (body.identity_assertion)
|
|
277
|
-
tokens.identityAssertion = body.identity_assertion;
|
|
278
|
-
if (body.assertion_expires)
|
|
279
|
-
tokens.assertionExpiresAt = body.assertion_expires;
|
|
280
|
-
if (body.scope)
|
|
281
|
-
tokens.scope = body.scope;
|
|
282
|
-
return tokens;
|
|
283
|
-
}
|
|
284
|
-
async function discover(apiBase, f) {
|
|
285
|
-
const base = apiBase.replace(/\/+$/, "");
|
|
286
|
-
const prmRes = await f(`${base}/.well-known/oauth-protected-resource`);
|
|
287
|
-
if (!prmRes.ok)
|
|
288
|
-
throw new Error(`discovery failed: protected-resource metadata HTTP ${prmRes.status}`);
|
|
289
|
-
const prm = await json(prmRes);
|
|
290
|
-
const as = (prm.authorization_servers ?? [])[0];
|
|
291
|
-
if (!as)
|
|
292
|
-
throw new Error("discovery failed: no authorization_servers in protected-resource metadata");
|
|
293
|
-
const asRes = await f(`${as.replace(/\/+$/, "")}/.well-known/oauth-authorization-server`);
|
|
294
|
-
if (!asRes.ok)
|
|
295
|
-
throw new Error(`discovery failed: authorization-server metadata HTTP ${asRes.status}`);
|
|
296
|
-
const meta = await json(asRes);
|
|
297
|
-
const agent = meta.agent_auth ?? {};
|
|
298
|
-
if (!meta.token_endpoint || !agent.identity_endpoint || !agent.claim_endpoint) {
|
|
299
|
-
throw new Error("discovery failed: authorization server is missing agent_auth endpoints");
|
|
300
|
-
}
|
|
301
|
-
return {
|
|
302
|
-
issuer: meta.issuer,
|
|
303
|
-
tokenEndpoint: meta.token_endpoint,
|
|
304
|
-
revocationEndpoint: meta.revocation_endpoint,
|
|
305
|
-
identityEndpoint: agent.identity_endpoint,
|
|
306
|
-
claimEndpoint: agent.claim_endpoint,
|
|
307
|
-
resource: prm.resource ?? meta.resource,
|
|
308
|
-
scopesSupported: prm.scopes_supported
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
async function register(ep, body, f) {
|
|
312
|
-
const res = await f(ep.identityEndpoint, {
|
|
278
|
+
// src/lib/emailAuth.ts
|
|
279
|
+
async function startEmailLogin(apiBase, email, f) {
|
|
280
|
+
const res = await f(`${apiBase}/auth/email/start`, {
|
|
313
281
|
method: "POST",
|
|
314
282
|
headers: { "content-type": "application/json" },
|
|
315
|
-
body: JSON.stringify(
|
|
283
|
+
body: JSON.stringify({ email })
|
|
316
284
|
});
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
throw new Error(
|
|
285
|
+
if (!res.ok) {
|
|
286
|
+
const err = await res.json().catch(() => ({}));
|
|
287
|
+
throw new Error(err.detail ?? "could not send the code");
|
|
320
288
|
}
|
|
321
|
-
return {
|
|
322
|
-
registrationId: data.registration_id,
|
|
323
|
-
registrationType: data.registration_type,
|
|
324
|
-
identityAssertion: data.identity_assertion,
|
|
325
|
-
assertionExpires: data.assertion_expires,
|
|
326
|
-
preClaimScopes: data.pre_claim_scopes,
|
|
327
|
-
postClaimScopes: data.post_claim_scopes,
|
|
328
|
-
claimToken: data.claim_token,
|
|
329
|
-
claim: parseCeremony(data.claim)
|
|
330
|
-
};
|
|
331
289
|
}
|
|
332
|
-
async function
|
|
333
|
-
const res = await f(
|
|
290
|
+
async function verifyEmailLogin(apiBase, email, code, f) {
|
|
291
|
+
const res = await f(`${apiBase}/auth/email/verify`, {
|
|
334
292
|
method: "POST",
|
|
335
|
-
headers: { "content-type": "application/
|
|
336
|
-
body:
|
|
293
|
+
headers: { "content-type": "application/json" },
|
|
294
|
+
body: JSON.stringify({ email, code })
|
|
337
295
|
});
|
|
338
|
-
const data = await json(
|
|
339
|
-
if (res.ok
|
|
340
|
-
|
|
341
|
-
switch (data.error) {
|
|
342
|
-
case "authorization_pending":
|
|
343
|
-
return { status: "pending" };
|
|
344
|
-
case "slow_down":
|
|
345
|
-
return { status: "slow_down" };
|
|
346
|
-
case "expired_token":
|
|
347
|
-
return { status: "expired" };
|
|
348
|
-
default:
|
|
349
|
-
throw new Error(`claim poll failed: ${data.error ?? `HTTP ${res.status}`}`);
|
|
296
|
+
const data = await res.json().catch(() => ({}));
|
|
297
|
+
if (!res.ok) {
|
|
298
|
+
throw new Error(data.error === "invalid_code" ? "that code is incorrect or expired" : data.detail ?? "verification failed");
|
|
350
299
|
}
|
|
300
|
+
return { accessToken: data.access_token ?? "", identityAssertion: data.identity_assertion, scope: data.scope };
|
|
351
301
|
}
|
|
352
|
-
var CLAIM_GRANT = "urn:workos:agent-auth:grant-type:claim";
|
|
353
|
-
|
|
354
|
-
// src/lib/loginFlow.ts
|
|
355
|
-
async function login(apiBase, f, hooks) {
|
|
356
|
-
const emit = (e) => hooks.onEvent?.(e);
|
|
357
|
-
const sleep = hooks.sleep ?? defaultSleep;
|
|
358
|
-
const now = hooks.now ?? Date.now;
|
|
359
|
-
emit({ type: "discovering", apiBase });
|
|
360
|
-
const ep = await discover(apiBase, f);
|
|
361
|
-
emit({ type: "discovered", endpoints: ep });
|
|
362
|
-
const email = (await hooks.email())?.trim();
|
|
363
|
-
if (!email)
|
|
364
|
-
return null;
|
|
365
|
-
emit({ type: "registering", email });
|
|
366
|
-
const reg = await register(ep, { type: "service_auth", login_hint: email }, f);
|
|
367
|
-
if (!reg.claim || !reg.claimToken)
|
|
368
|
-
throw new Error("service_auth did not return a claim ceremony");
|
|
369
|
-
emit({ type: "claim", ceremony: reg.claim });
|
|
370
|
-
emit({ type: "polling" });
|
|
371
|
-
let interval = Math.max(1, reg.claim.interval);
|
|
372
|
-
const deadline = now() + (hooks.timeoutMs ?? 10 * 60 * 1000);
|
|
373
|
-
for (;; ) {
|
|
374
|
-
const r = await pollClaim(ep.tokenEndpoint, reg.claimToken, f, now());
|
|
375
|
-
if (r.status === "done") {
|
|
376
|
-
emit({ type: "done" });
|
|
377
|
-
return { tokens: r.tokens, email };
|
|
378
|
-
}
|
|
379
|
-
if (r.status === "expired")
|
|
380
|
-
throw new Error("the code expired — run `vibegroup login` again");
|
|
381
|
-
if (r.status === "slow_down")
|
|
382
|
-
interval += 5;
|
|
383
|
-
if (now() > deadline)
|
|
384
|
-
throw new Error("login timed out");
|
|
385
|
-
await sleep(interval * 1000);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
389
|
-
var init_loginFlow = () => {};
|
|
390
302
|
|
|
391
303
|
// src/ui/session.ts
|
|
392
304
|
function jwtClaim(jwt, key) {
|
|
@@ -406,17 +318,6 @@ function sessionFromTokens(tokens, email) {
|
|
|
406
318
|
};
|
|
407
319
|
}
|
|
408
320
|
|
|
409
|
-
// src/lib/openBrowser.ts
|
|
410
|
-
import { spawn } from "node:child_process";
|
|
411
|
-
function openBrowser(url) {
|
|
412
|
-
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
413
|
-
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
414
|
-
try {
|
|
415
|
-
spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
416
|
-
} catch {}
|
|
417
|
-
}
|
|
418
|
-
var init_openBrowser = () => {};
|
|
419
|
-
|
|
420
321
|
// src/ui/theme.ts
|
|
421
322
|
var color;
|
|
422
323
|
var init_theme = __esm(() => {
|
|
@@ -433,53 +334,42 @@ var init_theme = __esm(() => {
|
|
|
433
334
|
// src/ui/Login.tsx
|
|
434
335
|
import { useEffect, useRef, useState } from "react";
|
|
435
336
|
import { Box, Text } from "ink";
|
|
436
|
-
import { Spinner, StatusMessage, TextInput, Alert
|
|
337
|
+
import { Spinner, StatusMessage, TextInput, Alert } from "@inkjs/ui";
|
|
437
338
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
438
339
|
function Login({
|
|
439
340
|
apiBase,
|
|
440
341
|
initialEmail,
|
|
441
342
|
onExit
|
|
442
343
|
}) {
|
|
443
|
-
const [phase, setPhase] = useState(initialEmail ? { t: "
|
|
444
|
-
const opened = useRef(false);
|
|
344
|
+
const [phase, setPhase] = useState(initialEmail ? { t: "sending", email: initialEmail } : { t: "email" });
|
|
445
345
|
const started = useRef(false);
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
email: async () => email,
|
|
453
|
-
onEvent: (e) => {
|
|
454
|
-
if (e.type === "registering")
|
|
455
|
-
setPhase({ t: "registering", email: e.email });
|
|
456
|
-
else if (e.type === "claim")
|
|
457
|
-
setPhase({ t: "waiting", code: e.ceremony.userCode, url: e.ceremony.verificationUri });
|
|
458
|
-
}
|
|
459
|
-
}).then((res) => {
|
|
460
|
-
if (!res) {
|
|
461
|
-
setPhase({ t: "error", message: "Login cancelled." });
|
|
462
|
-
setTimeout(() => onExit(1), 50);
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
writeAuth(sessionFromTokens(res.tokens, res.email));
|
|
466
|
-
setPhase({ t: "done", email: res.email });
|
|
467
|
-
setTimeout(() => onExit(0), 500);
|
|
468
|
-
}).catch((err) => {
|
|
346
|
+
const send = async (email) => {
|
|
347
|
+
setPhase({ t: "sending", email });
|
|
348
|
+
try {
|
|
349
|
+
await startEmailLogin(apiBase, email, fetch);
|
|
350
|
+
setPhase({ t: "code", email });
|
|
351
|
+
} catch (err) {
|
|
469
352
|
setPhase({ t: "error", message: err?.message ?? String(err) });
|
|
470
353
|
setTimeout(() => onExit(1), 800);
|
|
471
|
-
}
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
const verify = async (email, code) => {
|
|
357
|
+
setPhase({ t: "verifying", email });
|
|
358
|
+
try {
|
|
359
|
+
const tokens = await verifyEmailLogin(apiBase, email, code, fetch);
|
|
360
|
+
writeAuth(sessionFromTokens(tokens, email));
|
|
361
|
+
setPhase({ t: "done", email });
|
|
362
|
+
setTimeout(() => onExit(0), 500);
|
|
363
|
+
} catch (err) {
|
|
364
|
+
setPhase({ t: "code", email, error: err?.message ?? String(err) });
|
|
365
|
+
}
|
|
472
366
|
};
|
|
473
367
|
useEffect(() => {
|
|
474
|
-
if (initialEmail)
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
useEffect(() => {
|
|
478
|
-
if (phase.t === "waiting" && !opened.current) {
|
|
479
|
-
opened.current = true;
|
|
480
|
-
openBrowser(phase.url);
|
|
368
|
+
if (initialEmail && !started.current) {
|
|
369
|
+
started.current = true;
|
|
370
|
+
send(initialEmail);
|
|
481
371
|
}
|
|
482
|
-
}, [
|
|
372
|
+
}, []);
|
|
483
373
|
return /* @__PURE__ */ jsxs(Box, {
|
|
484
374
|
flexDirection: "column",
|
|
485
375
|
gap: 1,
|
|
@@ -509,9 +399,9 @@ function Login({
|
|
|
509
399
|
children: /* @__PURE__ */ jsx(TextInput, {
|
|
510
400
|
placeholder: "you@company.com",
|
|
511
401
|
onSubmit: (value) => {
|
|
512
|
-
const email = value.trim();
|
|
513
|
-
if (email)
|
|
514
|
-
|
|
402
|
+
const email = value.trim().toLowerCase();
|
|
403
|
+
if (email.includes("@"))
|
|
404
|
+
send(email);
|
|
515
405
|
else
|
|
516
406
|
onExit(1);
|
|
517
407
|
}
|
|
@@ -519,47 +409,48 @@ function Login({
|
|
|
519
409
|
})
|
|
520
410
|
]
|
|
521
411
|
}),
|
|
522
|
-
phase.t === "
|
|
523
|
-
label:
|
|
412
|
+
phase.t === "sending" && /* @__PURE__ */ jsx(Spinner, {
|
|
413
|
+
label: `Sending a code to ${phase.email}…`
|
|
524
414
|
}),
|
|
525
|
-
phase.t === "
|
|
526
|
-
label: `Registering ${phase.email}…`
|
|
527
|
-
}),
|
|
528
|
-
phase.t === "waiting" && /* @__PURE__ */ jsxs(Box, {
|
|
415
|
+
phase.t === "code" && /* @__PURE__ */ jsxs(Box, {
|
|
529
416
|
flexDirection: "column",
|
|
530
|
-
gap: 1,
|
|
531
417
|
children: [
|
|
532
|
-
/* @__PURE__ */ jsxs(
|
|
533
|
-
borderStyle: "round",
|
|
534
|
-
borderColor: color.accent,
|
|
535
|
-
paddingX: 1,
|
|
536
|
-
flexDirection: "column",
|
|
418
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
537
419
|
children: [
|
|
538
|
-
|
|
539
|
-
children: "Open this link, sign in, and enter the code:"
|
|
540
|
-
}),
|
|
541
|
-
/* @__PURE__ */ jsx(Box, {
|
|
542
|
-
marginY: 1,
|
|
543
|
-
children: /* @__PURE__ */ jsx(Badge, {
|
|
544
|
-
color: "cyan",
|
|
545
|
-
children: phase.code
|
|
546
|
-
})
|
|
547
|
-
}),
|
|
420
|
+
"Enter the 6-digit code sent to ",
|
|
548
421
|
/* @__PURE__ */ jsx(Text, {
|
|
549
422
|
color: color.accent,
|
|
550
|
-
children: phase.
|
|
423
|
+
children: phase.email
|
|
551
424
|
}),
|
|
552
|
-
|
|
553
|
-
dimColor: true,
|
|
554
|
-
children: "(the code goes into that page — not back here)"
|
|
555
|
-
})
|
|
425
|
+
":"
|
|
556
426
|
]
|
|
557
427
|
}),
|
|
558
|
-
/* @__PURE__ */ jsx(
|
|
559
|
-
|
|
428
|
+
/* @__PURE__ */ jsx(Box, {
|
|
429
|
+
marginTop: 1,
|
|
430
|
+
children: /* @__PURE__ */ jsx(TextInput, {
|
|
431
|
+
placeholder: "123456",
|
|
432
|
+
onSubmit: (value) => {
|
|
433
|
+
const code = value.trim();
|
|
434
|
+
if (code)
|
|
435
|
+
verify(phase.email, code);
|
|
436
|
+
}
|
|
437
|
+
})
|
|
438
|
+
}),
|
|
439
|
+
phase.error && /* @__PURE__ */ jsx(Box, {
|
|
440
|
+
marginTop: 1,
|
|
441
|
+
children: /* @__PURE__ */ jsxs(Text, {
|
|
442
|
+
color: color.err,
|
|
443
|
+
children: [
|
|
444
|
+
phase.error,
|
|
445
|
+
" — try again."
|
|
446
|
+
]
|
|
447
|
+
})
|
|
560
448
|
})
|
|
561
449
|
]
|
|
562
450
|
}),
|
|
451
|
+
phase.t === "verifying" && /* @__PURE__ */ jsx(Spinner, {
|
|
452
|
+
label: "Verifying…"
|
|
453
|
+
}),
|
|
563
454
|
phase.t === "done" && /* @__PURE__ */ jsxs(StatusMessage, {
|
|
564
455
|
variant: "success",
|
|
565
456
|
children: [
|
|
@@ -576,9 +467,7 @@ function Login({
|
|
|
576
467
|
});
|
|
577
468
|
}
|
|
578
469
|
var init_Login = __esm(() => {
|
|
579
|
-
init_loginFlow();
|
|
580
470
|
init_auth();
|
|
581
|
-
init_openBrowser();
|
|
582
471
|
init_theme();
|
|
583
472
|
});
|
|
584
473
|
|
|
@@ -603,24 +492,6 @@ var exports_init = {};
|
|
|
603
492
|
__export(exports_init, {
|
|
604
493
|
initCommand: () => initCommand
|
|
605
494
|
});
|
|
606
|
-
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
607
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
608
|
-
import { dirname as dirname3 } from "node:path";
|
|
609
|
-
function readManagedSettings() {
|
|
610
|
-
try {
|
|
611
|
-
const p = managedSettingsPath();
|
|
612
|
-
if (existsSync3(p))
|
|
613
|
-
return JSON.parse(readFileSync3(p, "utf8"));
|
|
614
|
-
} catch {}
|
|
615
|
-
return null;
|
|
616
|
-
}
|
|
617
|
-
function writeAllowlistWithSudo() {
|
|
618
|
-
const path = managedSettingsPath();
|
|
619
|
-
const json2 = JSON.stringify(mergeManagedSettings(readManagedSettings()), null, 2);
|
|
620
|
-
const script = `mkdir -p "${dirname3(path)}" && cat > "${path}"`;
|
|
621
|
-
const r = spawnSync2("sudo", ["sh", "-c", script], { input: json2, stdio: ["pipe", "inherit", "inherit"] });
|
|
622
|
-
return r.status === 0;
|
|
623
|
-
}
|
|
624
495
|
async function initCommand(rest, flags = {}, env = process.env) {
|
|
625
496
|
const dev = flags.dev === true;
|
|
626
497
|
console.log(`vibegroup setup
|
|
@@ -642,13 +513,13 @@ async function initCommand(rest, flags = {}, env = process.env) {
|
|
|
642
513
|
}
|
|
643
514
|
if (dev) {
|
|
644
515
|
console.log("• Dev mode: skipping the channel allowlist (launch with `vibegroup claude --dev`).");
|
|
645
|
-
} else if (
|
|
516
|
+
} else if (channelAllowlisted()) {
|
|
646
517
|
console.log("✓ Channel already enabled.");
|
|
647
518
|
} else {
|
|
648
519
|
console.log(`
|
|
649
520
|
• Enabling the vibegroup channel needs admin once (Claude Code gates channel plugins).`);
|
|
650
|
-
console.log(
|
|
651
|
-
if (!
|
|
521
|
+
console.log(" Enter your password if prompted.");
|
|
522
|
+
if (!enableChannelWithSudo()) {
|
|
652
523
|
console.error(" Could not write managed settings. Retry, or run `vibegroup init --dev` to skip it and use `vibegroup claude --dev`.");
|
|
653
524
|
return 1;
|
|
654
525
|
}
|
|
@@ -671,7 +542,7 @@ async function initCommand(rest, flags = {}, env = process.env) {
|
|
|
671
542
|
}
|
|
672
543
|
var init_init = __esm(() => {
|
|
673
544
|
init_auth();
|
|
674
|
-
|
|
545
|
+
init_channel();
|
|
675
546
|
init_pluginInstall();
|
|
676
547
|
init_login();
|
|
677
548
|
});
|
|
@@ -836,7 +707,7 @@ function extractFlag(args, name) {
|
|
|
836
707
|
}
|
|
837
708
|
return { value, rest };
|
|
838
709
|
}
|
|
839
|
-
function claudeCommand(args, env = process.env, launcher) {
|
|
710
|
+
function claudeCommand(args, env = process.env, launcher, channel = realChannelGate) {
|
|
840
711
|
if (!isLoggedIn(readAuth(env))) {
|
|
841
712
|
console.error("Not logged in — run `vibegroup login` first.");
|
|
842
713
|
return 1;
|
|
@@ -848,6 +719,13 @@ function claudeCommand(args, env = process.env, launcher) {
|
|
|
848
719
|
console.error("No team selected — pass `--team <slug>` (the team whose room you want to join).");
|
|
849
720
|
return 1;
|
|
850
721
|
}
|
|
722
|
+
if (!dangerously && !channel.allowlisted()) {
|
|
723
|
+
console.log("Enabling the vibegroup channel — one-time, needs admin. Enter your password if prompted.");
|
|
724
|
+
if (!channel.enable()) {
|
|
725
|
+
console.error("Could not enable the channel. Retry, or launch with `vibegroup claude --dev` (no admin needed).");
|
|
726
|
+
return 1;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
851
729
|
const vg = {
|
|
852
730
|
VIBEGROUP_TEAM: team,
|
|
853
731
|
VIBEGROUP_ROOM: room && room.length > 0 ? room : "general",
|
|
@@ -855,10 +733,12 @@ function claudeCommand(args, env = process.env, launcher) {
|
|
|
855
733
|
};
|
|
856
734
|
return launchClaude({ extraArgs: extra, dangerously, env: vg }, launcher);
|
|
857
735
|
}
|
|
858
|
-
var DEFAULT_API_BASE = "https://api.vibegroup.sh";
|
|
736
|
+
var DEFAULT_API_BASE = "https://api.vibegroup.sh", realChannelGate;
|
|
859
737
|
var init_claudeCmd = __esm(() => {
|
|
860
738
|
init_claudeLaunch();
|
|
861
739
|
init_auth();
|
|
740
|
+
init_channel();
|
|
741
|
+
realChannelGate = { allowlisted: channelAllowlisted, enable: enableChannelWithSudo };
|
|
862
742
|
});
|
|
863
743
|
|
|
864
744
|
// src/cli.ts
|
package/package.json
CHANGED
package/plugin/commands/init.md
CHANGED
|
@@ -1,37 +1,29 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Set up vibegroup —
|
|
2
|
+
description: Set up vibegroup — sign in with an email code, create/join a team, launch a channel session.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
Onboard the user to vibegroup. **
|
|
5
|
+
Onboard the user to vibegroup. Sign-in is **passwordless email code** — you send a code, the user reads it from their inbox and pastes it to you. Do NOT run `vibegroup login` (that's the human/terminal path). Use AskUserQuestion for choices; keep steps terse.
|
|
6
6
|
|
|
7
7
|
## Phase 0 — Already authenticated?
|
|
8
8
|
|
|
9
9
|
Read `~/.claude/vibegroup/auth.json` (honor `CLAUDE_CONFIG_DIR`). If it has a non-empty `accessToken`, you're signed in — skip to Phase 2.
|
|
10
10
|
|
|
11
|
-
## Phase 1 —
|
|
12
|
-
|
|
13
|
-
vibegroup publishes its agent-auth instructions at `https://api.vibegroup.sh/auth.md`. Follow the **`service_auth` (user-claimed)** path — no anonymous:
|
|
11
|
+
## Phase 1 — Sign in (email code, no browser)
|
|
14
12
|
|
|
15
13
|
1. Ask the user which email to sign in with (e.g. `terry@cruz.pe`).
|
|
16
|
-
2.
|
|
14
|
+
2. Send a code to that email:
|
|
17
15
|
```bash
|
|
18
|
-
curl -s -X POST https://api.vibegroup.sh/
|
|
19
|
-
-H 'content-type: application/json'
|
|
20
|
-
-d '{"type":"service_auth","login_hint":"<email>"}'
|
|
16
|
+
curl -s -X POST https://api.vibegroup.sh/auth/email/start \
|
|
17
|
+
-H 'content-type: application/json' -d '{"email":"<email>"}'
|
|
21
18
|
```
|
|
22
|
-
|
|
23
|
-
3.
|
|
24
|
-
|
|
25
|
-
> <verification_uri>
|
|
26
|
-
> (the code goes into that page — not back to me)
|
|
27
|
-
4. Poll until they confirm — wait `interval` seconds (default 5) between calls, give up after ~10 min:
|
|
19
|
+
(`{"ok":true}` means it's on the way.)
|
|
20
|
+
3. Tell the user, in one message: *"I sent a 6-digit code to `<email>` — paste it here when it arrives."* Then wait for them to give you the code.
|
|
21
|
+
4. Verify the code (this proves they own the email + signs them in):
|
|
28
22
|
```bash
|
|
29
|
-
curl -s -X POST https://api.vibegroup.sh/
|
|
30
|
-
-H 'content-type: application/
|
|
31
|
-
--data-urlencode 'grant_type=urn:workos:agent-auth:grant-type:claim' \
|
|
32
|
-
--data-urlencode 'claim_token=<claim_token>'
|
|
23
|
+
curl -s -X POST https://api.vibegroup.sh/auth/email/verify \
|
|
24
|
+
-H 'content-type: application/json' -d '{"email":"<email>","code":"<code>"}'
|
|
33
25
|
```
|
|
34
|
-
`{"error":"
|
|
26
|
+
`{"error":"invalid_code"}` → tell them it was wrong/expired and ask again (re-send with step 2 if needed). On success the response has `access_token` + `identity_assertion`.
|
|
35
27
|
5. Persist the session so the channel + CLI pick it up — write `~/.claude/vibegroup/auth.json` (file mode 600):
|
|
36
28
|
```json
|
|
37
29
|
{ "accessToken": "<access_token>", "identityAssertion": "<identity_assertion>", "user": { "id": "<email>", "email": "<email>" } }
|
|
@@ -45,10 +37,11 @@ The session is cached now, so the `vibegroup` CLI works. AskUserQuestion: **"Cre
|
|
|
45
37
|
|
|
46
38
|
## Phase 3 — Launch a channel session
|
|
47
39
|
|
|
48
|
-
|
|
40
|
+
The vibegroup channel is admin-gated (it lives in root-owned managed settings), so **you can't enable it from here** — it's a terminal action, like login. Tell the user to run, in their terminal:
|
|
49
41
|
```bash
|
|
50
42
|
vibegroup claude --team <slug>
|
|
51
43
|
```
|
|
44
|
+
The **first run enables the channel** (one `sudo` password prompt), then launches Claude Code in the room. Every later run just launches. If they'd rather not use admin, `vibegroup claude --team <slug> --dev` skips it via the development-channel flag (no password).
|
|
52
45
|
|
|
53
46
|
## Done
|
|
54
47
|
|