@birdybeep/cli 0.3.0 → 0.6.0
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/bin.cjs +774 -191
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-U4EIHC5C.js → chunk-BY5MNQE3.js} +762 -160
- package/dist/chunk-BY5MNQE3.js.map +1 -0
- package/dist/index.cjs +774 -191
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +1 -1
- package/package.json +7 -7
- package/dist/chunk-U4EIHC5C.js.map +0 -1
|
@@ -78,11 +78,17 @@ function isUnknownFlag(token, allowed) {
|
|
|
78
78
|
function renderRootHelp(version, commands) {
|
|
79
79
|
const width = Math.max(...commands.map((c) => c.name.length));
|
|
80
80
|
const lines = commands.map((c) => ` ${c.name.padEnd(width)} ${c.summary}`);
|
|
81
|
+
const featured = commands.filter((c) => c.gettingStarted !== void 0);
|
|
81
82
|
return [
|
|
82
83
|
`birdybeep ${version} \u2014 stream coding-agent lifecycle events to BirdyBeep.`,
|
|
83
84
|
"",
|
|
84
85
|
"Usage:",
|
|
85
86
|
" birdybeep <command> [options]",
|
|
87
|
+
...featured.length > 0 ? [
|
|
88
|
+
"",
|
|
89
|
+
"Getting started:",
|
|
90
|
+
...featured.map((c) => ` birdybeep ${c.name} ${c.gettingStarted ?? ""}`)
|
|
91
|
+
] : [],
|
|
86
92
|
"",
|
|
87
93
|
"Commands:",
|
|
88
94
|
...lines,
|
|
@@ -164,7 +170,11 @@ async function dispatch(argv, deps) {
|
|
|
164
170
|
if (rest.length === 0 || flags.help && command === void 0) {
|
|
165
171
|
io.emit(renderRootHelp(deps.version, deps.commands), {
|
|
166
172
|
version: deps.version,
|
|
167
|
-
commands: deps.commands.map((c) => ({
|
|
173
|
+
commands: deps.commands.map((c) => ({
|
|
174
|
+
name: c.name,
|
|
175
|
+
summary: c.summary,
|
|
176
|
+
...c.gettingStarted !== void 0 ? { gettingStarted: c.gettingStarted } : {}
|
|
177
|
+
}))
|
|
168
178
|
});
|
|
169
179
|
return EXIT.OK;
|
|
170
180
|
}
|
|
@@ -217,7 +227,7 @@ async function dispatch(argv, deps) {
|
|
|
217
227
|
}
|
|
218
228
|
|
|
219
229
|
// src/version.ts
|
|
220
|
-
var CLI_VERSION = "0.
|
|
230
|
+
var CLI_VERSION = "0.6.0".length > 0 ? "0.6.0" : "0.0.0";
|
|
221
231
|
|
|
222
232
|
// src/commands/agent.ts
|
|
223
233
|
import { claudeCodeAdapter } from "@birdybeep/claude-code";
|
|
@@ -225,6 +235,200 @@ import { codexAdapter } from "@birdybeep/codex";
|
|
|
225
235
|
import { copilotAdapter } from "@birdybeep/copilot";
|
|
226
236
|
import { cursorAdapter } from "@birdybeep/cursor";
|
|
227
237
|
import { opencodeAdapter } from "@birdybeep/opencode";
|
|
238
|
+
|
|
239
|
+
// src/diagnostics.ts
|
|
240
|
+
import { existsSync, readFileSync } from "fs";
|
|
241
|
+
import { homedir } from "os";
|
|
242
|
+
import {
|
|
243
|
+
getMachineIdentity,
|
|
244
|
+
getToken,
|
|
245
|
+
LocalEventQueue,
|
|
246
|
+
readFilteredActivity,
|
|
247
|
+
readObservedBuilds,
|
|
248
|
+
readUnpairedNotice
|
|
249
|
+
} from "@birdybeep/agent-core";
|
|
250
|
+
import {
|
|
251
|
+
BIRDYBEEP_HOOK_EVENTS as CLAUDE_HOOK_EVENTS,
|
|
252
|
+
claudeSettingsPath,
|
|
253
|
+
isBirdyBeepEntry as isClaudeEntry
|
|
254
|
+
} from "@birdybeep/claude-code";
|
|
255
|
+
import {
|
|
256
|
+
BIRDYBEEP_HOOK_EVENTS as CURSOR_HOOK_EVENTS,
|
|
257
|
+
cursorHooksPath,
|
|
258
|
+
detectCursor,
|
|
259
|
+
isBirdyBeepEntry as isCursorEntry
|
|
260
|
+
} from "@birdybeep/cursor";
|
|
261
|
+
async function gatherIntegrations(adapters) {
|
|
262
|
+
return Promise.all(
|
|
263
|
+
adapters.map(async (a) => ({
|
|
264
|
+
harness: a.id,
|
|
265
|
+
displayName: a.displayName,
|
|
266
|
+
status: await a.status()
|
|
267
|
+
}))
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
async function isPaired(tokenOptions = {}) {
|
|
271
|
+
return await getToken(tokenOptions) !== null;
|
|
272
|
+
}
|
|
273
|
+
function localQueueDepth() {
|
|
274
|
+
return new LocalEventQueue().size();
|
|
275
|
+
}
|
|
276
|
+
function localQueueOverflowDrops() {
|
|
277
|
+
return new LocalEventQueue().overflowDropCount();
|
|
278
|
+
}
|
|
279
|
+
function unpairedActivity() {
|
|
280
|
+
return readUnpairedNotice();
|
|
281
|
+
}
|
|
282
|
+
function describeUnpairedActivity(notice) {
|
|
283
|
+
const since = new Date(notice.firstAt).toISOString();
|
|
284
|
+
const from = notice.harnesses.length > 0 ? ` from ${notice.harnesses.join(", ")}` : "";
|
|
285
|
+
return `${notice.count} event(s)${from} fired since ${since} and were NOT sent \u2014 this machine is not paired.`;
|
|
286
|
+
}
|
|
287
|
+
function asRecord(value) {
|
|
288
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
289
|
+
}
|
|
290
|
+
function birdyBeepHookCount(path, events, isBirdyBeepEntry) {
|
|
291
|
+
if (!existsSync(path)) return 0;
|
|
292
|
+
let parsed;
|
|
293
|
+
try {
|
|
294
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
295
|
+
} catch {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
const hooks = asRecord(asRecord(parsed)["hooks"]);
|
|
299
|
+
let present = 0;
|
|
300
|
+
for (const event of events) {
|
|
301
|
+
const entries = hooks[event];
|
|
302
|
+
if (Array.isArray(entries) && entries.some(isBirdyBeepEntry)) present += 1;
|
|
303
|
+
}
|
|
304
|
+
return present;
|
|
305
|
+
}
|
|
306
|
+
async function cursorBridgeOnly(opts = {}) {
|
|
307
|
+
const home = opts.home ?? homedir();
|
|
308
|
+
const detection = await (opts.detect ?? (() => detectCursor({ home })))();
|
|
309
|
+
if (!detection.detected) return false;
|
|
310
|
+
const claude = birdyBeepHookCount(claudeSettingsPath(home), CLAUDE_HOOK_EVENTS, isClaudeEntry);
|
|
311
|
+
if (claude === null || claude === 0) return false;
|
|
312
|
+
return birdyBeepHookCount(cursorHooksPath(home), CURSOR_HOOK_EVENTS, isCursorEntry) === 0;
|
|
313
|
+
}
|
|
314
|
+
function filteredActivity() {
|
|
315
|
+
return readFilteredActivity();
|
|
316
|
+
}
|
|
317
|
+
function describeFilteredActivity(activity) {
|
|
318
|
+
const types = Object.entries(activity.byType).sort(([, a], [, b]) => b - a).map(([type, n]) => `${type} \xD7${n}`).join(", ");
|
|
319
|
+
const since = new Date(activity.firstAt).toISOString();
|
|
320
|
+
return `${activity.count} local-only event(s) since ${since}${types ? ` (${types})` : ""} \u2014 hooks are firing; these types never beep, so they are not sent.`;
|
|
321
|
+
}
|
|
322
|
+
function machineIdentity() {
|
|
323
|
+
return getMachineIdentity();
|
|
324
|
+
}
|
|
325
|
+
var CONFIGURED_STATUSES = /* @__PURE__ */ new Set([
|
|
326
|
+
"installed",
|
|
327
|
+
"needs_trust",
|
|
328
|
+
"needs_restart"
|
|
329
|
+
]);
|
|
330
|
+
function gradeSurfaces(surfaces, status, observation) {
|
|
331
|
+
const builds = Object.values(observation?.builds ?? {});
|
|
332
|
+
const configured = CONFIGURED_STATUSES.has(status);
|
|
333
|
+
const claimedByKind = /* @__PURE__ */ new Map();
|
|
334
|
+
for (const s of surfaces) {
|
|
335
|
+
if (s.version === void 0) continue;
|
|
336
|
+
const versions = claimedByKind.get(s.kind) ?? /* @__PURE__ */ new Set();
|
|
337
|
+
versions.add(s.version);
|
|
338
|
+
claimedByKind.set(s.kind, versions);
|
|
339
|
+
}
|
|
340
|
+
const graded = surfaces.map((surface) => {
|
|
341
|
+
const exact = builds.filter(
|
|
342
|
+
(b) => b.surface === surface.kind && b.version === surface.version && surface.version !== void 0
|
|
343
|
+
);
|
|
344
|
+
let soleOfKind = [];
|
|
345
|
+
if (surface.version === void 0) {
|
|
346
|
+
const sameKindVersionless = surfaces.filter(
|
|
347
|
+
(s) => s.version === void 0 && s.kind === surface.kind
|
|
348
|
+
);
|
|
349
|
+
const unclaimed = builds.filter(
|
|
350
|
+
(b) => b.surface === surface.kind && !(claimedByKind.get(surface.kind)?.has(b.version) ?? false)
|
|
351
|
+
);
|
|
352
|
+
if (unclaimed.length === 1 && sameKindVersionless.length === 1) soleOfKind = unclaimed;
|
|
353
|
+
}
|
|
354
|
+
const unattributed = surface.version === void 0 ? [] : builds.filter((b) => b.surface === "unknown" && b.version === surface.version);
|
|
355
|
+
const sharesVersion = surface.version !== void 0 && surfaces.some((s) => s !== surface && s.version === surface.version);
|
|
356
|
+
const ambiguous = unattributed.length > 0 && sharesVersion;
|
|
357
|
+
const matched = [...exact, ...soleOfKind, ...ambiguous ? [] : unattributed];
|
|
358
|
+
const events = matched.reduce((total, b) => total + b.count, 0);
|
|
359
|
+
const lastAt = matched.reduce(
|
|
360
|
+
(latest, b) => latest === void 0 || b.lastAt > latest ? b.lastAt : latest,
|
|
361
|
+
void 0
|
|
362
|
+
);
|
|
363
|
+
const observedVersion = surface.version === void 0 ? soleOfKind[0]?.version : void 0;
|
|
364
|
+
return {
|
|
365
|
+
surface,
|
|
366
|
+
events,
|
|
367
|
+
ambiguous,
|
|
368
|
+
...lastAt !== void 0 ? { lastAt } : {},
|
|
369
|
+
...observedVersion !== void 0 ? { observedVersion } : {}
|
|
370
|
+
};
|
|
371
|
+
});
|
|
372
|
+
const anyActive = graded.some((g) => g.events > 0 && g.surface.shadowed !== true);
|
|
373
|
+
return graded.map(({ ambiguous, ...g }) => ({
|
|
374
|
+
...g,
|
|
375
|
+
coverage: !configured ? "uncovered" : g.events > 0 ? "active" : anyActive && g.surface.shadowed !== true && !ambiguous ? "uncovered" : "wired"
|
|
376
|
+
}));
|
|
377
|
+
}
|
|
378
|
+
async function gatherSurfaces(adapters, options = {}) {
|
|
379
|
+
const observed = readObservedBuilds(options.observedBuilds ?? {});
|
|
380
|
+
return Promise.all(
|
|
381
|
+
adapters.map(async (adapter) => {
|
|
382
|
+
const observation = observed[adapter.id];
|
|
383
|
+
const base4 = {
|
|
384
|
+
harness: adapter.id,
|
|
385
|
+
displayName: adapter.displayName,
|
|
386
|
+
unversionedEvents: observation?.unversioned ?? 0
|
|
387
|
+
};
|
|
388
|
+
try {
|
|
389
|
+
const [detection, status] = await Promise.all([adapter.detect(), adapter.status()]);
|
|
390
|
+
return {
|
|
391
|
+
...base4,
|
|
392
|
+
status,
|
|
393
|
+
surfaces: detection.detected ? gradeSurfaces(detection.surfaces ?? [], status, observation) : []
|
|
394
|
+
};
|
|
395
|
+
} catch {
|
|
396
|
+
return { ...base4, status: "unknown", surfaces: [] };
|
|
397
|
+
}
|
|
398
|
+
})
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
function describeSurface(state) {
|
|
402
|
+
const version = state.surface.version ?? state.observedVersion;
|
|
403
|
+
return version !== void 0 ? `${state.surface.label} ${version}` : state.surface.label;
|
|
404
|
+
}
|
|
405
|
+
function describeSurfaceCoverage(state, group) {
|
|
406
|
+
if (state.coverage === "active") {
|
|
407
|
+
const last = state.lastAt !== void 0 ? `, last ${new Date(state.lastAt).toISOString()}` : "";
|
|
408
|
+
return `covered \u2014 ${state.events} event(s) from this build${last}`;
|
|
409
|
+
}
|
|
410
|
+
if (state.coverage === "wired") {
|
|
411
|
+
return state.surface.shadowed === true ? `${group.displayName}'s hooks are installed and this build shares them, but another install comes first on PATH \u2014 it only runs if that order changes` : `${group.displayName}'s hooks are installed and this build shares them; nothing has fired from it yet`;
|
|
412
|
+
}
|
|
413
|
+
if (!CONFIGURED_STATUSES.has(group.status)) {
|
|
414
|
+
return `not covered \u2014 ${group.displayName} carries no BirdyBeep hooks, so this build cannot beep`;
|
|
415
|
+
}
|
|
416
|
+
const active = group.surfaces.filter((s) => s.coverage === "active").map(describeSurface);
|
|
417
|
+
const delivering = active.join(", ");
|
|
418
|
+
const verb = active.length === 1 ? "is" : "are";
|
|
419
|
+
return `not covered \u2014 nothing has ever fired from this build, while ${delivering} ${verb} delivering through the same config`;
|
|
420
|
+
}
|
|
421
|
+
function installTarget(harness) {
|
|
422
|
+
return harness === "claude_code" ? "claude" : harness;
|
|
423
|
+
}
|
|
424
|
+
function surfaceRemedy(state, group) {
|
|
425
|
+
if (state.coverage !== "uncovered") return void 0;
|
|
426
|
+
if (!CONFIGURED_STATUSES.has(group.status)) return void 0;
|
|
427
|
+
const install = `\`birdybeep agent install ${installTarget(group.harness)}\``;
|
|
428
|
+
return state.surface.kind === "desktop" ? `Run a turn in ${state.surface.label}. If it stays uncovered, that build cannot run the hook command: a desktop app spawns its engine with your LOGIN shell's PATH, not an interactive shell's, so a bare command is invisible to it. Re-run ${install} from a shell where \`birdybeep\` resolves \u2014 it rewrites the entry with absolute paths that need no PATH at all.` : `Run a turn in ${state.surface.label}. If it stays uncovered, re-run ${install} from a shell where \`birdybeep\` resolves, then check that ${state.surface.enginePath} is the build you are actually running.`;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/commands/agent.ts
|
|
228
432
|
var DEFAULT_ADAPTERS = [
|
|
229
433
|
claudeCodeAdapter,
|
|
230
434
|
codexAdapter,
|
|
@@ -253,7 +457,10 @@ function selectAdapters(target, adapters) {
|
|
|
253
457
|
if (id === void 0) return "unknown";
|
|
254
458
|
return adapters.filter((a) => a.id === id);
|
|
255
459
|
}
|
|
256
|
-
|
|
460
|
+
function installTarget2(harness) {
|
|
461
|
+
return harness === "claude_code" ? "claude" : harness;
|
|
462
|
+
}
|
|
463
|
+
async function installSelected(adapters, ctx, tokenOptions) {
|
|
257
464
|
const target = ctx.args[0] ?? "all";
|
|
258
465
|
const selected = selectAdapters(target, adapters);
|
|
259
466
|
if (selected === "unknown") {
|
|
@@ -280,8 +487,9 @@ async function installSelected(adapters, ctx) {
|
|
|
280
487
|
requiredActions: result.requiredActions
|
|
281
488
|
});
|
|
282
489
|
}
|
|
490
|
+
const paired = await isPaired(tokenOptions);
|
|
283
491
|
if (ctx.flags.json) {
|
|
284
|
-
ctx.io.result({ target, results: outcomes });
|
|
492
|
+
ctx.io.result({ target, paired, results: outcomes });
|
|
285
493
|
return EXIT.OK;
|
|
286
494
|
}
|
|
287
495
|
if (outcomes.length === 0 || outcomes.every((o) => !o.detected)) {
|
|
@@ -289,13 +497,20 @@ async function installSelected(adapters, ctx) {
|
|
|
289
497
|
}
|
|
290
498
|
for (const o of outcomes) {
|
|
291
499
|
if (!o.detected) {
|
|
292
|
-
ctx.io.line(
|
|
500
|
+
ctx.io.line(
|
|
501
|
+
`\u2013 ${o.displayName}: not detected (skipped) \u2014 install it, then run \`birdybeep agent install ${installTarget2(o.harness)}\``
|
|
502
|
+
);
|
|
293
503
|
continue;
|
|
294
504
|
}
|
|
295
505
|
const changed = (o.changedFiles ?? []).length > 0 ? o.changedFiles.join(", ") : "no changes";
|
|
296
506
|
ctx.io.line(`\u2713 ${o.displayName}: ${o.status} (${changed})`);
|
|
297
507
|
for (const action of o.requiredActions ?? []) ctx.io.line(` \u2192 ${action}`);
|
|
298
508
|
}
|
|
509
|
+
if (!paired) {
|
|
510
|
+
ctx.io.line(
|
|
511
|
+
"\u26A0 This machine is not paired, so nothing these hooks produce can reach you. Run `birdybeep setup` \u2014 it pairs, wires up every agent, and sends a test Beep."
|
|
512
|
+
);
|
|
513
|
+
}
|
|
299
514
|
return EXIT.OK;
|
|
300
515
|
}
|
|
301
516
|
async function uninstallSelected(adapters, ctx) {
|
|
@@ -334,6 +549,7 @@ async function uninstallSelected(adapters, ctx) {
|
|
|
334
549
|
}
|
|
335
550
|
function createAgentCommand(deps = {}) {
|
|
336
551
|
const adapters = deps.adapters ?? DEFAULT_ADAPTERS;
|
|
552
|
+
const tokenOptions = deps.tokenOptions ?? {};
|
|
337
553
|
return {
|
|
338
554
|
name: "agent",
|
|
339
555
|
summary: "Install or uninstall harness adapters",
|
|
@@ -343,7 +559,7 @@ function createAgentCommand(deps = {}) {
|
|
|
343
559
|
name: "install",
|
|
344
560
|
summary: "Install adapters (all | claude | codex | opencode | cursor | copilot)",
|
|
345
561
|
usage: "birdybeep agent install [all|claude|codex|opencode|cursor|copilot]",
|
|
346
|
-
run: (ctx) => installSelected(adapters, ctx)
|
|
562
|
+
run: (ctx) => installSelected(adapters, ctx, tokenOptions)
|
|
347
563
|
},
|
|
348
564
|
{
|
|
349
565
|
name: "uninstall",
|
|
@@ -357,7 +573,8 @@ function createAgentCommand(deps = {}) {
|
|
|
357
573
|
|
|
358
574
|
// src/commands/doctor.ts
|
|
359
575
|
import {
|
|
360
|
-
createSender as defaultCreateSender
|
|
576
|
+
createSender as defaultCreateSender,
|
|
577
|
+
DEFAULT_QUEUE_MAX_ENTRIES as QUEUE_CAP
|
|
361
578
|
} from "@birdybeep/agent-core";
|
|
362
579
|
import { claudeCodeAdapter as claudeCodeAdapter2 } from "@birdybeep/claude-code";
|
|
363
580
|
import { codexAdapter as codexAdapter2 } from "@birdybeep/codex";
|
|
@@ -366,7 +583,7 @@ import { cursorAdapter as cursorAdapter2 } from "@birdybeep/cursor";
|
|
|
366
583
|
import { opencodeAdapter as opencodeAdapter2 } from "@birdybeep/opencode";
|
|
367
584
|
|
|
368
585
|
// src/config.ts
|
|
369
|
-
import { mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
|
|
586
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
370
587
|
import { join } from "path";
|
|
371
588
|
import { birdyBeepConfigDir as birdyBeepConfigDir2 } from "@birdybeep/agent-core";
|
|
372
589
|
var DEFAULT_API_URL = "https://api.birdybeep.com";
|
|
@@ -376,7 +593,7 @@ function cliConfigPath() {
|
|
|
376
593
|
}
|
|
377
594
|
function readCliConfig() {
|
|
378
595
|
try {
|
|
379
|
-
const parsed = JSON.parse(
|
|
596
|
+
const parsed = JSON.parse(readFileSync2(cliConfigPath(), "utf8"));
|
|
380
597
|
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
381
598
|
} catch {
|
|
382
599
|
return {};
|
|
@@ -405,31 +622,6 @@ function resolveRegistryUrl() {
|
|
|
405
622
|
return DEFAULT_REGISTRY_URL;
|
|
406
623
|
}
|
|
407
624
|
|
|
408
|
-
// src/diagnostics.ts
|
|
409
|
-
import {
|
|
410
|
-
getMachineIdentity,
|
|
411
|
-
getToken,
|
|
412
|
-
LocalEventQueue
|
|
413
|
-
} from "@birdybeep/agent-core";
|
|
414
|
-
async function gatherIntegrations(adapters) {
|
|
415
|
-
return Promise.all(
|
|
416
|
-
adapters.map(async (a) => ({
|
|
417
|
-
harness: a.id,
|
|
418
|
-
displayName: a.displayName,
|
|
419
|
-
status: await a.status()
|
|
420
|
-
}))
|
|
421
|
-
);
|
|
422
|
-
}
|
|
423
|
-
async function isPaired(tokenOptions = {}) {
|
|
424
|
-
return await getToken(tokenOptions) !== null;
|
|
425
|
-
}
|
|
426
|
-
function localQueueDepth() {
|
|
427
|
-
return new LocalEventQueue().size();
|
|
428
|
-
}
|
|
429
|
-
function machineIdentity() {
|
|
430
|
-
return getMachineIdentity();
|
|
431
|
-
}
|
|
432
|
-
|
|
433
625
|
// src/commands/doctor.ts
|
|
434
626
|
var DEFAULT_ADAPTERS2 = [
|
|
435
627
|
claudeCodeAdapter2,
|
|
@@ -472,6 +664,32 @@ function createDoctorCommand(deps = {}) {
|
|
|
472
664
|
remedy: "Run `birdybeep pair` to pair this machine."
|
|
473
665
|
}
|
|
474
666
|
);
|
|
667
|
+
const unpaired = unpairedActivity();
|
|
668
|
+
if (unpaired !== null) {
|
|
669
|
+
checks.push({
|
|
670
|
+
name: "Events lost while unpaired",
|
|
671
|
+
ok: false,
|
|
672
|
+
detail: describeUnpairedActivity(unpaired),
|
|
673
|
+
remedy: "Run `birdybeep pair`. Events that fired before pairing are gone \u2014 a first pairing does not replay them."
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
if (await cursorBridgeOnly(deps.detectCursor ? { detect: deps.detectCursor } : {})) {
|
|
677
|
+
checks.push({
|
|
678
|
+
name: "Approval beeps from Cursor",
|
|
679
|
+
ok: false,
|
|
680
|
+
detail: "Cursor is running your agent through the Claude Code hooks \u2014 that is why Cursor events arrive without a Cursor install. Its bridge drops Notification and PermissionRequest, so approvals never reach you.",
|
|
681
|
+
remedy: "Run `birdybeep agent install cursor` to get approval beeps from Cursor's own shell and MCP permission prompts. Keeping both installed is safe \u2014 duplicate events are collapsed."
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
const filtered = filteredActivity();
|
|
685
|
+
if (filtered !== null) {
|
|
686
|
+
checks.push({
|
|
687
|
+
name: "Local-only events (never notifiable)",
|
|
688
|
+
ok: true,
|
|
689
|
+
detail: describeFilteredActivity(filtered)
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
const surfaceGroups = await gatherSurfaces(adapters, deps.surfaceOptions ?? {});
|
|
475
693
|
for (const adapter of adapters) {
|
|
476
694
|
const result = await adapter.doctor();
|
|
477
695
|
for (const c of result.checks) {
|
|
@@ -482,14 +700,26 @@ function createDoctorCommand(deps = {}) {
|
|
|
482
700
|
...c.remedy !== void 0 ? { remedy: c.remedy } : {}
|
|
483
701
|
});
|
|
484
702
|
}
|
|
703
|
+
const group = surfaceGroups.find((g) => g.harness === adapter.id);
|
|
704
|
+
if (group === void 0) continue;
|
|
705
|
+
for (const state of group.surfaces) {
|
|
706
|
+
const remedy = surfaceRemedy(state, group);
|
|
707
|
+
checks.push({
|
|
708
|
+
name: `${adapter.displayName}: ${describeSurface(state)}`,
|
|
709
|
+
ok: state.coverage !== "uncovered",
|
|
710
|
+
detail: describeSurfaceCoverage(state, group),
|
|
711
|
+
...remedy !== void 0 ? { remedy } : {}
|
|
712
|
+
});
|
|
713
|
+
}
|
|
485
714
|
}
|
|
486
715
|
const depthBefore = localQueueDepth();
|
|
487
716
|
const drain = await makeSender(apiUrl).drainNow();
|
|
488
717
|
const depthAfter = localQueueDepth();
|
|
718
|
+
const overflowDropped = localQueueOverflowDrops();
|
|
489
719
|
checks.push({
|
|
490
720
|
name: "Local queue",
|
|
491
721
|
ok: true,
|
|
492
|
-
detail: `${depthBefore} queued \u2192 ${drain.delivered} delivered, ${depthAfter} remaining`
|
|
722
|
+
detail: `${depthBefore} queued \u2192 ${drain.delivered} delivered, ${depthAfter} remaining` + (overflowDropped > 0 ? `; ${overflowDropped} dropped by the ${QUEUE_CAP} entry cap` : "")
|
|
493
723
|
});
|
|
494
724
|
const reachable = await probeNetwork(apiUrl);
|
|
495
725
|
checks.push(
|
|
@@ -505,7 +735,10 @@ function createDoctorCommand(deps = {}) {
|
|
|
505
735
|
ctx.io.result({
|
|
506
736
|
ok,
|
|
507
737
|
checks,
|
|
508
|
-
|
|
738
|
+
surfaces: surfaceGroups,
|
|
739
|
+
queue: { depthBefore, delivered: drain.delivered, depthAfter, overflowDropped },
|
|
740
|
+
...unpaired !== null ? { unpairedActivity: unpaired } : {},
|
|
741
|
+
...filtered !== null ? { filteredActivity: filtered } : {}
|
|
509
742
|
});
|
|
510
743
|
} else {
|
|
511
744
|
for (const c of checks) {
|
|
@@ -529,14 +762,15 @@ import {
|
|
|
529
762
|
createSender as defaultCreateSender2,
|
|
530
763
|
resolveOnPath
|
|
531
764
|
} from "@birdybeep/agent-core";
|
|
532
|
-
import { runClaudeHook } from "@birdybeep/claude-code";
|
|
533
|
-
import { runCodexHook } from "@birdybeep/codex";
|
|
765
|
+
import { isClaudeCodeHookPayload, runClaudeHook } from "@birdybeep/claude-code";
|
|
766
|
+
import { isCodexHookPayload, runCodexHook } from "@birdybeep/codex";
|
|
534
767
|
import {
|
|
535
768
|
isCopilotHookEventName,
|
|
769
|
+
isCopilotHookPayload,
|
|
536
770
|
runCopilotHook
|
|
537
771
|
} from "@birdybeep/copilot";
|
|
538
|
-
import { runCursorHook } from "@birdybeep/cursor";
|
|
539
|
-
import { runOpenCodeHook } from "@birdybeep/opencode";
|
|
772
|
+
import { isCursorHookEventName, isCursorHookPayload, runCursorHook } from "@birdybeep/cursor";
|
|
773
|
+
import { isOpenCodeEventPayload, runOpenCodeHook } from "@birdybeep/opencode";
|
|
540
774
|
var RUNNERS = {
|
|
541
775
|
claude: runClaudeHook,
|
|
542
776
|
codex: runCodexHook,
|
|
@@ -568,12 +802,43 @@ function withTimeout(promise, ms, fallback) {
|
|
|
568
802
|
function isHarnessName(value) {
|
|
569
803
|
return value === "claude" || value === "codex" || value === "opencode" || value === "cursor" || value === "copilot";
|
|
570
804
|
}
|
|
805
|
+
function resolveHookHarness(harness, payload) {
|
|
806
|
+
return harness === "claude" && isCursorHookPayload(payload) ? "cursor" : harness;
|
|
807
|
+
}
|
|
808
|
+
function recognizesPayload(harness, payload) {
|
|
809
|
+
switch (harness) {
|
|
810
|
+
case "claude":
|
|
811
|
+
return isClaudeCodeHookPayload(payload);
|
|
812
|
+
case "cursor":
|
|
813
|
+
return isCursorHookEventName(asRecord2(payload)["hook_event_name"]);
|
|
814
|
+
case "codex":
|
|
815
|
+
return isCodexHookPayload(payload);
|
|
816
|
+
case "opencode":
|
|
817
|
+
return isOpenCodeEventPayload(payload);
|
|
818
|
+
case "copilot":
|
|
819
|
+
return isCopilotHookPayload(payload);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
function asRecord2(value) {
|
|
823
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
824
|
+
}
|
|
825
|
+
function describeDiscriminator(payload) {
|
|
826
|
+
const record = asRecord2(payload);
|
|
827
|
+
for (const field of ["hook_event_name", "type"]) {
|
|
828
|
+
const value = record[field];
|
|
829
|
+
if (typeof value !== "string") continue;
|
|
830
|
+
const capped = value.length > 64 ? `${value.slice(0, 63)}\u2026` : value;
|
|
831
|
+
return `${field} ${JSON.stringify(capped)}`;
|
|
832
|
+
}
|
|
833
|
+
return "the payload";
|
|
834
|
+
}
|
|
571
835
|
function runHookCommand(harness, payload, sender, copilotEventName) {
|
|
572
|
-
|
|
836
|
+
const handler = resolveHookHarness(harness, payload);
|
|
837
|
+
if (handler === "copilot") {
|
|
573
838
|
if (copilotEventName === void 0) return Promise.resolve({ outcome: "skipped" });
|
|
574
839
|
return runCopilotHook(copilotEventName, payload, { sender });
|
|
575
840
|
}
|
|
576
|
-
return RUNNERS[
|
|
841
|
+
return RUNNERS[handler](payload, { sender });
|
|
577
842
|
}
|
|
578
843
|
function readStdinDefault() {
|
|
579
844
|
return new Promise((resolve) => {
|
|
@@ -660,10 +925,16 @@ function createHookCommand(deps = {}) {
|
|
|
660
925
|
return EXIT.OK;
|
|
661
926
|
}
|
|
662
927
|
const copilotEventName = harness === "copilot" && isCopilotHookEventName(ctx.args[1]) ? ctx.args[1] : void 0;
|
|
663
|
-
|
|
928
|
+
if (harness === "copilot" && copilotEventName === void 0) {
|
|
929
|
+
ctx.io.errline(
|
|
930
|
+
`birdybeep hook copilot: second argument must be a Copilot hook event name, got ${JSON.stringify(ctx.args[1] ?? "(none)")} \u2014 nothing was sent.`
|
|
931
|
+
);
|
|
932
|
+
return EXIT.USAGE;
|
|
933
|
+
}
|
|
934
|
+
const read = await withTimeout(
|
|
664
935
|
readHookPayload(ctx.args, readStdin, harness === "copilot"),
|
|
665
936
|
stdinTimeoutMs,
|
|
666
|
-
|
|
937
|
+
null
|
|
667
938
|
);
|
|
668
939
|
const notifyStdinFile = process.env[NOTIFY_STDIN_FILE_ENV];
|
|
669
940
|
if (notifyStdinFile !== void 0 && dirname(notifyStdinFile) === tmpdir() && basename(notifyStdinFile).startsWith("birdybeep-notify-")) {
|
|
@@ -672,23 +943,58 @@ function createHookCommand(deps = {}) {
|
|
|
672
943
|
} catch {
|
|
673
944
|
}
|
|
674
945
|
}
|
|
946
|
+
const drop = (reason, detail) => {
|
|
947
|
+
ctx.io.result({ harness, outcome: "skipped", reason });
|
|
948
|
+
ctx.io.errline(`birdybeep hook ${harness}: ${detail} \u2014 nothing was sent.`);
|
|
949
|
+
return EXIT.ERROR;
|
|
950
|
+
};
|
|
951
|
+
if (read === null) {
|
|
952
|
+
return drop(
|
|
953
|
+
"stdin-timeout",
|
|
954
|
+
`timed out after ${stdinTimeoutMs}ms waiting for the payload on stdin`
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
const raw = read;
|
|
958
|
+
if (raw.trim().length === 0) {
|
|
959
|
+
return drop("empty-payload", "the payload was empty");
|
|
960
|
+
}
|
|
675
961
|
let payload;
|
|
676
962
|
try {
|
|
677
963
|
payload = JSON.parse(raw);
|
|
678
964
|
} catch {
|
|
679
|
-
|
|
680
|
-
|
|
965
|
+
return drop("invalid-json", `the ${raw.length}-byte payload is not valid JSON`);
|
|
966
|
+
}
|
|
967
|
+
const handler = resolveHookHarness(harness, payload);
|
|
968
|
+
const routedFrom = handler !== harness ? { routedFrom: harness } : {};
|
|
969
|
+
if (!recognizesPayload(handler, payload)) {
|
|
970
|
+
ctx.io.result({
|
|
971
|
+
harness: handler,
|
|
972
|
+
...routedFrom,
|
|
973
|
+
outcome: "skipped",
|
|
974
|
+
reason: "foreign-payload"
|
|
975
|
+
});
|
|
976
|
+
const article = handler === "opencode" ? "an" : "a";
|
|
977
|
+
ctx.io.errline(
|
|
978
|
+
`birdybeep hook ${harness}: ${describeDiscriminator(payload)} is not ${article} ${handler} hook event \u2014 nothing was sent. Check which tool is running this hook.`
|
|
979
|
+
);
|
|
980
|
+
return EXIT.ERROR;
|
|
681
981
|
}
|
|
682
982
|
const sender = makeSender(resolveApiUrl());
|
|
683
983
|
const result = await runHookCommand(harness, payload, sender, copilotEventName);
|
|
684
984
|
ctx.io.result({
|
|
685
|
-
harness,
|
|
985
|
+
harness: handler,
|
|
986
|
+
...routedFrom,
|
|
686
987
|
...copilotEventName !== void 0 ? { event: copilotEventName } : {},
|
|
687
988
|
outcome: result.outcome,
|
|
688
989
|
eventType: result.eventType,
|
|
689
990
|
...result.send?.decision ? { decision: result.send.decision } : {},
|
|
690
991
|
...result.send?.status !== void 0 ? { status: result.send.status } : {}
|
|
691
992
|
});
|
|
993
|
+
if (result.outcome === "unpaired") {
|
|
994
|
+
ctx.io.errline(
|
|
995
|
+
"birdybeep: this machine is not paired \u2014 the event was not sent and was not queued. Run `birdybeep pair` (or `birdybeep doctor` to see how many events this has cost)."
|
|
996
|
+
);
|
|
997
|
+
}
|
|
692
998
|
return EXIT.OK;
|
|
693
999
|
}
|
|
694
1000
|
};
|
|
@@ -748,9 +1054,12 @@ function createUnpairCommand(deps = {}) {
|
|
|
748
1054
|
// src/commands/pair.ts
|
|
749
1055
|
import { closeSync as closeSync2, openSync as openSync2 } from "fs";
|
|
750
1056
|
import {
|
|
1057
|
+
clearUnpairedNotice,
|
|
751
1058
|
deriveCodeChallengeS256,
|
|
752
1059
|
generateCodeVerifier,
|
|
753
|
-
getMachineIdentity as
|
|
1060
|
+
getMachineIdentity as getMachineIdentity3,
|
|
1061
|
+
getToken as getToken3,
|
|
1062
|
+
LocalEventQueue as LocalEventQueue2,
|
|
754
1063
|
setToken
|
|
755
1064
|
} from "@birdybeep/agent-core";
|
|
756
1065
|
import { renderUnicodeCompact } from "uqr";
|
|
@@ -829,6 +1138,280 @@ async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint,
|
|
|
829
1138
|
return { status: "error", code: code ?? "unknown", message, retryable: true };
|
|
830
1139
|
}
|
|
831
1140
|
|
|
1141
|
+
// src/commands/setup.ts
|
|
1142
|
+
import { claudeCodeAdapter as claudeCodeAdapter3 } from "@birdybeep/claude-code";
|
|
1143
|
+
import { codexAdapter as codexAdapter3 } from "@birdybeep/codex";
|
|
1144
|
+
import { copilotAdapter as copilotAdapter3 } from "@birdybeep/copilot";
|
|
1145
|
+
import { cursorAdapter as cursorAdapter3 } from "@birdybeep/cursor";
|
|
1146
|
+
import { opencodeAdapter as opencodeAdapter3 } from "@birdybeep/opencode";
|
|
1147
|
+
|
|
1148
|
+
// src/commands/test.ts
|
|
1149
|
+
import { randomUUID } from "crypto";
|
|
1150
|
+
import {
|
|
1151
|
+
createSender as defaultCreateSender3,
|
|
1152
|
+
getMachineIdentity as getMachineIdentity2,
|
|
1153
|
+
normalizeEvent
|
|
1154
|
+
} from "@birdybeep/agent-core";
|
|
1155
|
+
function buildTestEvent(opts = {}) {
|
|
1156
|
+
const machine = getMachineIdentity2();
|
|
1157
|
+
return normalizeEvent(
|
|
1158
|
+
{
|
|
1159
|
+
event_type: "test",
|
|
1160
|
+
status: "running",
|
|
1161
|
+
harness: "claude_code",
|
|
1162
|
+
// schema requires a harness; the "test" type distinguishes it
|
|
1163
|
+
// Unique per run: a repeat `birdybeep test` inside the backend's dedupe window must
|
|
1164
|
+
// still beep — a constant id made the second test silently "deduped" (9fh).
|
|
1165
|
+
source_session_id: `birdybeep-cli-test-${randomUUID()}`,
|
|
1166
|
+
machine: { label: machine.label, os: machine.os },
|
|
1167
|
+
workspace: { cwd: process.cwd() },
|
|
1168
|
+
title: "BirdyBeep test event",
|
|
1169
|
+
body: "If you can see this, your machine is wired up correctly.",
|
|
1170
|
+
metadata: { test: true }
|
|
1171
|
+
},
|
|
1172
|
+
opts
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
function createTestCommand(deps = {}) {
|
|
1176
|
+
const makeSender = deps.createSender ?? ((baseUrl) => defaultCreateSender3(
|
|
1177
|
+
deps.tokenOptions ? { baseUrl, tokenOptions: deps.tokenOptions } : { baseUrl }
|
|
1178
|
+
));
|
|
1179
|
+
return {
|
|
1180
|
+
name: "test",
|
|
1181
|
+
summary: "Send a test event end-to-end",
|
|
1182
|
+
usage: "birdybeep test [--json]",
|
|
1183
|
+
run: async (ctx) => {
|
|
1184
|
+
const event = buildTestEvent();
|
|
1185
|
+
const result = await makeSender(resolveApiUrl()).send(event);
|
|
1186
|
+
if (ctx.flags.json) {
|
|
1187
|
+
ctx.io.result({
|
|
1188
|
+
outcome: result.outcome,
|
|
1189
|
+
...result.status ? { status: result.status } : {},
|
|
1190
|
+
...result.decision ? { decision: result.decision } : {}
|
|
1191
|
+
});
|
|
1192
|
+
} else if (result.outcome === "delivered") {
|
|
1193
|
+
if (result.decision === "notified" || result.decision === void 0) {
|
|
1194
|
+
ctx.io.line("\u2713 Test event delivered \u2014 check your phone for a test Beep.");
|
|
1195
|
+
} else if (result.decision === "suppressed") {
|
|
1196
|
+
ctx.io.line(
|
|
1197
|
+
"\u26A0 The backend accepted the test event but suppressed the push \u2014 this machine or integration is probably muted. Check mutes in the app, or run `birdybeep doctor`."
|
|
1198
|
+
);
|
|
1199
|
+
} else if (result.decision === "deduped") {
|
|
1200
|
+
ctx.io.line(
|
|
1201
|
+
"\u26A0 The backend accepted the test event but folded it into a recent duplicate \u2014 wait ~30s and run `birdybeep test` again."
|
|
1202
|
+
);
|
|
1203
|
+
} else {
|
|
1204
|
+
ctx.io.line(
|
|
1205
|
+
`\u26A0 The backend accepted the test event but decided "${result.decision}" \u2014 no push was sent. Run \`birdybeep doctor\`.`
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
} else if (result.outcome === "unpaired") {
|
|
1209
|
+
ctx.io.line(
|
|
1210
|
+
"\u2717 NOT PAIRED \u2014 this machine has no BirdyBeep machine token, so nothing was sent (and nothing was queued). Run `birdybeep pair`."
|
|
1211
|
+
);
|
|
1212
|
+
} else if (result.outcome === "queued") {
|
|
1213
|
+
ctx.io.line("\u2022 Offline \u2014 test event queued; it will deliver when you reconnect.");
|
|
1214
|
+
} else {
|
|
1215
|
+
ctx.io.line("\u2717 Test event was rejected by the backend. Run `birdybeep doctor`.");
|
|
1216
|
+
}
|
|
1217
|
+
return result.outcome === "dropped" || result.outcome === "unpaired" ? EXIT.ERROR : EXIT.OK;
|
|
1218
|
+
}
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// src/commands/setup.ts
|
|
1223
|
+
var SETUP_ADAPTERS = [
|
|
1224
|
+
claudeCodeAdapter3,
|
|
1225
|
+
codexAdapter3,
|
|
1226
|
+
opencodeAdapter3,
|
|
1227
|
+
cursorAdapter3,
|
|
1228
|
+
copilotAdapter3
|
|
1229
|
+
];
|
|
1230
|
+
function failedSetupReport(error) {
|
|
1231
|
+
return {
|
|
1232
|
+
harnesses: [],
|
|
1233
|
+
counts: { installed: 0, needsYou: 0, notInstalled: 0, failed: 0 },
|
|
1234
|
+
error,
|
|
1235
|
+
ok: false
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
var PENDING_STATUSES = /* @__PURE__ */ new Set([
|
|
1239
|
+
"needs_trust",
|
|
1240
|
+
"needs_restart"
|
|
1241
|
+
]);
|
|
1242
|
+
async function installDetected(adapters) {
|
|
1243
|
+
const installs = [];
|
|
1244
|
+
for (const adapter of adapters) {
|
|
1245
|
+
try {
|
|
1246
|
+
const detection = await adapter.detect();
|
|
1247
|
+
if (!detection.detected) {
|
|
1248
|
+
installs.push({ adapter, detected: false });
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
installs.push({ adapter, detected: true, result: await adapter.install() });
|
|
1252
|
+
} catch (err) {
|
|
1253
|
+
installs.push({
|
|
1254
|
+
adapter,
|
|
1255
|
+
detected: true,
|
|
1256
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return installs;
|
|
1261
|
+
}
|
|
1262
|
+
function rowState(state, group, status) {
|
|
1263
|
+
if (status === "error" || group.status === "error") return "failed";
|
|
1264
|
+
if (status !== void 0 && PENDING_STATUSES.has(status)) return "needs you";
|
|
1265
|
+
if (state.coverage === "active") return "beeping";
|
|
1266
|
+
if (state.coverage === "wired") return "ready";
|
|
1267
|
+
return "not covered";
|
|
1268
|
+
}
|
|
1269
|
+
function buildHarnessReports(installs, groups) {
|
|
1270
|
+
return installs.map((install) => {
|
|
1271
|
+
const { adapter } = install;
|
|
1272
|
+
const base4 = {
|
|
1273
|
+
harness: adapter.id,
|
|
1274
|
+
displayName: adapter.displayName,
|
|
1275
|
+
detected: install.detected,
|
|
1276
|
+
...install.result !== void 0 ? {
|
|
1277
|
+
status: install.result.status,
|
|
1278
|
+
changedFiles: install.result.changedFiles,
|
|
1279
|
+
backupFiles: install.result.backupFiles
|
|
1280
|
+
} : {},
|
|
1281
|
+
...install.error !== void 0 ? { error: install.error } : {}
|
|
1282
|
+
};
|
|
1283
|
+
if (install.error !== void 0) {
|
|
1284
|
+
return {
|
|
1285
|
+
...base4,
|
|
1286
|
+
actions: [
|
|
1287
|
+
`${adapter.displayName} could not be set up: ${install.error}`,
|
|
1288
|
+
`Run \`birdybeep agent install ${installTarget2(adapter.id)}\` to retry it on its own.`
|
|
1289
|
+
],
|
|
1290
|
+
rows: [{ harness: adapter.id, displayName: adapter.displayName, state: "failed" }]
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
if (!install.detected) {
|
|
1294
|
+
return {
|
|
1295
|
+
...base4,
|
|
1296
|
+
actions: [],
|
|
1297
|
+
rows: [
|
|
1298
|
+
{
|
|
1299
|
+
harness: adapter.id,
|
|
1300
|
+
displayName: adapter.displayName,
|
|
1301
|
+
state: "not installed"
|
|
1302
|
+
}
|
|
1303
|
+
]
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
const group = groups.find((g) => g.harness === adapter.id);
|
|
1307
|
+
const status = install.result?.status;
|
|
1308
|
+
const surfaces = group?.surfaces ?? [];
|
|
1309
|
+
const rows = group === void 0 || surfaces.length === 0 ? [
|
|
1310
|
+
{
|
|
1311
|
+
harness: adapter.id,
|
|
1312
|
+
displayName: adapter.displayName,
|
|
1313
|
+
state: status !== void 0 && PENDING_STATUSES.has(status) ? "needs you" : "ready"
|
|
1314
|
+
}
|
|
1315
|
+
] : surfaces.map((state) => {
|
|
1316
|
+
const graded = rowState(state, group, status);
|
|
1317
|
+
const remedy = surfaceRemedy(state, group) ?? (graded === "not covered" ? `${adapter.displayName} carries no BirdyBeep hooks \u2014 re-run \`birdybeep agent install ${installTarget2(adapter.id)}\` from a shell where \`birdybeep\` resolves.` : void 0);
|
|
1318
|
+
return {
|
|
1319
|
+
harness: adapter.id,
|
|
1320
|
+
displayName: adapter.displayName,
|
|
1321
|
+
build: describeSurface(state),
|
|
1322
|
+
kind: state.surface.kind,
|
|
1323
|
+
state: graded,
|
|
1324
|
+
...remedy !== void 0 ? { remedy } : {}
|
|
1325
|
+
};
|
|
1326
|
+
});
|
|
1327
|
+
return { ...base4, actions: [...install.result?.requiredActions ?? []], rows };
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
function pad(text, width) {
|
|
1331
|
+
return text.length >= width ? text : text + " ".repeat(width - text.length);
|
|
1332
|
+
}
|
|
1333
|
+
var MARKS = {
|
|
1334
|
+
beeping: "\u2713",
|
|
1335
|
+
ready: "\u2713",
|
|
1336
|
+
"needs you": "!",
|
|
1337
|
+
"not covered": "\u2717",
|
|
1338
|
+
"not installed": "\u2013",
|
|
1339
|
+
failed: "\u2717"
|
|
1340
|
+
};
|
|
1341
|
+
function renderCoverageTable(reports) {
|
|
1342
|
+
const rows = reports.flatMap((r) => r.rows);
|
|
1343
|
+
const nameWidth = Math.max(7, ...rows.map((r) => r.displayName.length));
|
|
1344
|
+
const buildWidth = Math.max(5, ...rows.map((r) => (r.build ?? "\u2014").length));
|
|
1345
|
+
const lines = ["coverage", ` ${pad("harness", nameWidth)} ${pad("build", buildWidth)} state`];
|
|
1346
|
+
for (const report of reports) {
|
|
1347
|
+
for (const row of report.rows) {
|
|
1348
|
+
lines.push(
|
|
1349
|
+
`${MARKS[row.state]} ${pad(row.displayName, nameWidth)} ${pad(row.build ?? "\u2014", buildWidth)} ${row.state}`
|
|
1350
|
+
);
|
|
1351
|
+
if (row.remedy !== void 0) lines.push(` \u2192 ${row.remedy}`);
|
|
1352
|
+
}
|
|
1353
|
+
for (const action of report.actions) lines.push(` \u2192 ${action}`);
|
|
1354
|
+
}
|
|
1355
|
+
return lines;
|
|
1356
|
+
}
|
|
1357
|
+
function describeMissing(reports) {
|
|
1358
|
+
const missing = reports.filter((r) => !r.detected && r.error === void 0);
|
|
1359
|
+
if (missing.length === 0) return [];
|
|
1360
|
+
const names = missing.map((r) => r.displayName);
|
|
1361
|
+
if (missing.length === reports.length) {
|
|
1362
|
+
return [
|
|
1363
|
+
"No supported coding agent is installed on this machine, so there was nothing to wire up.",
|
|
1364
|
+
`Install one of ${names.join(", ")}, then run \`birdybeep setup\` again \u2014 pairing is already done, so it picks up from here.`
|
|
1365
|
+
];
|
|
1366
|
+
}
|
|
1367
|
+
return [
|
|
1368
|
+
`Not installed: ${names.join(", ")}. Install any of them, then run \`birdybeep setup\` again to wire it up.`
|
|
1369
|
+
];
|
|
1370
|
+
}
|
|
1371
|
+
async function runHarnessSetup(ctx, options, deps = {}) {
|
|
1372
|
+
const adapters = deps.adapters ?? [...SETUP_ADAPTERS];
|
|
1373
|
+
const installs = await installDetected(adapters);
|
|
1374
|
+
const groups = await gatherSurfaces(adapters, deps.surfaceOptions ?? {});
|
|
1375
|
+
const reports = buildHarnessReports(installs, groups);
|
|
1376
|
+
ctx.io.line("");
|
|
1377
|
+
for (const line of renderCoverageTable(reports)) ctx.io.line(line);
|
|
1378
|
+
const missing = describeMissing(reports);
|
|
1379
|
+
if (missing.length > 0) {
|
|
1380
|
+
ctx.io.line("");
|
|
1381
|
+
for (const line of missing) ctx.io.line(line);
|
|
1382
|
+
}
|
|
1383
|
+
const counts = {
|
|
1384
|
+
installed: reports.filter((r) => r.detected && r.error === void 0).length,
|
|
1385
|
+
needsYou: reports.filter((r) => r.rows.some((row) => row.state === "needs you")).length,
|
|
1386
|
+
notInstalled: reports.filter((r) => !r.detected && r.error === void 0).length,
|
|
1387
|
+
// A row that graded `failed` counts too: an adapter that returned status "error" never threw,
|
|
1388
|
+
// so counting only thrown errors would report a clean run over a harness that is broken.
|
|
1389
|
+
failed: reports.filter((r) => r.error !== void 0 || r.rows.some((x) => x.state === "failed")).length
|
|
1390
|
+
};
|
|
1391
|
+
let beep;
|
|
1392
|
+
let beepOk = true;
|
|
1393
|
+
if (options.sendTest) {
|
|
1394
|
+
ctx.io.line("");
|
|
1395
|
+
const command = createTestCommand({
|
|
1396
|
+
...deps.createSender !== void 0 ? { createSender: deps.createSender } : {},
|
|
1397
|
+
...deps.tokenOptions !== void 0 ? { tokenOptions: deps.tokenOptions } : {}
|
|
1398
|
+
});
|
|
1399
|
+
const beepIo = {
|
|
1400
|
+
...ctx.io,
|
|
1401
|
+
result: (value) => {
|
|
1402
|
+
beep = value;
|
|
1403
|
+
}
|
|
1404
|
+
};
|
|
1405
|
+
beepOk = await command.run?.({ args: [], flags: ctx.flags, io: beepIo }) === 0;
|
|
1406
|
+
}
|
|
1407
|
+
return {
|
|
1408
|
+
harnesses: reports,
|
|
1409
|
+
counts,
|
|
1410
|
+
...beep !== void 0 ? { beep } : {},
|
|
1411
|
+
ok: counts.failed === 0 && beepOk
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
|
|
832
1415
|
// src/commands/pair.ts
|
|
833
1416
|
var DEFAULT_POLL_INTERVAL_MS = 2e3;
|
|
834
1417
|
var HEARTBEAT_MS = 15e3;
|
|
@@ -836,11 +1419,15 @@ function renderQrMatrix(qrPayload) {
|
|
|
836
1419
|
return renderUnicodeCompact(qrPayload, { border: 2 });
|
|
837
1420
|
}
|
|
838
1421
|
function parsePairFlags(args) {
|
|
839
|
-
const flags = { yes: false };
|
|
1422
|
+
const flags = { yes: false, noInstall: false, noTest: false };
|
|
840
1423
|
for (let i = 0; i < args.length; i += 1) {
|
|
841
1424
|
const token = args[i] ?? "";
|
|
842
1425
|
if (token === "--yes" || token === "-y") {
|
|
843
1426
|
flags.yes = true;
|
|
1427
|
+
} else if (token === "--no-install") {
|
|
1428
|
+
flags.noInstall = true;
|
|
1429
|
+
} else if (token === "--no-test") {
|
|
1430
|
+
flags.noTest = true;
|
|
844
1431
|
} else if (token === "--expect-email" || token.startsWith("--expect-email=")) {
|
|
845
1432
|
const inline = token.startsWith("--expect-email=") ? token.slice("--expect-email=".length) : void 0;
|
|
846
1433
|
const value = inline ?? args[++i];
|
|
@@ -960,7 +1547,18 @@ async function promptForAnswer(question, on) {
|
|
|
960
1547
|
input.once?.("error", () => done(""));
|
|
961
1548
|
});
|
|
962
1549
|
}
|
|
963
|
-
function
|
|
1550
|
+
async function runSetupChain(ctx, deps, flags) {
|
|
1551
|
+
try {
|
|
1552
|
+
return await runHarnessSetup(ctx, { sendTest: !flags.noTest }, deps);
|
|
1553
|
+
} catch (err) {
|
|
1554
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1555
|
+
ctx.io.errline(
|
|
1556
|
+
`This machine is paired, but wiring up your coding agents failed: ${message}. Run \`birdybeep agent install all\` to do it on its own, then \`birdybeep doctor\`.`
|
|
1557
|
+
);
|
|
1558
|
+
return failedSetupReport(message);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
function createPairingCommand(verb, deps = {}) {
|
|
964
1562
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
965
1563
|
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
966
1564
|
const clock = deps.now ?? (() => Date.now());
|
|
@@ -973,9 +1571,10 @@ function createPairCommand(deps = {}) {
|
|
|
973
1571
|
return typeof pinned === "string" && pinned.trim().length > 0 ? pinned : void 0;
|
|
974
1572
|
});
|
|
975
1573
|
return {
|
|
976
|
-
name:
|
|
977
|
-
summary:
|
|
978
|
-
usage:
|
|
1574
|
+
name: verb.name,
|
|
1575
|
+
summary: verb.summary,
|
|
1576
|
+
usage: verb.usage,
|
|
1577
|
+
...verb.gettingStarted !== void 0 ? { gettingStarted: verb.gettingStarted } : {},
|
|
979
1578
|
options: [
|
|
980
1579
|
{
|
|
981
1580
|
flag: "--yes",
|
|
@@ -986,16 +1585,41 @@ function createPairCommand(deps = {}) {
|
|
|
986
1585
|
flag: "--expect-email",
|
|
987
1586
|
value: "<addr>",
|
|
988
1587
|
summary: "Only trust the pairing if this account approved it (else fail)"
|
|
1588
|
+
},
|
|
1589
|
+
{
|
|
1590
|
+
flag: "--no-install",
|
|
1591
|
+
summary: "Stop after pairing \u2014 don't detect or wire up any coding agent"
|
|
1592
|
+
},
|
|
1593
|
+
{
|
|
1594
|
+
flag: "--no-test",
|
|
1595
|
+
summary: "Don't send the test Beep at the end"
|
|
989
1596
|
}
|
|
990
1597
|
],
|
|
991
1598
|
run: async (ctx) => {
|
|
992
1599
|
const pairFlags = parsePairFlags(ctx.args);
|
|
993
1600
|
if (pairFlags.error !== void 0) {
|
|
994
|
-
ctx.io.errline(`birdybeep
|
|
1601
|
+
ctx.io.errline(`birdybeep ${verb.name}: ${pairFlags.error}.`);
|
|
995
1602
|
return EXIT.USAGE;
|
|
996
1603
|
}
|
|
1604
|
+
const chain = deps.setup === false || pairFlags.noInstall ? void 0 : deps.setup ?? {};
|
|
1605
|
+
const setupDeps = {
|
|
1606
|
+
...deps.tokenOptions !== void 0 ? { tokenOptions: deps.tokenOptions } : {},
|
|
1607
|
+
...chain
|
|
1608
|
+
};
|
|
1609
|
+
if (verb.skipWhenPaired && await getToken3(deps.tokenOptions ?? {}) !== null) {
|
|
1610
|
+
ctx.io.line(
|
|
1611
|
+
chain !== void 0 ? "\u2713 Already paired \u2014 checking which coding agents are wired up." : "\u2713 Already paired. Nothing else to do with --no-install."
|
|
1612
|
+
);
|
|
1613
|
+
const report2 = chain !== void 0 ? await runSetupChain(ctx, setupDeps, pairFlags) : void 0;
|
|
1614
|
+
ctx.io.result({
|
|
1615
|
+
paired: true,
|
|
1616
|
+
alreadyPaired: true,
|
|
1617
|
+
...report2 !== void 0 ? { setup: report2 } : {}
|
|
1618
|
+
});
|
|
1619
|
+
return report2 !== void 0 && !report2.ok ? EXIT.ERROR : EXIT.OK;
|
|
1620
|
+
}
|
|
997
1621
|
const apiUrl = resolveApiUrl();
|
|
998
|
-
const identity =
|
|
1622
|
+
const identity = getMachineIdentity3();
|
|
999
1623
|
const codeVerifier = generateCodeVerifier();
|
|
1000
1624
|
const codeChallenge = deriveCodeChallengeS256(codeVerifier);
|
|
1001
1625
|
const start = await pairStart(
|
|
@@ -1095,19 +1719,50 @@ function createPairCommand(deps = {}) {
|
|
|
1095
1719
|
}
|
|
1096
1720
|
await setToken(paired.machineToken, deps.tokenOptions ?? {});
|
|
1097
1721
|
writeCliConfig({ apiUrl });
|
|
1722
|
+
const discarded = new LocalEventQueue2().discardBefore(clock());
|
|
1723
|
+
clearUnpairedNotice();
|
|
1098
1724
|
const humanSuffix = approvedBy !== void 0 ? ` to ${approvedBy}` : "";
|
|
1099
|
-
|
|
1725
|
+
const discardedSuffix = discarded > 0 ? ` Discarded ${discarded} event(s) queued before pairing \u2014 you won't be beeped about them.` : "";
|
|
1726
|
+
const nextStep = chain === void 0 ? " Run `birdybeep setup` to wire up your coding agents." : "";
|
|
1727
|
+
ctx.io.line(`\u2713 Paired${humanSuffix}.${nextStep}${discardedSuffix}`);
|
|
1728
|
+
const report = chain !== void 0 ? await runSetupChain(ctx, setupDeps, pairFlags) : void 0;
|
|
1729
|
+
ctx.io.result({
|
|
1100
1730
|
paired: true,
|
|
1101
1731
|
machineId: paired.machineId,
|
|
1102
|
-
|
|
1732
|
+
discardedPrePairingEvents: discarded,
|
|
1733
|
+
...approvedBy !== void 0 ? { approvedByEmail: approvedBy } : {},
|
|
1734
|
+
...report !== void 0 ? { setup: report } : {}
|
|
1103
1735
|
});
|
|
1104
|
-
return EXIT.OK;
|
|
1736
|
+
return report !== void 0 && !report.ok ? EXIT.ERROR : EXIT.OK;
|
|
1105
1737
|
}
|
|
1106
1738
|
};
|
|
1107
1739
|
}
|
|
1740
|
+
function createPairCommand(deps = {}) {
|
|
1741
|
+
return createPairingCommand(
|
|
1742
|
+
{
|
|
1743
|
+
name: "pair",
|
|
1744
|
+
summary: "Pair this machine and wire up every coding agent on it",
|
|
1745
|
+
usage: "birdybeep pair [--yes] [--expect-email <addr>] [--no-install] [--no-test] [--json]",
|
|
1746
|
+
skipWhenPaired: false
|
|
1747
|
+
},
|
|
1748
|
+
deps
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
function createSetupCommand(deps = {}) {
|
|
1752
|
+
return createPairingCommand(
|
|
1753
|
+
{
|
|
1754
|
+
name: "setup",
|
|
1755
|
+
summary: "Set up BirdyBeep here: pair, wire up every coding agent, test",
|
|
1756
|
+
usage: "birdybeep setup [--yes] [--expect-email <addr>] [--no-install] [--no-test] [--json]",
|
|
1757
|
+
gettingStarted: "Pair this machine, wire up every coding agent it finds, and send a test Beep.",
|
|
1758
|
+
skipWhenPaired: true
|
|
1759
|
+
},
|
|
1760
|
+
deps
|
|
1761
|
+
);
|
|
1762
|
+
}
|
|
1108
1763
|
|
|
1109
1764
|
// src/commands/queue.ts
|
|
1110
|
-
import { LocalEventQueue as
|
|
1765
|
+
import { LocalEventQueue as LocalEventQueue3 } from "@birdybeep/agent-core";
|
|
1111
1766
|
function createQueueCommand() {
|
|
1112
1767
|
return {
|
|
1113
1768
|
name: "queue",
|
|
@@ -1119,7 +1774,7 @@ function createQueueCommand() {
|
|
|
1119
1774
|
summary: "Clear the local offline event queue (debug)",
|
|
1120
1775
|
usage: "birdybeep queue clear",
|
|
1121
1776
|
run: (ctx) => {
|
|
1122
|
-
const cleared = new
|
|
1777
|
+
const cleared = new LocalEventQueue3().clear();
|
|
1123
1778
|
ctx.io.emit(`Cleared ${cleared} queued event(s).`, { cleared });
|
|
1124
1779
|
return EXIT.OK;
|
|
1125
1780
|
}
|
|
@@ -1131,20 +1786,20 @@ function createQueueCommand() {
|
|
|
1131
1786
|
// src/commands/report-status.ts
|
|
1132
1787
|
import {
|
|
1133
1788
|
errorEnvelopeSchema as errorEnvelopeSchema2,
|
|
1134
|
-
getToken as
|
|
1789
|
+
getToken as getToken4,
|
|
1135
1790
|
integrationStatusResponseSchema
|
|
1136
1791
|
} from "@birdybeep/agent-core";
|
|
1137
|
-
import { CLAUDE_CODE_ADAPTER_VERSION, claudeCodeAdapter as
|
|
1138
|
-
import { CODEX_ADAPTER_VERSION, codexAdapter as
|
|
1139
|
-
import { COPILOT_ADAPTER_VERSION, copilotAdapter as
|
|
1140
|
-
import { CURSOR_ADAPTER_VERSION, cursorAdapter as
|
|
1141
|
-
import { OPENCODE_ADAPTER_VERSION, opencodeAdapter as
|
|
1792
|
+
import { CLAUDE_CODE_ADAPTER_VERSION, claudeCodeAdapter as claudeCodeAdapter4 } from "@birdybeep/claude-code";
|
|
1793
|
+
import { CODEX_ADAPTER_VERSION, codexAdapter as codexAdapter4 } from "@birdybeep/codex";
|
|
1794
|
+
import { COPILOT_ADAPTER_VERSION, copilotAdapter as copilotAdapter4 } from "@birdybeep/copilot";
|
|
1795
|
+
import { CURSOR_ADAPTER_VERSION, cursorAdapter as cursorAdapter4 } from "@birdybeep/cursor";
|
|
1796
|
+
import { OPENCODE_ADAPTER_VERSION, opencodeAdapter as opencodeAdapter4 } from "@birdybeep/opencode";
|
|
1142
1797
|
var DEFAULT_ADAPTERS3 = [
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1798
|
+
claudeCodeAdapter4,
|
|
1799
|
+
codexAdapter4,
|
|
1800
|
+
opencodeAdapter4,
|
|
1801
|
+
cursorAdapter4,
|
|
1802
|
+
copilotAdapter4
|
|
1148
1803
|
];
|
|
1149
1804
|
var ADAPTER_VERSIONS = {
|
|
1150
1805
|
claude_code: CLAUDE_CODE_ADAPTER_VERSION,
|
|
@@ -1174,7 +1829,7 @@ function createReportStatusCommand(deps = {}) {
|
|
|
1174
1829
|
summary: "Internal: report integration status to the backend",
|
|
1175
1830
|
usage: "birdybeep report-status [--json]",
|
|
1176
1831
|
run: async (ctx) => {
|
|
1177
|
-
const token = await
|
|
1832
|
+
const token = await getToken4(deps.tokenOptions ?? {});
|
|
1178
1833
|
if (token === null) {
|
|
1179
1834
|
ctx.io.errline("No machine token \u2014 run `birdybeep pair` first.");
|
|
1180
1835
|
return EXIT.ERROR;
|
|
@@ -1237,23 +1892,23 @@ function createReportStatusCommand(deps = {}) {
|
|
|
1237
1892
|
|
|
1238
1893
|
// src/commands/status.ts
|
|
1239
1894
|
import {
|
|
1240
|
-
createSender as
|
|
1895
|
+
createSender as defaultCreateSender4
|
|
1241
1896
|
} from "@birdybeep/agent-core";
|
|
1242
|
-
import { claudeCodeAdapter as
|
|
1243
|
-
import { codexAdapter as
|
|
1244
|
-
import { copilotAdapter as
|
|
1245
|
-
import { cursorAdapter as
|
|
1246
|
-
import { opencodeAdapter as
|
|
1897
|
+
import { claudeCodeAdapter as claudeCodeAdapter5 } from "@birdybeep/claude-code";
|
|
1898
|
+
import { codexAdapter as codexAdapter5 } from "@birdybeep/codex";
|
|
1899
|
+
import { copilotAdapter as copilotAdapter5 } from "@birdybeep/copilot";
|
|
1900
|
+
import { cursorAdapter as cursorAdapter5 } from "@birdybeep/cursor";
|
|
1901
|
+
import { opencodeAdapter as opencodeAdapter5 } from "@birdybeep/opencode";
|
|
1247
1902
|
var DEFAULT_ADAPTERS4 = [
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1903
|
+
claudeCodeAdapter5,
|
|
1904
|
+
codexAdapter5,
|
|
1905
|
+
opencodeAdapter5,
|
|
1906
|
+
cursorAdapter5,
|
|
1907
|
+
copilotAdapter5
|
|
1253
1908
|
];
|
|
1254
1909
|
function createStatusCommand(deps = {}) {
|
|
1255
1910
|
const adapters = deps.adapters ?? DEFAULT_ADAPTERS4;
|
|
1256
|
-
const makeSender = deps.createSender ?? ((baseUrl) =>
|
|
1911
|
+
const makeSender = deps.createSender ?? ((baseUrl) => defaultCreateSender4(
|
|
1257
1912
|
deps.tokenOptions ? { baseUrl, tokenOptions: deps.tokenOptions } : { baseUrl }
|
|
1258
1913
|
));
|
|
1259
1914
|
return {
|
|
@@ -1264,14 +1919,21 @@ function createStatusCommand(deps = {}) {
|
|
|
1264
1919
|
const machine = machineIdentity();
|
|
1265
1920
|
const paired = await isPaired(deps.tokenOptions ?? {});
|
|
1266
1921
|
const integrations = await gatherIntegrations(adapters);
|
|
1922
|
+
const surfaces = await gatherSurfaces(adapters, deps.surfaceOptions ?? {});
|
|
1267
1923
|
const depthBefore = localQueueDepth();
|
|
1924
|
+
const unpaired = unpairedActivity();
|
|
1925
|
+
const filtered = filteredActivity();
|
|
1268
1926
|
const drain = await makeSender(resolveApiUrl()).drainNow();
|
|
1269
1927
|
const depthAfter = localQueueDepth();
|
|
1928
|
+
const overflowDropped = localQueueOverflowDrops();
|
|
1270
1929
|
const report = {
|
|
1271
1930
|
machine,
|
|
1272
1931
|
paired,
|
|
1273
1932
|
integrations,
|
|
1274
|
-
|
|
1933
|
+
surfaces,
|
|
1934
|
+
queue: { depthBefore, delivered: drain.delivered, depthAfter, overflowDropped },
|
|
1935
|
+
...unpaired !== null ? { unpairedActivity: unpaired } : {},
|
|
1936
|
+
...filtered !== null ? { filteredActivity: filtered } : {}
|
|
1275
1937
|
};
|
|
1276
1938
|
if (ctx.flags.json) {
|
|
1277
1939
|
ctx.io.result(report);
|
|
@@ -1279,89 +1941,29 @@ function createStatusCommand(deps = {}) {
|
|
|
1279
1941
|
ctx.io.line(`Machine: ${machine.label} (${machine.os})`);
|
|
1280
1942
|
ctx.io.line(paired ? "Paired: yes" : "Paired: no \u2014 run `birdybeep pair`");
|
|
1281
1943
|
ctx.io.line("Integrations:");
|
|
1282
|
-
for (const i of integrations)
|
|
1944
|
+
for (const i of integrations) {
|
|
1945
|
+
ctx.io.line(` ${i.displayName}: ${i.status}`);
|
|
1946
|
+
const group = surfaces.find((g) => g.harness === i.harness);
|
|
1947
|
+
for (const state of group?.surfaces ?? []) {
|
|
1948
|
+
const mark = state.coverage === "active" ? "\u2713" : state.coverage === "wired" ? "\xB7" : "\u2717";
|
|
1949
|
+
ctx.io.line(` ${mark} ${describeSurface(state)} \u2014 ${state.coverage}`);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1283
1952
|
ctx.io.line(
|
|
1284
|
-
`Queue: ${depthBefore} queued \u2192 ${drain.delivered} delivered, ${depthAfter} remaining`
|
|
1953
|
+
`Queue: ${depthBefore} queued \u2192 ${drain.delivered} delivered, ${depthAfter} remaining` + (overflowDropped > 0 ? `, ${overflowDropped} dropped by the queue cap` : "")
|
|
1285
1954
|
);
|
|
1955
|
+
if (unpaired !== null) ctx.io.line(`\u26A0 Lost: ${describeUnpairedActivity(unpaired)}`);
|
|
1956
|
+
if (filtered !== null) ctx.io.line(`Local: ${describeFilteredActivity(filtered)}`);
|
|
1286
1957
|
}
|
|
1287
1958
|
return paired ? EXIT.OK : EXIT.ERROR;
|
|
1288
1959
|
}
|
|
1289
1960
|
};
|
|
1290
1961
|
}
|
|
1291
1962
|
|
|
1292
|
-
// src/commands/test.ts
|
|
1293
|
-
import { randomUUID } from "crypto";
|
|
1294
|
-
import {
|
|
1295
|
-
createSender as defaultCreateSender4,
|
|
1296
|
-
getMachineIdentity as getMachineIdentity3,
|
|
1297
|
-
normalizeEvent
|
|
1298
|
-
} from "@birdybeep/agent-core";
|
|
1299
|
-
function buildTestEvent(opts = {}) {
|
|
1300
|
-
const machine = getMachineIdentity3();
|
|
1301
|
-
return normalizeEvent(
|
|
1302
|
-
{
|
|
1303
|
-
event_type: "test",
|
|
1304
|
-
status: "running",
|
|
1305
|
-
harness: "claude_code",
|
|
1306
|
-
// schema requires a harness; the "test" type distinguishes it
|
|
1307
|
-
// Unique per run: a repeat `birdybeep test` inside the backend's dedupe window must
|
|
1308
|
-
// still beep — a constant id made the second test silently "deduped" (9fh).
|
|
1309
|
-
source_session_id: `birdybeep-cli-test-${randomUUID()}`,
|
|
1310
|
-
machine: { label: machine.label, os: machine.os },
|
|
1311
|
-
workspace: { cwd: process.cwd() },
|
|
1312
|
-
title: "BirdyBeep test event",
|
|
1313
|
-
body: "If you can see this, your machine is wired up correctly.",
|
|
1314
|
-
metadata: { test: true }
|
|
1315
|
-
},
|
|
1316
|
-
opts
|
|
1317
|
-
);
|
|
1318
|
-
}
|
|
1319
|
-
function createTestCommand(deps = {}) {
|
|
1320
|
-
const makeSender = deps.createSender ?? ((baseUrl) => defaultCreateSender4(
|
|
1321
|
-
deps.tokenOptions ? { baseUrl, tokenOptions: deps.tokenOptions } : { baseUrl }
|
|
1322
|
-
));
|
|
1323
|
-
return {
|
|
1324
|
-
name: "test",
|
|
1325
|
-
summary: "Send a test event end-to-end",
|
|
1326
|
-
usage: "birdybeep test [--json]",
|
|
1327
|
-
run: async (ctx) => {
|
|
1328
|
-
const event = buildTestEvent();
|
|
1329
|
-
const result = await makeSender(resolveApiUrl()).send(event);
|
|
1330
|
-
if (ctx.flags.json) {
|
|
1331
|
-
ctx.io.result({
|
|
1332
|
-
outcome: result.outcome,
|
|
1333
|
-
...result.status ? { status: result.status } : {},
|
|
1334
|
-
...result.decision ? { decision: result.decision } : {}
|
|
1335
|
-
});
|
|
1336
|
-
} else if (result.outcome === "delivered") {
|
|
1337
|
-
if (result.decision === "notified" || result.decision === void 0) {
|
|
1338
|
-
ctx.io.line("\u2713 Test event delivered \u2014 check your phone for a test Beep.");
|
|
1339
|
-
} else if (result.decision === "suppressed") {
|
|
1340
|
-
ctx.io.line(
|
|
1341
|
-
"\u26A0 The backend accepted the test event but suppressed the push \u2014 this machine or integration is probably muted. Check mutes in the app, or run `birdybeep doctor`."
|
|
1342
|
-
);
|
|
1343
|
-
} else if (result.decision === "deduped") {
|
|
1344
|
-
ctx.io.line(
|
|
1345
|
-
"\u26A0 The backend accepted the test event but folded it into a recent duplicate \u2014 wait ~30s and run `birdybeep test` again."
|
|
1346
|
-
);
|
|
1347
|
-
} else {
|
|
1348
|
-
ctx.io.line(
|
|
1349
|
-
`\u26A0 The backend accepted the test event but decided "${result.decision}" \u2014 no push was sent. Run \`birdybeep doctor\`.`
|
|
1350
|
-
);
|
|
1351
|
-
}
|
|
1352
|
-
} else if (result.outcome === "queued") {
|
|
1353
|
-
ctx.io.line("\u2022 Offline \u2014 test event queued; it will deliver when you reconnect.");
|
|
1354
|
-
} else {
|
|
1355
|
-
ctx.io.line("\u2717 Test event was rejected by the backend. Run `birdybeep doctor`.");
|
|
1356
|
-
}
|
|
1357
|
-
return result.outcome === "dropped" ? EXIT.ERROR : EXIT.OK;
|
|
1358
|
-
}
|
|
1359
|
-
};
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
1963
|
// src/commands.ts
|
|
1363
1964
|
function buildCommands() {
|
|
1364
1965
|
return [
|
|
1966
|
+
createSetupCommand(),
|
|
1365
1967
|
createPairCommand(),
|
|
1366
1968
|
createLogoutCommand(),
|
|
1367
1969
|
createUnpairCommand(),
|
|
@@ -1376,7 +1978,7 @@ function buildCommands() {
|
|
|
1376
1978
|
}
|
|
1377
1979
|
|
|
1378
1980
|
// src/update-check.ts
|
|
1379
|
-
import { mkdirSync as mkdirSync3, readFileSync as
|
|
1981
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1380
1982
|
import { join as join3 } from "path";
|
|
1381
1983
|
import { birdyBeepConfigDir as birdyBeepConfigDir3 } from "@birdybeep/agent-core";
|
|
1382
1984
|
var PACKAGE_NAME = "@birdybeep/cli";
|
|
@@ -1436,7 +2038,7 @@ function updateCachePath() {
|
|
|
1436
2038
|
}
|
|
1437
2039
|
function readUpdateCache() {
|
|
1438
2040
|
try {
|
|
1439
|
-
const parsed = JSON.parse(
|
|
2041
|
+
const parsed = JSON.parse(readFileSync3(updateCachePath(), "utf8"));
|
|
1440
2042
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1441
2043
|
const { checkedAt, latest } = parsed;
|
|
1442
2044
|
if (typeof checkedAt !== "number") return null;
|
|
@@ -1536,4 +2138,4 @@ export {
|
|
|
1536
2138
|
buildCommands,
|
|
1537
2139
|
runCli
|
|
1538
2140
|
};
|
|
1539
|
-
//# sourceMappingURL=chunk-
|
|
2141
|
+
//# sourceMappingURL=chunk-BY5MNQE3.js.map
|