@bivy/bivy 0.7.0-staging.107 → 0.7.0-staging.109
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/bivy.mjs +36 -4
- package/dist/github-device-auth.js +16 -0
- package/dist/repo-workspace.js +19 -0
- package/dist/secrets.js +6 -2
- package/dist/server.js +96 -2
- package/package.json +1 -1
package/bin/bivy.mjs
CHANGED
|
@@ -3343,9 +3343,11 @@ async function cmdSetup(args = []) {
|
|
|
3343
3343
|
console.log(` ${agentReady ? c.green("✓") : c.yellow("!")} runtime ${agentReady ? `${setupAgent?.label || "Pi"} available` : "not installed — run 'bivy agents:install'"}`);
|
|
3344
3344
|
console.log(` ${modelReady ? (setupAgent?.needsBivyModel ? c.green("✓") : c.dim("○")) : c.yellow("!")} model ${modelReady ? (setupAgent?.needsBivyModel ? "credential configured" : "agent-managed — verified by the first task") : "not configured — run 'bivy login'"}`);
|
|
3345
3345
|
console.log(` ${c.dim("○")} repository chosen from the directory where you start Bivy`);
|
|
3346
|
+
const ghReady = githubConnected(finalConfig);
|
|
3347
|
+
console.log(` ${ghReady ? c.green("✓") : c.dim("○")} GitHub ${ghReady ? "connected — your repos will list in the app" : c.dim("not connected — 'bivy github:connect' to list repos (optional)")}`);
|
|
3346
3348
|
console.log(` ${agentReady && modelReady ? c.green("✓") : c.yellow("!")} first task ${agentReady && modelReady ? "ready to try" : "blocked by the stage above"}`);
|
|
3347
3349
|
console.log(` ${fs.existsSync(relayConfigPath) ? c.green("✓") : c.yellow("!")} remote ${fs.existsSync(relayConfigPath) ? "configured" : "not configured — run 'bivy relay:setup'"}\n`);
|
|
3348
|
-
printFirstRunSteps(modelReady);
|
|
3350
|
+
printFirstRunSteps(modelReady, finalConfig);
|
|
3349
3351
|
await finishSetupRemote(finalConfig, setupSession);
|
|
3350
3352
|
}
|
|
3351
3353
|
|
|
@@ -3433,10 +3435,29 @@ async function openRemoteApp(config, { setupSession = null, open = true } = {})
|
|
|
3433
3435
|
return { relay, remoteBase, accountUrl, pairedUrl, openUrl };
|
|
3434
3436
|
}
|
|
3435
3437
|
|
|
3436
|
-
|
|
3438
|
+
// Whether GitHub is connected for repo listing/cloning. Bivy's own connect flow
|
|
3439
|
+
// (`bivy github:connect`, or the app's Connect button) writes BIVY_GITHUB_TOKEN
|
|
3440
|
+
// — usually a `secret://` vault reference — into cli.json's env; an explicit env
|
|
3441
|
+
// token counts too. A `gh auth login` session also works at runtime (the node
|
|
3442
|
+
// falls back to `gh auth token`), but that can't be known without shelling out,
|
|
3443
|
+
// so it's treated as "not connected here" — the hint is optional either way.
|
|
3444
|
+
function githubConnected(config = null) {
|
|
3445
|
+
const token = String(
|
|
3446
|
+
config?.env?.BIVY_GITHUB_TOKEN || process.env.BIVY_GITHUB_TOKEN || process.env.GITHUB_TOKEN || "",
|
|
3447
|
+
).trim();
|
|
3448
|
+
return Boolean(token);
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
function printFirstRunSteps(modelReady = false, config = null) {
|
|
3437
3452
|
console.log(" Run your first task:");
|
|
3438
|
-
|
|
3439
|
-
console.log(` ${
|
|
3453
|
+
let n = 0;
|
|
3454
|
+
if (!modelReady) console.log(` ${++n}. Model access: ${c.cyan("bivy login")} ${c.dim("(for Pi; other agents use their own login)")}`);
|
|
3455
|
+
// GitHub is optional — "No repo" sessions work without it — so this only shows
|
|
3456
|
+
// when nothing is connected yet, and never blocks the flow.
|
|
3457
|
+
if (!githubConnected(config)) {
|
|
3458
|
+
console.log(` ${++n}. GitHub ${c.dim("(optional)")}: ${c.cyan("bivy github:connect")} ${c.dim("— lets the app list your repos; required for private ones")}`);
|
|
3459
|
+
}
|
|
3460
|
+
console.log(` ${++n}. Start chatting: ${c.cyan("bivy")}`);
|
|
3440
3461
|
console.log(` Starter task: ${c.cyan('bivy exec "explain this repository and identify one low-risk improvement"')}\n`);
|
|
3441
3462
|
}
|
|
3442
3463
|
|
|
@@ -3614,6 +3635,17 @@ async function cmdDoctor(args = []) {
|
|
|
3614
3635
|
console.log(c.bold("\n Bivy doctor\n"));
|
|
3615
3636
|
console.log(` ${mark(hasSupportedNode())} Node ${process.version}${hasSupportedNode() ? "" : c.dim(" (needs >= 22.19.0)")}`);
|
|
3616
3637
|
console.log(` ${mark(commandExists("git"), true)} git${commandExists("git") ? "" : c.dim(" (recommended for repo-backed sessions)")}`);
|
|
3638
|
+
// GitHub is optional (a "No repo" session needs none), so this only ever warns.
|
|
3639
|
+
// `gh` is NOT required — it's a token fallback; the primary path is Bivy's own
|
|
3640
|
+
// 'bivy github:connect' (or the app's Connect button). We surface gh only as an
|
|
3641
|
+
// available shortcut when it's installed but nothing is connected yet.
|
|
3642
|
+
const ghConnected = githubConnected(config);
|
|
3643
|
+
const ghHint = ghConnected
|
|
3644
|
+
? c.green("connected")
|
|
3645
|
+
: commandExists("gh")
|
|
3646
|
+
? c.dim("not connected — 'bivy github:connect' (or 'gh auth login')")
|
|
3647
|
+
: c.dim("not connected — 'bivy github:connect' to list/clone private repos");
|
|
3648
|
+
console.log(` ${mark(ghConnected, true)} GitHub ${ghHint}`);
|
|
3617
3649
|
console.log(` ${mark(reachable)} node ${reachable ? c.green("reachable") : c.dim("not reachable — 'bivy start'")} at ${url(config)}`);
|
|
3618
3650
|
console.log(` ${mark(/running/.test(serviceStatusLine()), true)} ${serviceStatusLine()}`);
|
|
3619
3651
|
const defaultAgent = String(config.env?.BIVY_RUNTIME || runtimes?.current?.id || "pi");
|
|
@@ -67,6 +67,22 @@ export function interpretTokenResponse(data) {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
70
|
+
/**
|
|
71
|
+
* A SINGLE access-token poll (no internal waiting) — for a caller that drives
|
|
72
|
+
* its own cadence. The web-driven connect flow uses this: the node holds the
|
|
73
|
+
* device code and the browser polls it on GitHub's interval, so the node never
|
|
74
|
+
* blocks a request thread in a poll loop. `pollForAccessToken` is the CLI's
|
|
75
|
+
* self-driving loop built on the same interpretation.
|
|
76
|
+
*/
|
|
77
|
+
export async function pollAccessTokenOnce(clientId, deviceCode) {
|
|
78
|
+
const res = await fetch(ACCESS_TOKEN_URL, {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers: { accept: "application/json", "content-type": "application/json" },
|
|
81
|
+
body: JSON.stringify({ client_id: clientId, device_code: deviceCode, grant_type: "urn:ietf:params:oauth:grant-type:device_code" }),
|
|
82
|
+
});
|
|
83
|
+
const data = (await res.json().catch(() => ({})));
|
|
84
|
+
return interpretTokenResponse(data);
|
|
85
|
+
}
|
|
70
86
|
/** Step 2: poll until the user authorizes (or the code expires). */
|
|
71
87
|
export async function pollForAccessToken(clientId, device, signal) {
|
|
72
88
|
let intervalMs = device.intervalSec * 1000;
|
package/dist/repo-workspace.js
CHANGED
|
@@ -100,6 +100,25 @@ export async function resolveGitHubToken(env = process.env) {
|
|
|
100
100
|
return undefined;
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Whether the GitHub CLI (`gh`) is installed on this machine — used only to
|
|
105
|
+
* shade the "no GitHub token" message: when `gh` is present but `gh auth token`
|
|
106
|
+
* gave us nothing, the user is one `gh auth login` away, so the picker can say
|
|
107
|
+
* so. It never means `gh` is REQUIRED — `bivy github:connect` is the primary
|
|
108
|
+
* path and needs no CLI (see resolveGitHubToken). Mirrors the `command -v`
|
|
109
|
+
* probe in secrets.ts.
|
|
110
|
+
*/
|
|
111
|
+
export async function ghCliInstalled() {
|
|
112
|
+
const which = process.platform === "win32" ? "where" : "command";
|
|
113
|
+
const args = process.platform === "win32" ? ["gh"] : ["-v", "gh"];
|
|
114
|
+
try {
|
|
115
|
+
await exec(which, args, process.platform === "win32" ? {} : { shell: true });
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
103
122
|
/**
|
|
104
123
|
* Refresh the remote-tracking refs so a session branches off the CURRENT state
|
|
105
124
|
* of `origin`, not whatever the local checkout last saw. Best-effort: an offline
|
package/dist/secrets.js
CHANGED
|
@@ -183,8 +183,12 @@ export class SecretVault {
|
|
|
183
183
|
if (fs.existsSync(this.keyFile))
|
|
184
184
|
checks.push({ name: "local key file permissions", ok: modeIsPrivate(this.keyFile), detail: this.keyFile });
|
|
185
185
|
checks.push({ name: "1Password CLI", ok: await commandExists("op"), detail: "required for op:// references" });
|
|
186
|
-
checks.push({ name: "GitHub CLI", ok: await commandExists("gh"), detail: "
|
|
187
|
-
|
|
186
|
+
checks.push({ name: "GitHub CLI (optional)", ok: await commandExists("gh"), detail: "optional token shortcut; Bivy connects GitHub itself via `bivy github:connect`" });
|
|
187
|
+
// 1Password and the GitHub CLI are optional shortcuts — a missing one must
|
|
188
|
+
// not fail the vault's health (Bivy connects GitHub itself; op is only for
|
|
189
|
+
// op:// refs). Match on a stable prefix so renaming the label can't silently
|
|
190
|
+
// turn either back into a hard failure.
|
|
191
|
+
return { ok: checks.every((c) => c.ok || c.name.startsWith("1Password CLI") || c.name.startsWith("GitHub CLI")), checks };
|
|
188
192
|
}
|
|
189
193
|
key() {
|
|
190
194
|
try {
|
package/dist/server.js
CHANGED
|
@@ -60,7 +60,7 @@ import { checkDiskAdmission } from "./harness/disk-admission.js";
|
|
|
60
60
|
import { sandboxTier, setConfiguredSandboxTier, normalizeSandboxTier } from "./harness/sandbox.js";
|
|
61
61
|
import { setConfiguredAutoAttachToolImages } from "./harness/tool-image-attachments.js";
|
|
62
62
|
import { injectMcpProxyForSession, injectBivyToolsForSession } from "./harness/mcp-inject.js";
|
|
63
|
-
import { parseRepo, inferGitHubRepoFromWorkspace, isSharedCloneRoot, resolveGitHubToken, cloneOrUpdateRepo, resolveDefaultBaseRef, resolveBranchBaseRef, resolveAdoptBaseRef, fetchOrigin } from "./repo-workspace.js";
|
|
63
|
+
import { parseRepo, inferGitHubRepoFromWorkspace, isSharedCloneRoot, resolveGitHubToken, ghCliInstalled, cloneOrUpdateRepo, resolveDefaultBaseRef, resolveBranchBaseRef, resolveAdoptBaseRef, fetchOrigin } from "./repo-workspace.js";
|
|
64
64
|
import { configureGitAuth, writeGitCredentialEndpoint } from "./git-auth.js";
|
|
65
65
|
import { GitHubTaskPoller, resolveGitHubTaskConfig, buildTaskPrompt, buildResumePrompt, buildInteractiveResumePrompt, DEFAULT_ISSUE_INSTRUCTIONS, parseBivyDirectives, commitAll, pushBranch, mergeBaseIntoBranch, completeMerge, abortMerge, findOpenPullRequestForBranch, findPullRequestsForBranch, findMergedPullRequestForBranch, issueBranchName, getPullRequest, commentIssue, listOpenLabelledIssues, selectActionableIssues, getIssue, getIssueCommentBody, addLabel, removeLabel, announcePickup, } from "./github-tasks.js";
|
|
66
66
|
import { buildLinearTaskPrompt, getLinearIssue, linearBranchName } from "./linear-tasks.js";
|
|
@@ -84,6 +84,7 @@ import { ReplicationService } from "./session/replication-service.js";
|
|
|
84
84
|
import { createSessionNewDedupe } from "./session/session-new-dedupe.js";
|
|
85
85
|
import { evaluateForkPrereqs, blockingForkPrereqs, missingForkPrereqs } from "./session/fork-prereqs.js";
|
|
86
86
|
import { SecretVault, resolveSecret } from "./secrets.js";
|
|
87
|
+
import { deviceFlowClientId, requestDeviceCode, pollAccessTokenOnce, REPO_CONNECT_SCOPE } from "./github-device-auth.js";
|
|
87
88
|
import { InstallationTokenCache, createAppJwt, resolveInstallationId } from "./github-app-auth.js";
|
|
88
89
|
import { loadGitHubAppConfigs, orderAppsForOwner, listGitHubApps, removeGitHubApp, upsertGitHubApp, privateKeyIdFor, } from "./github-apps.js";
|
|
89
90
|
import { buildAppManifest, convertManifest, renderManifestForm } from "./github-app-manifest.js";
|
|
@@ -2824,6 +2825,15 @@ const RELAY_COMMANDS = {
|
|
|
2824
2825
|
async "repos.list"() {
|
|
2825
2826
|
relay?.sendEvent({ type: "repos.list", ...(await listAccessibleRepos()) });
|
|
2826
2827
|
},
|
|
2828
|
+
// Web-driven "Connect GitHub" for the repo picker: start the node's device
|
|
2829
|
+
// flow, then poll it on GitHub's interval. Both answer with the same
|
|
2830
|
+
// `github.connect.status` event so the client has one shape to handle.
|
|
2831
|
+
async "github.connect.start"() {
|
|
2832
|
+
relay?.sendEvent({ type: "github.connect.status", ...(await startGithubConnect()) });
|
|
2833
|
+
},
|
|
2834
|
+
async "github.connect.poll"() {
|
|
2835
|
+
relay?.sendEvent({ type: "github.connect.status", ...(await pollGithubConnect()) });
|
|
2836
|
+
},
|
|
2827
2837
|
// Branches for the repo the composer's repo pill just picked, so the branch
|
|
2828
2838
|
// pill next to it can offer a specific remote branch to clone/base a new
|
|
2829
2839
|
// session from instead of always the repo's default. See listRepoBranches.
|
|
@@ -10134,7 +10144,7 @@ async function listAccessibleRepos() {
|
|
|
10134
10144
|
try {
|
|
10135
10145
|
const token = await resolveGitHubToken();
|
|
10136
10146
|
if (!token)
|
|
10137
|
-
return { authed: false, repos: [] };
|
|
10147
|
+
return { authed: false, repos: [], reason: (await ghCliInstalled()) ? "gh-unauthed" : "no-token" };
|
|
10138
10148
|
const ghRes = await fetch("https://api.github.com/user/repos?sort=updated&per_page=100&affiliation=owner,collaborator,organization_member", {
|
|
10139
10149
|
headers: { authorization: `Bearer ${token}`, accept: "application/vnd.github+json", "user-agent": "bivy" },
|
|
10140
10150
|
});
|
|
@@ -10159,6 +10169,83 @@ async function listAccessibleRepos() {
|
|
|
10159
10169
|
return { authed: false, repos: [], error: error instanceof Error ? error.message : String(error) };
|
|
10160
10170
|
}
|
|
10161
10171
|
}
|
|
10172
|
+
let pendingGithubConnect = null;
|
|
10173
|
+
async function startGithubConnect() {
|
|
10174
|
+
const clientId = deviceFlowClientId();
|
|
10175
|
+
if (!clientId)
|
|
10176
|
+
return { status: "unconfigured" };
|
|
10177
|
+
try {
|
|
10178
|
+
const device = await requestDeviceCode(clientId, REPO_CONNECT_SCOPE);
|
|
10179
|
+
pendingGithubConnect = { clientId, device, expiresAt: Date.now() + device.expiresInSec * 1000 };
|
|
10180
|
+
return {
|
|
10181
|
+
status: "waiting",
|
|
10182
|
+
userCode: device.userCode,
|
|
10183
|
+
verificationUri: device.verificationUri,
|
|
10184
|
+
intervalMs: device.intervalSec * 1000,
|
|
10185
|
+
expiresInMs: device.expiresInSec * 1000,
|
|
10186
|
+
};
|
|
10187
|
+
}
|
|
10188
|
+
catch (error) {
|
|
10189
|
+
return { status: "error", error: error instanceof Error ? error.message : String(error) };
|
|
10190
|
+
}
|
|
10191
|
+
}
|
|
10192
|
+
async function pollGithubConnect() {
|
|
10193
|
+
const pending = pendingGithubConnect;
|
|
10194
|
+
if (!pending)
|
|
10195
|
+
return { status: "idle" };
|
|
10196
|
+
if (Date.now() > pending.expiresAt) {
|
|
10197
|
+
pendingGithubConnect = null;
|
|
10198
|
+
return { status: "expired" };
|
|
10199
|
+
}
|
|
10200
|
+
let poll;
|
|
10201
|
+
try {
|
|
10202
|
+
poll = await pollAccessTokenOnce(pending.clientId, pending.device.deviceCode);
|
|
10203
|
+
}
|
|
10204
|
+
catch (error) {
|
|
10205
|
+
// A transient network blip mid-flow — keep the code alive and let the client
|
|
10206
|
+
// poll again rather than discarding a device code the user may have authorized.
|
|
10207
|
+
return { status: "error", error: error instanceof Error ? error.message : String(error) };
|
|
10208
|
+
}
|
|
10209
|
+
switch (poll.status) {
|
|
10210
|
+
case "ok":
|
|
10211
|
+
pendingGithubConnect = null;
|
|
10212
|
+
persistConnectedGithubToken(poll.token);
|
|
10213
|
+
return { status: "connected" };
|
|
10214
|
+
case "slow_down":
|
|
10215
|
+
// GitHub says we're polling too fast — widen the interval it hands the
|
|
10216
|
+
// browser so the next poll backs off (and doesn't burn the device code).
|
|
10217
|
+
pending.device.intervalSec = poll.intervalSec ?? pending.device.intervalSec + 5;
|
|
10218
|
+
// falls through — same "keep waiting" answer, just a larger interval.
|
|
10219
|
+
case "pending":
|
|
10220
|
+
return {
|
|
10221
|
+
status: "waiting",
|
|
10222
|
+
userCode: pending.device.userCode,
|
|
10223
|
+
verificationUri: pending.device.verificationUri,
|
|
10224
|
+
intervalMs: pending.device.intervalSec * 1000,
|
|
10225
|
+
expiresInMs: Math.max(0, pending.expiresAt - Date.now()),
|
|
10226
|
+
};
|
|
10227
|
+
case "denied":
|
|
10228
|
+
pendingGithubConnect = null;
|
|
10229
|
+
return { status: "denied" };
|
|
10230
|
+
case "expired":
|
|
10231
|
+
pendingGithubConnect = null;
|
|
10232
|
+
return { status: "expired" };
|
|
10233
|
+
default:
|
|
10234
|
+
pendingGithubConnect = null;
|
|
10235
|
+
return { status: "error", error: poll.error };
|
|
10236
|
+
}
|
|
10237
|
+
}
|
|
10238
|
+
// Store the repo-scoped token exactly like `bivy github:connect`: the raw token
|
|
10239
|
+
// in the node's secret vault, and only a `secret://` reference in cli.json. But
|
|
10240
|
+
// ALSO update the LIVE process env so resolveGitHubToken() picks it up without a
|
|
10241
|
+
// restart (the Tier-1 caveat), and drop the repo-list cache so the very next
|
|
10242
|
+
// list is authed.
|
|
10243
|
+
function persistConnectedGithubToken(token) {
|
|
10244
|
+
new SecretVault(appDir).setLocal("github.repo-token", token, "GitHub repo/work-queue token");
|
|
10245
|
+
saveCliEnv({ BIVY_GITHUB_TOKEN: "secret://github.repo-token" });
|
|
10246
|
+
process.env.BIVY_GITHUB_TOKEN = "secret://github.repo-token";
|
|
10247
|
+
invalidateGithubListingCaches();
|
|
10248
|
+
}
|
|
10162
10249
|
// Fetch a repo's remote branch names with a given token (or none, for a public
|
|
10163
10250
|
// repo). One GitHub call; returns null on a non-OK response so the caller can
|
|
10164
10251
|
// decide whether to retry with a different token.
|
|
@@ -10582,6 +10669,13 @@ app.get("/github/app/manifest/callback", async (req, res, next) => {
|
|
|
10582
10669
|
app.get("/api/repos", async (_req, res) => {
|
|
10583
10670
|
res.json(await listAccessibleRepos());
|
|
10584
10671
|
});
|
|
10672
|
+
// Direct-transport (local PWA) equivalents of the github.connect.* commands.
|
|
10673
|
+
app.post("/api/github/connect/start", async (_req, res) => {
|
|
10674
|
+
res.json(await startGithubConnect());
|
|
10675
|
+
});
|
|
10676
|
+
app.get("/api/github/connect/poll", async (_req, res) => {
|
|
10677
|
+
res.json(await pollGithubConnect());
|
|
10678
|
+
});
|
|
10585
10679
|
app.get("/api/repos/branches", async (req, res) => {
|
|
10586
10680
|
res.json(await listRepoBranches(String(req.query.repo || "").trim()));
|
|
10587
10681
|
});
|
package/package.json
CHANGED