@birdybeep/cli 0.0.1
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/LICENSE +21 -0
- package/dist/bin.cjs +941 -0
- package/dist/bin.cjs.map +1 -0
- package/dist/bin.d.cts +1 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +36 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-2KWA3QXF.js +943 -0
- package/dist/chunk-2KWA3QXF.js.map +1 -0
- package/dist/index.cjs +958 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +102 -0
- package/dist/index.d.ts +102 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
|
@@ -0,0 +1,943 @@
|
|
|
1
|
+
// src/framework.ts
|
|
2
|
+
import { mkdirSync } from "fs";
|
|
3
|
+
import { birdyBeepConfigDir } from "@birdybeep/agent-core";
|
|
4
|
+
var EXIT = { OK: 0, ERROR: 1, USAGE: 2 };
|
|
5
|
+
function createIo(json, stdout, stderr) {
|
|
6
|
+
return {
|
|
7
|
+
json,
|
|
8
|
+
line: (text) => {
|
|
9
|
+
if (!json) stdout.write(`${text}
|
|
10
|
+
`);
|
|
11
|
+
},
|
|
12
|
+
errline: (text) => stderr.write(`${text}
|
|
13
|
+
`),
|
|
14
|
+
result: (value) => {
|
|
15
|
+
if (json) stdout.write(`${JSON.stringify(value)}
|
|
16
|
+
`);
|
|
17
|
+
},
|
|
18
|
+
emit: (human, value) => {
|
|
19
|
+
if (json) stdout.write(`${JSON.stringify(value)}
|
|
20
|
+
`);
|
|
21
|
+
else stdout.write(`${human}
|
|
22
|
+
`);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
var MissingInputError = class extends Error {
|
|
27
|
+
constructor(field) {
|
|
28
|
+
super(`missing required value: ${field}`);
|
|
29
|
+
this.field = field;
|
|
30
|
+
this.name = "MissingInputError";
|
|
31
|
+
}
|
|
32
|
+
field;
|
|
33
|
+
};
|
|
34
|
+
function requireValue(ctx, field, provided) {
|
|
35
|
+
if (provided !== void 0) return provided;
|
|
36
|
+
if (ctx.flags.nonInteractive) throw new MissingInputError(field);
|
|
37
|
+
throw new MissingInputError(field);
|
|
38
|
+
}
|
|
39
|
+
var GLOBAL_FLAG_TOKENS = /* @__PURE__ */ new Set([
|
|
40
|
+
"--json",
|
|
41
|
+
"--non-interactive",
|
|
42
|
+
"--version",
|
|
43
|
+
"-v",
|
|
44
|
+
"--help",
|
|
45
|
+
"-h"
|
|
46
|
+
]);
|
|
47
|
+
function parseGlobalFlags(argv) {
|
|
48
|
+
const flags = { json: false, nonInteractive: false, help: false, version: false };
|
|
49
|
+
const rest = [];
|
|
50
|
+
for (const token of argv) {
|
|
51
|
+
switch (token) {
|
|
52
|
+
case "--json":
|
|
53
|
+
flags.json = true;
|
|
54
|
+
break;
|
|
55
|
+
case "--non-interactive":
|
|
56
|
+
flags.nonInteractive = true;
|
|
57
|
+
break;
|
|
58
|
+
case "--version":
|
|
59
|
+
case "-v":
|
|
60
|
+
flags.version = true;
|
|
61
|
+
break;
|
|
62
|
+
case "--help":
|
|
63
|
+
case "-h":
|
|
64
|
+
flags.help = true;
|
|
65
|
+
break;
|
|
66
|
+
default:
|
|
67
|
+
rest.push(token);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { flags, rest };
|
|
71
|
+
}
|
|
72
|
+
function isUnknownFlag(token) {
|
|
73
|
+
return token.startsWith("-") && !GLOBAL_FLAG_TOKENS.has(token);
|
|
74
|
+
}
|
|
75
|
+
function renderRootHelp(version, commands) {
|
|
76
|
+
const width = Math.max(...commands.map((c) => c.name.length));
|
|
77
|
+
const lines = commands.map((c) => ` ${c.name.padEnd(width)} ${c.summary}`);
|
|
78
|
+
return [
|
|
79
|
+
`birdybeep ${version} \u2014 stream coding-agent lifecycle events to BirdyBeep.`,
|
|
80
|
+
"",
|
|
81
|
+
"Usage:",
|
|
82
|
+
" birdybeep <command> [options]",
|
|
83
|
+
"",
|
|
84
|
+
"Commands:",
|
|
85
|
+
...lines,
|
|
86
|
+
"",
|
|
87
|
+
"Global options:",
|
|
88
|
+
" --json Machine-readable JSON output",
|
|
89
|
+
" --non-interactive Never prompt; fail fast if input is required",
|
|
90
|
+
" -h, --help Show help (root or per-command)",
|
|
91
|
+
" -v, --version Show the CLI version"
|
|
92
|
+
].join("\n");
|
|
93
|
+
}
|
|
94
|
+
function renderCommandHelp(path, command) {
|
|
95
|
+
const lines = [
|
|
96
|
+
`birdybeep ${path} \u2014 ${command.summary}`,
|
|
97
|
+
"",
|
|
98
|
+
"Usage:",
|
|
99
|
+
` ${command.usage ?? `birdybeep ${path} [options]`}`
|
|
100
|
+
];
|
|
101
|
+
if (command.subcommands && command.subcommands.length > 0) {
|
|
102
|
+
const width = Math.max(...command.subcommands.map((c) => c.name.length));
|
|
103
|
+
lines.push(
|
|
104
|
+
"",
|
|
105
|
+
"Subcommands:",
|
|
106
|
+
...command.subcommands.map((c) => ` ${c.name.padEnd(width)} ${c.summary}`)
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return lines.join("\n");
|
|
110
|
+
}
|
|
111
|
+
async function dispatch(argv, deps) {
|
|
112
|
+
const { flags, rest } = parseGlobalFlags(argv);
|
|
113
|
+
const io = createIo(flags.json, deps.stdout, deps.stderr);
|
|
114
|
+
if (deps.ensureConfig !== false) {
|
|
115
|
+
try {
|
|
116
|
+
mkdirSync(birdyBeepConfigDir(), { recursive: true, mode: 448 });
|
|
117
|
+
} catch {
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (flags.version) {
|
|
121
|
+
io.emit(deps.version, { version: deps.version });
|
|
122
|
+
return EXIT.OK;
|
|
123
|
+
}
|
|
124
|
+
let command = deps.commands.find((c) => c.name === rest[0]);
|
|
125
|
+
const pathParts = [];
|
|
126
|
+
let argsStart = 1;
|
|
127
|
+
if (command) {
|
|
128
|
+
pathParts.push(command.name);
|
|
129
|
+
if (command.subcommands && command.subcommands.length > 0) {
|
|
130
|
+
const sub = command.subcommands.find((c) => c.name === rest[1]);
|
|
131
|
+
if (sub) {
|
|
132
|
+
command = sub;
|
|
133
|
+
pathParts.push(sub.name);
|
|
134
|
+
argsStart = 2;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (rest.length === 0 || flags.help && command === void 0) {
|
|
139
|
+
io.emit(renderRootHelp(deps.version, deps.commands), {
|
|
140
|
+
version: deps.version,
|
|
141
|
+
commands: deps.commands.map((c) => ({ name: c.name, summary: c.summary }))
|
|
142
|
+
});
|
|
143
|
+
return EXIT.OK;
|
|
144
|
+
}
|
|
145
|
+
if (command === void 0) {
|
|
146
|
+
io.errline(`birdybeep: unknown command "${rest[0]}". Run \`birdybeep --help\`.`);
|
|
147
|
+
return EXIT.USAGE;
|
|
148
|
+
}
|
|
149
|
+
const path = pathParts.join(" ");
|
|
150
|
+
if (flags.help) {
|
|
151
|
+
io.emit(renderCommandHelp(path, command), {
|
|
152
|
+
name: path,
|
|
153
|
+
summary: command.summary,
|
|
154
|
+
usage: command.usage,
|
|
155
|
+
subcommands: command.subcommands?.map((c) => ({ name: c.name, summary: c.summary }))
|
|
156
|
+
});
|
|
157
|
+
return EXIT.OK;
|
|
158
|
+
}
|
|
159
|
+
if (command.run === void 0) {
|
|
160
|
+
io.errline(renderCommandHelp(path, command));
|
|
161
|
+
return EXIT.USAGE;
|
|
162
|
+
}
|
|
163
|
+
const args = rest.slice(argsStart);
|
|
164
|
+
const unknown = args.find(isUnknownFlag);
|
|
165
|
+
if (unknown !== void 0) {
|
|
166
|
+
io.errline(`birdybeep ${path}: unknown option "${unknown}".`);
|
|
167
|
+
return EXIT.USAGE;
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
return await command.run({ args, flags, io });
|
|
171
|
+
} catch (err) {
|
|
172
|
+
if (err instanceof MissingInputError) {
|
|
173
|
+
io.errline(
|
|
174
|
+
`birdybeep ${path}: ${err.message} (re-run without --non-interactive to be prompted).`
|
|
175
|
+
);
|
|
176
|
+
return EXIT.USAGE;
|
|
177
|
+
}
|
|
178
|
+
io.errline(`birdybeep ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
179
|
+
return EXIT.ERROR;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/version.ts
|
|
184
|
+
var CLI_VERSION = "0.0.1".length > 0 ? "0.0.1" : "0.0.0";
|
|
185
|
+
|
|
186
|
+
// src/commands/agent.ts
|
|
187
|
+
import { claudeCodeAdapter } from "@birdybeep/claude-code";
|
|
188
|
+
import { codexAdapter } from "@birdybeep/codex";
|
|
189
|
+
import { opencodeAdapter } from "@birdybeep/opencode";
|
|
190
|
+
var DEFAULT_ADAPTERS = [claudeCodeAdapter, codexAdapter, opencodeAdapter];
|
|
191
|
+
var TARGET_TO_ID = {
|
|
192
|
+
claude: "claude_code",
|
|
193
|
+
codex: "codex",
|
|
194
|
+
opencode: "opencode"
|
|
195
|
+
};
|
|
196
|
+
var AGENT_TARGETS = ["all", "claude", "codex", "opencode"];
|
|
197
|
+
function selectAdapters(target, adapters) {
|
|
198
|
+
if (target === "all") return adapters;
|
|
199
|
+
const id = TARGET_TO_ID[target];
|
|
200
|
+
if (id === void 0) return "unknown";
|
|
201
|
+
return adapters.filter((a) => a.id === id);
|
|
202
|
+
}
|
|
203
|
+
async function installSelected(adapters, ctx) {
|
|
204
|
+
const target = ctx.args[0] ?? "all";
|
|
205
|
+
const selected = selectAdapters(target, adapters);
|
|
206
|
+
if (selected === "unknown") {
|
|
207
|
+
ctx.io.errline(
|
|
208
|
+
`birdybeep agent install: unknown target "${target}" (expected ${AGENT_TARGETS.join("|")}).`
|
|
209
|
+
);
|
|
210
|
+
return EXIT.USAGE;
|
|
211
|
+
}
|
|
212
|
+
const outcomes = [];
|
|
213
|
+
for (const adapter of selected) {
|
|
214
|
+
const detection = await adapter.detect();
|
|
215
|
+
if (!detection.detected) {
|
|
216
|
+
outcomes.push({ harness: adapter.id, displayName: adapter.displayName, detected: false });
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const result = await adapter.install();
|
|
220
|
+
outcomes.push({
|
|
221
|
+
harness: adapter.id,
|
|
222
|
+
displayName: adapter.displayName,
|
|
223
|
+
detected: true,
|
|
224
|
+
status: result.status,
|
|
225
|
+
changedFiles: result.changedFiles,
|
|
226
|
+
backupFiles: result.backupFiles,
|
|
227
|
+
requiredActions: result.requiredActions
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
if (ctx.flags.json) {
|
|
231
|
+
ctx.io.result({ target, results: outcomes });
|
|
232
|
+
return EXIT.OK;
|
|
233
|
+
}
|
|
234
|
+
if (outcomes.length === 0 || outcomes.every((o) => !o.detected)) {
|
|
235
|
+
ctx.io.line("No supported harnesses detected \u2014 nothing to install.");
|
|
236
|
+
}
|
|
237
|
+
for (const o of outcomes) {
|
|
238
|
+
if (!o.detected) {
|
|
239
|
+
ctx.io.line(`\u2013 ${o.displayName}: not detected (skipped)`);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const changed = (o.changedFiles ?? []).length > 0 ? o.changedFiles.join(", ") : "no changes";
|
|
243
|
+
ctx.io.line(`\u2713 ${o.displayName}: ${o.status} (${changed})`);
|
|
244
|
+
for (const action of o.requiredActions ?? []) ctx.io.line(` \u2192 ${action}`);
|
|
245
|
+
}
|
|
246
|
+
return EXIT.OK;
|
|
247
|
+
}
|
|
248
|
+
async function uninstallSelected(adapters, ctx) {
|
|
249
|
+
const target = ctx.args[0] ?? "all";
|
|
250
|
+
const selected = selectAdapters(target, adapters);
|
|
251
|
+
if (selected === "unknown") {
|
|
252
|
+
ctx.io.errline(
|
|
253
|
+
`birdybeep agent uninstall: unknown target "${target}" (expected ${AGENT_TARGETS.join("|")}).`
|
|
254
|
+
);
|
|
255
|
+
return EXIT.USAGE;
|
|
256
|
+
}
|
|
257
|
+
const outcomes = [];
|
|
258
|
+
for (const adapter of selected) {
|
|
259
|
+
const result = await adapter.uninstall();
|
|
260
|
+
outcomes.push({
|
|
261
|
+
harness: adapter.id,
|
|
262
|
+
displayName: adapter.displayName,
|
|
263
|
+
changed: result.changed,
|
|
264
|
+
removedFiles: result.removedFiles,
|
|
265
|
+
restoredFiles: result.restoredFiles
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
if (ctx.flags.json) {
|
|
269
|
+
ctx.io.result({ target, results: outcomes });
|
|
270
|
+
return EXIT.OK;
|
|
271
|
+
}
|
|
272
|
+
for (const o of outcomes) {
|
|
273
|
+
if (!o.changed) {
|
|
274
|
+
ctx.io.line(`\u2013 ${o.displayName}: nothing to remove`);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const touched = [...o.removedFiles, ...o.restoredFiles].join(", ") || "config restored";
|
|
278
|
+
ctx.io.line(`\u2713 ${o.displayName}: removed (${touched})`);
|
|
279
|
+
}
|
|
280
|
+
return EXIT.OK;
|
|
281
|
+
}
|
|
282
|
+
function createAgentCommand(deps = {}) {
|
|
283
|
+
const adapters = deps.adapters ?? DEFAULT_ADAPTERS;
|
|
284
|
+
return {
|
|
285
|
+
name: "agent",
|
|
286
|
+
summary: "Install or uninstall harness adapters",
|
|
287
|
+
usage: "birdybeep agent <install|uninstall> [all|claude|codex|opencode]",
|
|
288
|
+
subcommands: [
|
|
289
|
+
{
|
|
290
|
+
name: "install",
|
|
291
|
+
summary: "Install adapters (all | claude | codex | opencode)",
|
|
292
|
+
usage: "birdybeep agent install [all|claude|codex|opencode]",
|
|
293
|
+
run: (ctx) => installSelected(adapters, ctx)
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
name: "uninstall",
|
|
297
|
+
summary: "Restore harness config to its pre-install state",
|
|
298
|
+
usage: "birdybeep agent uninstall [all|claude|codex|opencode]",
|
|
299
|
+
run: (ctx) => uninstallSelected(adapters, ctx)
|
|
300
|
+
}
|
|
301
|
+
]
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// src/commands/doctor.ts
|
|
306
|
+
import {
|
|
307
|
+
createSender as defaultCreateSender
|
|
308
|
+
} from "@birdybeep/agent-core";
|
|
309
|
+
import { claudeCodeAdapter as claudeCodeAdapter2 } from "@birdybeep/claude-code";
|
|
310
|
+
import { codexAdapter as codexAdapter2 } from "@birdybeep/codex";
|
|
311
|
+
import { opencodeAdapter as opencodeAdapter2 } from "@birdybeep/opencode";
|
|
312
|
+
|
|
313
|
+
// src/config.ts
|
|
314
|
+
import { mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
|
|
315
|
+
import { join } from "path";
|
|
316
|
+
import { birdyBeepConfigDir as birdyBeepConfigDir2 } from "@birdybeep/agent-core";
|
|
317
|
+
var DEFAULT_API_URL = "https://api.birdybeep.com";
|
|
318
|
+
var CONFIG_FILE = "config.json";
|
|
319
|
+
function cliConfigPath() {
|
|
320
|
+
return join(birdyBeepConfigDir2(), CONFIG_FILE);
|
|
321
|
+
}
|
|
322
|
+
function readCliConfig() {
|
|
323
|
+
try {
|
|
324
|
+
const parsed = JSON.parse(readFileSync(cliConfigPath(), "utf8"));
|
|
325
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
326
|
+
} catch {
|
|
327
|
+
return {};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function writeCliConfig(patch) {
|
|
331
|
+
const current = readCliConfig();
|
|
332
|
+
const merged = {};
|
|
333
|
+
const apiUrl = patch.apiUrl ?? current.apiUrl;
|
|
334
|
+
if (apiUrl !== void 0) merged.apiUrl = apiUrl;
|
|
335
|
+
mkdirSync2(birdyBeepConfigDir2(), { recursive: true, mode: 448 });
|
|
336
|
+
writeFileSync(cliConfigPath(), `${JSON.stringify(merged, null, 2)}
|
|
337
|
+
`, { mode: 384 });
|
|
338
|
+
}
|
|
339
|
+
function resolveApiUrl() {
|
|
340
|
+
const env = process.env["BIRDYBEEP_API_URL"];
|
|
341
|
+
if (env !== void 0 && env.length > 0) return env;
|
|
342
|
+
return readCliConfig().apiUrl ?? DEFAULT_API_URL;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// src/diagnostics.ts
|
|
346
|
+
import {
|
|
347
|
+
getMachineIdentity,
|
|
348
|
+
getToken,
|
|
349
|
+
LocalEventQueue
|
|
350
|
+
} from "@birdybeep/agent-core";
|
|
351
|
+
async function gatherIntegrations(adapters) {
|
|
352
|
+
return Promise.all(
|
|
353
|
+
adapters.map(async (a) => ({
|
|
354
|
+
harness: a.id,
|
|
355
|
+
displayName: a.displayName,
|
|
356
|
+
status: await a.status()
|
|
357
|
+
}))
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
async function isLoggedIn(tokenOptions = {}) {
|
|
361
|
+
return await getToken(tokenOptions) !== null;
|
|
362
|
+
}
|
|
363
|
+
function localQueueDepth() {
|
|
364
|
+
return new LocalEventQueue().size();
|
|
365
|
+
}
|
|
366
|
+
function machineIdentity() {
|
|
367
|
+
return getMachineIdentity();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/commands/doctor.ts
|
|
371
|
+
var DEFAULT_ADAPTERS2 = [claudeCodeAdapter2, codexAdapter2, opencodeAdapter2];
|
|
372
|
+
async function defaultProbeNetwork(baseUrl) {
|
|
373
|
+
try {
|
|
374
|
+
const controller = new AbortController();
|
|
375
|
+
const timer = setTimeout(() => controller.abort(), 3e3);
|
|
376
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
377
|
+
const res = await fetch(baseUrl, { method: "HEAD", signal: controller.signal });
|
|
378
|
+
clearTimeout(timer);
|
|
379
|
+
return res.status < 500;
|
|
380
|
+
} catch {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function createDoctorCommand(deps = {}) {
|
|
385
|
+
const adapters = deps.adapters ?? DEFAULT_ADAPTERS2;
|
|
386
|
+
const probeNetwork = deps.probeNetwork ?? defaultProbeNetwork;
|
|
387
|
+
const makeSender = deps.createSender ?? ((baseUrl) => defaultCreateSender(
|
|
388
|
+
deps.tokenOptions ? { baseUrl, tokenOptions: deps.tokenOptions } : { baseUrl }
|
|
389
|
+
));
|
|
390
|
+
return {
|
|
391
|
+
name: "doctor",
|
|
392
|
+
summary: "Diagnose token, trust, restart, and offline-queue issues",
|
|
393
|
+
usage: "birdybeep doctor [--json]",
|
|
394
|
+
run: async (ctx) => {
|
|
395
|
+
const checks = [];
|
|
396
|
+
const apiUrl = resolveApiUrl();
|
|
397
|
+
const loggedIn = await isLoggedIn(deps.tokenOptions ?? {});
|
|
398
|
+
checks.push(
|
|
399
|
+
loggedIn ? { name: "Machine token", ok: true } : {
|
|
400
|
+
name: "Machine token",
|
|
401
|
+
ok: false,
|
|
402
|
+
detail: "No machine token found.",
|
|
403
|
+
remedy: "Run `birdybeep login` to pair this machine."
|
|
404
|
+
}
|
|
405
|
+
);
|
|
406
|
+
for (const adapter of adapters) {
|
|
407
|
+
const result = await adapter.doctor();
|
|
408
|
+
for (const c of result.checks) {
|
|
409
|
+
checks.push({
|
|
410
|
+
name: `${adapter.displayName}: ${c.name}`,
|
|
411
|
+
ok: c.ok,
|
|
412
|
+
...c.detail !== void 0 ? { detail: c.detail } : {},
|
|
413
|
+
...c.remedy !== void 0 ? { remedy: c.remedy } : {}
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const depthBefore = localQueueDepth();
|
|
418
|
+
const drain = await makeSender(apiUrl).drainNow();
|
|
419
|
+
const depthAfter = localQueueDepth();
|
|
420
|
+
checks.push({
|
|
421
|
+
name: "Local queue",
|
|
422
|
+
ok: true,
|
|
423
|
+
detail: `${depthBefore} queued \u2192 ${drain.delivered} delivered, ${depthAfter} remaining`
|
|
424
|
+
});
|
|
425
|
+
const reachable = await probeNetwork(apiUrl);
|
|
426
|
+
checks.push(
|
|
427
|
+
reachable ? { name: "Backend reachable", ok: true } : {
|
|
428
|
+
name: "Backend reachable",
|
|
429
|
+
ok: false,
|
|
430
|
+
detail: `Could not reach ${apiUrl}.`,
|
|
431
|
+
remedy: "Check your network; queued events will retry automatically."
|
|
432
|
+
}
|
|
433
|
+
);
|
|
434
|
+
const ok = checks.every((c) => c.ok);
|
|
435
|
+
if (ctx.flags.json) {
|
|
436
|
+
ctx.io.result({
|
|
437
|
+
ok,
|
|
438
|
+
checks,
|
|
439
|
+
queue: { depthBefore, delivered: drain.delivered, depthAfter }
|
|
440
|
+
});
|
|
441
|
+
} else {
|
|
442
|
+
for (const c of checks) {
|
|
443
|
+
ctx.io.line(`${c.ok ? "\u2713" : "\u2717"} ${c.name}${c.detail ? ` \u2014 ${c.detail}` : ""}`);
|
|
444
|
+
if (!c.ok && c.remedy) ctx.io.line(` \u2192 ${c.remedy}`);
|
|
445
|
+
}
|
|
446
|
+
ctx.io.line(ok ? "\nAll checks passed." : "\nSome checks failed \u2014 see fixes above.");
|
|
447
|
+
}
|
|
448
|
+
return ok ? EXIT.OK : EXIT.ERROR;
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/commands/hook.ts
|
|
454
|
+
import {
|
|
455
|
+
createSender as defaultCreateSender2
|
|
456
|
+
} from "@birdybeep/agent-core";
|
|
457
|
+
import { runClaudeHook } from "@birdybeep/claude-code";
|
|
458
|
+
import { runCodexHook } from "@birdybeep/codex";
|
|
459
|
+
import { runOpenCodeHook } from "@birdybeep/opencode";
|
|
460
|
+
var RUNNERS = {
|
|
461
|
+
claude: runClaudeHook,
|
|
462
|
+
codex: runCodexHook,
|
|
463
|
+
opencode: runOpenCodeHook
|
|
464
|
+
};
|
|
465
|
+
var HOOK_HARNESSES = ["claude", "codex", "opencode"];
|
|
466
|
+
var STDIN_READ_TIMEOUT_MS = 3e3;
|
|
467
|
+
function withTimeout(promise, ms, fallback) {
|
|
468
|
+
return new Promise((resolve) => {
|
|
469
|
+
let settled = false;
|
|
470
|
+
const finish = (value) => {
|
|
471
|
+
if (settled) return;
|
|
472
|
+
settled = true;
|
|
473
|
+
clearTimeout(timer);
|
|
474
|
+
resolve(value);
|
|
475
|
+
};
|
|
476
|
+
const timer = setTimeout(() => finish(fallback), ms);
|
|
477
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
478
|
+
void promise.then(finish, () => finish(fallback));
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
function isHarnessName(value) {
|
|
482
|
+
return value === "claude" || value === "codex" || value === "opencode";
|
|
483
|
+
}
|
|
484
|
+
function runHookCommand(harness, payload, sender) {
|
|
485
|
+
return RUNNERS[harness](payload, { sender });
|
|
486
|
+
}
|
|
487
|
+
function readStdinDefault() {
|
|
488
|
+
return new Promise((resolve) => {
|
|
489
|
+
if (process.stdin.isTTY) {
|
|
490
|
+
resolve("");
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
let data = "";
|
|
494
|
+
process.stdin.setEncoding("utf8");
|
|
495
|
+
process.stdin.on("data", (chunk) => data += chunk);
|
|
496
|
+
process.stdin.on("end", () => resolve(data));
|
|
497
|
+
process.stdin.on("error", () => resolve(""));
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
async function readHookPayload(args, readStdin) {
|
|
501
|
+
return args[1] ?? await readStdin();
|
|
502
|
+
}
|
|
503
|
+
function createHookCommand(deps = {}) {
|
|
504
|
+
const makeSender = deps.createSender ?? ((baseUrl) => defaultCreateSender2({ baseUrl }));
|
|
505
|
+
const readStdin = deps.readStdin ?? readStdinDefault;
|
|
506
|
+
const stdinTimeoutMs = deps.stdinTimeoutMs ?? STDIN_READ_TIMEOUT_MS;
|
|
507
|
+
return {
|
|
508
|
+
name: "hook",
|
|
509
|
+
summary: "Internal: normalize + send an event fired by a harness hook",
|
|
510
|
+
usage: "birdybeep hook <claude|codex|opencode>",
|
|
511
|
+
run: async (ctx) => {
|
|
512
|
+
const harness = ctx.args[0];
|
|
513
|
+
if (!isHarnessName(harness)) {
|
|
514
|
+
ctx.io.errline(`birdybeep hook: expected one of ${HOOK_HARNESSES.join("|")}`);
|
|
515
|
+
return EXIT.USAGE;
|
|
516
|
+
}
|
|
517
|
+
const raw = await withTimeout(readHookPayload(ctx.args, readStdin), stdinTimeoutMs, "");
|
|
518
|
+
let payload;
|
|
519
|
+
try {
|
|
520
|
+
payload = JSON.parse(raw);
|
|
521
|
+
} catch {
|
|
522
|
+
ctx.io.result({ harness, outcome: "skipped" });
|
|
523
|
+
return EXIT.OK;
|
|
524
|
+
}
|
|
525
|
+
const sender = makeSender(resolveApiUrl());
|
|
526
|
+
const result = await runHookCommand(harness, payload, sender);
|
|
527
|
+
ctx.io.result({ harness, outcome: result.outcome, eventType: result.eventType });
|
|
528
|
+
return EXIT.OK;
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// src/commands/login.ts
|
|
534
|
+
import { getMachineIdentity as getMachineIdentity2, setToken } from "@birdybeep/agent-core";
|
|
535
|
+
import { renderUnicodeCompact } from "uqr";
|
|
536
|
+
|
|
537
|
+
// src/pairing.ts
|
|
538
|
+
import {
|
|
539
|
+
pairStartResponseSchema,
|
|
540
|
+
pairTokenResponseSchema
|
|
541
|
+
} from "@birdybeep/agent-core";
|
|
542
|
+
function base(apiUrl) {
|
|
543
|
+
return apiUrl.replace(/\/$/, "");
|
|
544
|
+
}
|
|
545
|
+
async function pairStart(apiUrl, input, fetchImpl) {
|
|
546
|
+
const body = {
|
|
547
|
+
machine_label: input.machineLabel,
|
|
548
|
+
...input.os !== void 0 ? { os: input.os } : {},
|
|
549
|
+
...input.cliVersion !== void 0 ? { cli_version: input.cliVersion } : {}
|
|
550
|
+
};
|
|
551
|
+
const res = await fetchImpl(`${base(apiUrl)}/v1/pair/start`, {
|
|
552
|
+
method: "POST",
|
|
553
|
+
headers: { "content-type": "application/json" },
|
|
554
|
+
body: JSON.stringify(body)
|
|
555
|
+
});
|
|
556
|
+
if (!res.ok) throw new Error(`pairing could not be started (HTTP ${res.status})`);
|
|
557
|
+
const parsed = pairStartResponseSchema.safeParse(await res.json());
|
|
558
|
+
if (!parsed.success) throw new Error("pairing start returned an unexpected response shape");
|
|
559
|
+
return parsed.data;
|
|
560
|
+
}
|
|
561
|
+
async function pairTokenPoll(apiUrl, deviceCode, fetchImpl, machineFingerprint) {
|
|
562
|
+
const body = {
|
|
563
|
+
device_code: deviceCode,
|
|
564
|
+
...machineFingerprint !== void 0 ? { machine_fingerprint: machineFingerprint } : {}
|
|
565
|
+
};
|
|
566
|
+
const res = await fetchImpl(`${base(apiUrl)}/v1/pair/token`, {
|
|
567
|
+
method: "POST",
|
|
568
|
+
headers: { "content-type": "application/json" },
|
|
569
|
+
body: JSON.stringify(body)
|
|
570
|
+
});
|
|
571
|
+
if (!res.ok) return { status: "pending" };
|
|
572
|
+
const parsed = pairTokenResponseSchema.safeParse(await res.json());
|
|
573
|
+
if (!parsed.success) return { status: "pending" };
|
|
574
|
+
return {
|
|
575
|
+
status: "paired",
|
|
576
|
+
machineToken: parsed.data.machine_token,
|
|
577
|
+
machineId: parsed.data.machine_id
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// src/commands/login.ts
|
|
582
|
+
var DEFAULT_POLL_INTERVAL_MS = 2e3;
|
|
583
|
+
function renderQrMatrix(qrPayload) {
|
|
584
|
+
return renderUnicodeCompact(qrPayload, { border: 2 });
|
|
585
|
+
}
|
|
586
|
+
function createLoginCommand(deps = {}) {
|
|
587
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
588
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
589
|
+
const clock = deps.now ?? (() => Date.now());
|
|
590
|
+
const renderQr = deps.renderQr ?? renderQrMatrix;
|
|
591
|
+
const intervalMs = deps.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
592
|
+
return {
|
|
593
|
+
name: "login",
|
|
594
|
+
summary: "Pair this machine with your BirdyBeep account (QR or manual)",
|
|
595
|
+
usage: "birdybeep login [--json]",
|
|
596
|
+
run: async (ctx) => {
|
|
597
|
+
const apiUrl = resolveApiUrl();
|
|
598
|
+
const identity = getMachineIdentity2();
|
|
599
|
+
const start = await pairStart(
|
|
600
|
+
apiUrl,
|
|
601
|
+
{ machineLabel: identity.label, os: identity.os, cliVersion: CLI_VERSION },
|
|
602
|
+
fetchImpl
|
|
603
|
+
);
|
|
604
|
+
if (ctx.flags.json) {
|
|
605
|
+
ctx.io.result({
|
|
606
|
+
status: "pairing_started",
|
|
607
|
+
user_code: start.user_code,
|
|
608
|
+
qr_payload: start.qr_payload,
|
|
609
|
+
expires_at: start.expires_at
|
|
610
|
+
});
|
|
611
|
+
} else {
|
|
612
|
+
ctx.io.line(
|
|
613
|
+
"To pair this machine, scan the code or open the link, then confirm in the app:"
|
|
614
|
+
);
|
|
615
|
+
const isTTY = deps.isTTY ?? process.stdout.isTTY === true;
|
|
616
|
+
if (isTTY) ctx.io.line(renderQr(start.qr_payload));
|
|
617
|
+
ctx.io.line(` Scan or open: ${start.qr_payload}`);
|
|
618
|
+
ctx.io.line(` Code: ${start.user_code}`);
|
|
619
|
+
ctx.io.line("Waiting for confirmation\u2026");
|
|
620
|
+
}
|
|
621
|
+
const deadline = Date.parse(start.expires_at);
|
|
622
|
+
let paired;
|
|
623
|
+
while (clock() < deadline) {
|
|
624
|
+
await sleep(intervalMs);
|
|
625
|
+
const poll = await pairTokenPoll(
|
|
626
|
+
apiUrl,
|
|
627
|
+
start.device_code,
|
|
628
|
+
fetchImpl,
|
|
629
|
+
identity.fingerprintHash
|
|
630
|
+
);
|
|
631
|
+
if (poll.status === "paired") {
|
|
632
|
+
paired = poll;
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (paired === void 0 || paired.status !== "paired") {
|
|
637
|
+
ctx.io.result({ paired: false, reason: "timeout" });
|
|
638
|
+
ctx.io.errline(
|
|
639
|
+
"Pairing timed out before it was confirmed. Run `birdybeep login` to retry."
|
|
640
|
+
);
|
|
641
|
+
return EXIT.ERROR;
|
|
642
|
+
}
|
|
643
|
+
await setToken(paired.machineToken, deps.tokenOptions ?? {});
|
|
644
|
+
writeCliConfig({ apiUrl });
|
|
645
|
+
ctx.io.emit(`\u2713 Paired. Run \`birdybeep test\` to send a test Beep.`, {
|
|
646
|
+
paired: true,
|
|
647
|
+
machineId: paired.machineId
|
|
648
|
+
});
|
|
649
|
+
return EXIT.OK;
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// src/commands/logout.ts
|
|
655
|
+
import { clearToken } from "@birdybeep/agent-core";
|
|
656
|
+
function createLogoutCommand(deps = {}) {
|
|
657
|
+
return {
|
|
658
|
+
name: "logout",
|
|
659
|
+
summary: "Remove the local machine token",
|
|
660
|
+
usage: "birdybeep logout",
|
|
661
|
+
run: async (ctx) => {
|
|
662
|
+
await clearToken(deps.tokenOptions ?? {});
|
|
663
|
+
ctx.io.emit("Logged out \u2014 the machine token was removed.", { loggedOut: true });
|
|
664
|
+
return EXIT.OK;
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// src/commands/queue.ts
|
|
670
|
+
import { LocalEventQueue as LocalEventQueue2 } from "@birdybeep/agent-core";
|
|
671
|
+
function createQueueCommand() {
|
|
672
|
+
return {
|
|
673
|
+
name: "queue",
|
|
674
|
+
summary: "Local event-queue maintenance",
|
|
675
|
+
usage: "birdybeep queue <clear>",
|
|
676
|
+
subcommands: [
|
|
677
|
+
{
|
|
678
|
+
name: "clear",
|
|
679
|
+
summary: "Clear the local offline event queue (debug)",
|
|
680
|
+
usage: "birdybeep queue clear",
|
|
681
|
+
run: (ctx) => {
|
|
682
|
+
const cleared = new LocalEventQueue2().clear();
|
|
683
|
+
ctx.io.emit(`Cleared ${cleared} queued event(s).`, { cleared });
|
|
684
|
+
return EXIT.OK;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
]
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// src/commands/report-status.ts
|
|
692
|
+
import {
|
|
693
|
+
errorEnvelopeSchema,
|
|
694
|
+
getToken as getToken2,
|
|
695
|
+
integrationStatusResponseSchema
|
|
696
|
+
} from "@birdybeep/agent-core";
|
|
697
|
+
import { CLAUDE_CODE_ADAPTER_VERSION, claudeCodeAdapter as claudeCodeAdapter3 } from "@birdybeep/claude-code";
|
|
698
|
+
import { CODEX_ADAPTER_VERSION, codexAdapter as codexAdapter3 } from "@birdybeep/codex";
|
|
699
|
+
import { OPENCODE_ADAPTER_VERSION, opencodeAdapter as opencodeAdapter3 } from "@birdybeep/opencode";
|
|
700
|
+
var DEFAULT_ADAPTERS3 = [claudeCodeAdapter3, codexAdapter3, opencodeAdapter3];
|
|
701
|
+
var ADAPTER_VERSIONS = {
|
|
702
|
+
claude_code: CLAUDE_CODE_ADAPTER_VERSION,
|
|
703
|
+
codex: CODEX_ADAPTER_VERSION,
|
|
704
|
+
opencode: OPENCODE_ADAPTER_VERSION
|
|
705
|
+
};
|
|
706
|
+
var base2 = (apiUrl) => apiUrl.replace(/\/$/, "");
|
|
707
|
+
async function gatherItems(adapters) {
|
|
708
|
+
return Promise.all(
|
|
709
|
+
adapters.map(async (a) => {
|
|
710
|
+
const [detection, status] = await Promise.all([a.detect(), a.status()]);
|
|
711
|
+
const item = { harness: a.id, status };
|
|
712
|
+
if (detection.version !== void 0) item.harness_version = detection.version;
|
|
713
|
+
const adapterVersion = ADAPTER_VERSIONS[a.id];
|
|
714
|
+
if (adapterVersion !== void 0) item.adapter_version = adapterVersion;
|
|
715
|
+
return item;
|
|
716
|
+
})
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
function createReportStatusCommand(deps = {}) {
|
|
720
|
+
const adapters = deps.adapters ?? DEFAULT_ADAPTERS3;
|
|
721
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
722
|
+
return {
|
|
723
|
+
name: "report-status",
|
|
724
|
+
summary: "Internal: report integration status to the backend",
|
|
725
|
+
usage: "birdybeep report-status [--json]",
|
|
726
|
+
run: async (ctx) => {
|
|
727
|
+
const token = await getToken2(deps.tokenOptions ?? {});
|
|
728
|
+
if (token === null) {
|
|
729
|
+
ctx.io.errline("Not logged in \u2014 run `birdybeep login` first.");
|
|
730
|
+
return EXIT.ERROR;
|
|
731
|
+
}
|
|
732
|
+
const items = await gatherItems(adapters);
|
|
733
|
+
if (items.length === 0) {
|
|
734
|
+
ctx.io.emit("No integrations to report.", { outcome: "reported", integrations: [] });
|
|
735
|
+
return EXIT.OK;
|
|
736
|
+
}
|
|
737
|
+
let effective = items.map((i) => ({ harness: i.harness, status: i.status }));
|
|
738
|
+
let outcome = "deferred";
|
|
739
|
+
let errorCode;
|
|
740
|
+
try {
|
|
741
|
+
const res = await fetchImpl(`${base2(resolveApiUrl())}/v1/integrations/status`, {
|
|
742
|
+
method: "POST",
|
|
743
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
744
|
+
body: JSON.stringify({ integrations: items })
|
|
745
|
+
});
|
|
746
|
+
if (res.ok) {
|
|
747
|
+
outcome = "reported";
|
|
748
|
+
const parsed = integrationStatusResponseSchema.safeParse(
|
|
749
|
+
await res.json().catch(() => void 0)
|
|
750
|
+
);
|
|
751
|
+
if (parsed.success) {
|
|
752
|
+
effective = parsed.data.integrations.map((i) => ({
|
|
753
|
+
harness: i.harness,
|
|
754
|
+
status: i.status
|
|
755
|
+
}));
|
|
756
|
+
}
|
|
757
|
+
} else {
|
|
758
|
+
const env = errorEnvelopeSchema.safeParse(await res.json().catch(() => void 0));
|
|
759
|
+
errorCode = env.success ? env.data.error.code : void 0;
|
|
760
|
+
const terminal = errorCode !== void 0 ? errorCode === "unauthorized" || errorCode === "forbidden" || errorCode === "token_revoked" : res.status === 401 || res.status === 403;
|
|
761
|
+
outcome = terminal ? "terminal" : "deferred";
|
|
762
|
+
}
|
|
763
|
+
} catch {
|
|
764
|
+
outcome = "deferred";
|
|
765
|
+
}
|
|
766
|
+
if (ctx.flags.json) {
|
|
767
|
+
ctx.io.result({
|
|
768
|
+
outcome,
|
|
769
|
+
integrations: effective,
|
|
770
|
+
...errorCode !== void 0 ? { error: errorCode } : {}
|
|
771
|
+
});
|
|
772
|
+
} else if (outcome === "terminal") {
|
|
773
|
+
ctx.io.errline(
|
|
774
|
+
`Report rejected (${errorCode ?? "auth"}) \u2014 your token may be revoked. Re-run \`birdybeep login\`.`
|
|
775
|
+
);
|
|
776
|
+
} else {
|
|
777
|
+
for (const e of effective) {
|
|
778
|
+
ctx.io.line(
|
|
779
|
+
outcome === "reported" ? `\u2713 ${e.harness}: ${e.status} (reported)` : `\u2022 ${e.harness}: ${e.status} (deferred \u2014 backend unreachable)`
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return outcome === "terminal" ? EXIT.ERROR : EXIT.OK;
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// src/commands/status.ts
|
|
789
|
+
import {
|
|
790
|
+
createSender as defaultCreateSender3
|
|
791
|
+
} from "@birdybeep/agent-core";
|
|
792
|
+
import { claudeCodeAdapter as claudeCodeAdapter4 } from "@birdybeep/claude-code";
|
|
793
|
+
import { codexAdapter as codexAdapter4 } from "@birdybeep/codex";
|
|
794
|
+
import { opencodeAdapter as opencodeAdapter4 } from "@birdybeep/opencode";
|
|
795
|
+
var DEFAULT_ADAPTERS4 = [claudeCodeAdapter4, codexAdapter4, opencodeAdapter4];
|
|
796
|
+
function createStatusCommand(deps = {}) {
|
|
797
|
+
const adapters = deps.adapters ?? DEFAULT_ADAPTERS4;
|
|
798
|
+
const makeSender = deps.createSender ?? ((baseUrl) => defaultCreateSender3(
|
|
799
|
+
deps.tokenOptions ? { baseUrl, tokenOptions: deps.tokenOptions } : { baseUrl }
|
|
800
|
+
));
|
|
801
|
+
return {
|
|
802
|
+
name: "status",
|
|
803
|
+
summary: "Show pairing + per-harness integration status",
|
|
804
|
+
usage: "birdybeep status [--json]",
|
|
805
|
+
run: async (ctx) => {
|
|
806
|
+
const machine = machineIdentity();
|
|
807
|
+
const loggedIn = await isLoggedIn(deps.tokenOptions ?? {});
|
|
808
|
+
const integrations = await gatherIntegrations(adapters);
|
|
809
|
+
const depthBefore = localQueueDepth();
|
|
810
|
+
const drain = await makeSender(resolveApiUrl()).drainNow();
|
|
811
|
+
const depthAfter = localQueueDepth();
|
|
812
|
+
const report = {
|
|
813
|
+
machine,
|
|
814
|
+
loggedIn,
|
|
815
|
+
integrations,
|
|
816
|
+
queue: { depthBefore, delivered: drain.delivered, depthAfter }
|
|
817
|
+
};
|
|
818
|
+
if (ctx.flags.json) {
|
|
819
|
+
ctx.io.result(report);
|
|
820
|
+
} else {
|
|
821
|
+
ctx.io.line(`Machine: ${machine.label} (${machine.os})`);
|
|
822
|
+
ctx.io.line(
|
|
823
|
+
loggedIn ? "Login: paired" : "Login: NOT logged in \u2014 run `birdybeep login`"
|
|
824
|
+
);
|
|
825
|
+
ctx.io.line("Integrations:");
|
|
826
|
+
for (const i of integrations) ctx.io.line(` ${i.displayName}: ${i.status}`);
|
|
827
|
+
ctx.io.line(
|
|
828
|
+
`Queue: ${depthBefore} queued \u2192 ${drain.delivered} delivered, ${depthAfter} remaining`
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
return loggedIn ? EXIT.OK : EXIT.ERROR;
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// src/commands/test.ts
|
|
837
|
+
import { randomUUID } from "crypto";
|
|
838
|
+
import {
|
|
839
|
+
createSender as defaultCreateSender4,
|
|
840
|
+
getMachineIdentity as getMachineIdentity3,
|
|
841
|
+
normalizeEvent
|
|
842
|
+
} from "@birdybeep/agent-core";
|
|
843
|
+
function buildTestEvent(opts = {}) {
|
|
844
|
+
const machine = getMachineIdentity3();
|
|
845
|
+
return normalizeEvent(
|
|
846
|
+
{
|
|
847
|
+
event_type: "test",
|
|
848
|
+
status: "running",
|
|
849
|
+
harness: "claude_code",
|
|
850
|
+
// schema requires a harness; the "test" type distinguishes it
|
|
851
|
+
// Unique per run: a repeat `birdybeep test` inside the backend's dedupe window must
|
|
852
|
+
// still beep — a constant id made the second test silently "deduped" (9fh).
|
|
853
|
+
source_session_id: `birdybeep-cli-test-${randomUUID()}`,
|
|
854
|
+
machine: { label: machine.label, os: machine.os },
|
|
855
|
+
workspace: { cwd: process.cwd() },
|
|
856
|
+
title: "BirdyBeep test event",
|
|
857
|
+
body: "If you can see this, your machine is wired up correctly.",
|
|
858
|
+
metadata: { test: true }
|
|
859
|
+
},
|
|
860
|
+
opts
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
function createTestCommand(deps = {}) {
|
|
864
|
+
const makeSender = deps.createSender ?? ((baseUrl) => defaultCreateSender4(
|
|
865
|
+
deps.tokenOptions ? { baseUrl, tokenOptions: deps.tokenOptions } : { baseUrl }
|
|
866
|
+
));
|
|
867
|
+
return {
|
|
868
|
+
name: "test",
|
|
869
|
+
summary: "Send a test event end-to-end",
|
|
870
|
+
usage: "birdybeep test [--json]",
|
|
871
|
+
run: async (ctx) => {
|
|
872
|
+
const event = buildTestEvent();
|
|
873
|
+
const result = await makeSender(resolveApiUrl()).send(event);
|
|
874
|
+
if (ctx.flags.json) {
|
|
875
|
+
ctx.io.result({
|
|
876
|
+
outcome: result.outcome,
|
|
877
|
+
...result.status ? { status: result.status } : {},
|
|
878
|
+
...result.decision ? { decision: result.decision } : {}
|
|
879
|
+
});
|
|
880
|
+
} else if (result.outcome === "delivered") {
|
|
881
|
+
if (result.decision === "notified" || result.decision === void 0) {
|
|
882
|
+
ctx.io.line("\u2713 Test event delivered \u2014 check your phone for a test Beep.");
|
|
883
|
+
} else if (result.decision === "suppressed") {
|
|
884
|
+
ctx.io.line(
|
|
885
|
+
"\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`."
|
|
886
|
+
);
|
|
887
|
+
} else if (result.decision === "deduped") {
|
|
888
|
+
ctx.io.line(
|
|
889
|
+
"\u26A0 The backend accepted the test event but folded it into a recent duplicate \u2014 wait ~30s and run `birdybeep test` again."
|
|
890
|
+
);
|
|
891
|
+
} else {
|
|
892
|
+
ctx.io.line(
|
|
893
|
+
`\u26A0 The backend accepted the test event but decided "${result.decision}" \u2014 no push was sent. Run \`birdybeep doctor\`.`
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
} else if (result.outcome === "queued") {
|
|
897
|
+
ctx.io.line("\u2022 Offline \u2014 test event queued; it will deliver when you reconnect.");
|
|
898
|
+
} else {
|
|
899
|
+
ctx.io.line("\u2717 Test event was rejected by the backend. Run `birdybeep doctor`.");
|
|
900
|
+
}
|
|
901
|
+
return result.outcome === "dropped" ? EXIT.ERROR : EXIT.OK;
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// src/commands.ts
|
|
907
|
+
function buildCommands() {
|
|
908
|
+
return [
|
|
909
|
+
createLoginCommand(),
|
|
910
|
+
createLogoutCommand(),
|
|
911
|
+
createStatusCommand(),
|
|
912
|
+
createTestCommand(),
|
|
913
|
+
createDoctorCommand(),
|
|
914
|
+
createAgentCommand(),
|
|
915
|
+
createHookCommand(),
|
|
916
|
+
createQueueCommand(),
|
|
917
|
+
createReportStatusCommand()
|
|
918
|
+
];
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// src/cli.ts
|
|
922
|
+
function runCli(argv, deps = {}) {
|
|
923
|
+
return dispatch(argv, {
|
|
924
|
+
version: CLI_VERSION,
|
|
925
|
+
commands: deps.commands ?? buildCommands(),
|
|
926
|
+
stdout: deps.stdout ?? process.stdout,
|
|
927
|
+
stderr: deps.stderr ?? process.stderr,
|
|
928
|
+
...deps.ensureConfig !== void 0 ? { ensureConfig: deps.ensureConfig } : {}
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
export {
|
|
933
|
+
EXIT,
|
|
934
|
+
createIo,
|
|
935
|
+
MissingInputError,
|
|
936
|
+
requireValue,
|
|
937
|
+
parseGlobalFlags,
|
|
938
|
+
dispatch,
|
|
939
|
+
CLI_VERSION,
|
|
940
|
+
buildCommands,
|
|
941
|
+
runCli
|
|
942
|
+
};
|
|
943
|
+
//# sourceMappingURL=chunk-2KWA3QXF.js.map
|