badgr-cli 1.1.6 → 1.1.7
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/README.md +49 -13
- package/package.json +1 -1
- package/src/admin.js +29 -0
- package/src/api.js +35 -29
- package/src/badgr.js +18 -6
- package/src/catalog.js +12 -0
- package/src/commands/capacity.js +38 -1
- package/src/commands/check.js +127 -0
- package/src/commands/connect.js +3 -2
- package/src/commands/diagnose.js +89 -41
- package/src/commands/down.js +16 -7
- package/src/commands/job.js +5 -5
- package/src/commands/launch.js +37 -12
- package/src/commands/node.js +250 -0
- package/src/commands/run.js +14 -4
- package/src/commands/serve.js +42 -8
- package/src/commands/status.js +13 -5
- package/src/commands/train.js +1 -1
- package/src/config.js +12 -19
- package/src/credentials.js +6 -0
- package/src/fallback.js +9 -11
- package/src/gpuDoctor/gpuInfo.js +144 -5
- package/src/nodeWorker/node_worker.py +326 -0
- package/src/nodeWorker/test_node_worker.py +188 -0
- package/src/progress.js +2 -2
- package/src/spec.js +2 -2
package/src/commands/diagnose.js
CHANGED
|
@@ -5,10 +5,14 @@ import { ensureLoggedIn } from '../onboarding.js';
|
|
|
5
5
|
import { webBaseUrl } from '../config.js';
|
|
6
6
|
|
|
7
7
|
const DIAGNOSE_HELP = `
|
|
8
|
-
Paste anything. This IS
|
|
8
|
+
Paste anything. This IS Badgr Preflight: free, no GPU, no login.
|
|
9
9
|
Badgr detects the input, resolves a command, and automatically runs every
|
|
10
10
|
applicable free structural check on it. No GPU launches without explicit
|
|
11
|
-
--approve, which starts a real Badgr Run.
|
|
11
|
+
--approve, which starts a real Badgr Run (the Smoke Test step below).
|
|
12
|
+
|
|
13
|
+
Badgr Preflight
|
|
14
|
+
-> static checks -> resource fit -> compatibility checks
|
|
15
|
+
-> optional Smoke Test (a tiny, capped, real GPU run) -> Verdict
|
|
12
16
|
|
|
13
17
|
Usage:
|
|
14
18
|
badgr diagnose "<input>"
|
|
@@ -31,6 +35,12 @@ Flags:
|
|
|
31
35
|
browser sign-in automatically if not logged in).
|
|
32
36
|
This is a real, billable GPU-provisioned execution
|
|
33
37
|
-- distinct from the free diagnosis above.
|
|
38
|
+
--paid-ok Explicit consent to run as a normal paid job when a
|
|
39
|
+
free Smoke Test entitlement exists but can't cover
|
|
40
|
+
this proof (over the $0.50 cap, or the daily free-
|
|
41
|
+
verification budget is exhausted). Without this,
|
|
42
|
+
--approve stops and shows the reason instead of
|
|
43
|
+
silently charging you.
|
|
34
44
|
--smoke Deprecated, no-op. The free mechanical checks this
|
|
35
45
|
used to gate are now always run automatically as
|
|
36
46
|
part of plain "badgr diagnose" -- kept only so
|
|
@@ -44,7 +54,7 @@ Flags:
|
|
|
44
54
|
--json Machine-readable JSON output
|
|
45
55
|
--help, -h Show this help
|
|
46
56
|
|
|
47
|
-
|
|
57
|
+
Verdict: every run prints one of NEEDS INFO / READY / SMOKE CHECKED / INVALID / VERIFIED.
|
|
48
58
|
NEEDS INFO -- cannot yet produce a complete command
|
|
49
59
|
READY -- complete evidence-backed command, no applicable checks to run
|
|
50
60
|
SMOKE CHECKED -- applicable checks ran; all PASS or SKIPPED
|
|
@@ -179,11 +189,11 @@ function detectInput(raw, flags) {
|
|
|
179
189
|
return { kind: 'text', label: 'Text / conversation', body: classified.body };
|
|
180
190
|
}
|
|
181
191
|
|
|
182
|
-
// backend
|
|
192
|
+
// the backend's own /extract now returns the canonical
|
|
183
193
|
// "service" (persistent, badgr serve) vs "finite_job" (bounded, badgr
|
|
184
194
|
// run/train/comfyui) classification directly on the extraction as
|
|
185
|
-
// `workload_shape` (
|
|
186
|
-
//
|
|
195
|
+
// `workload_shape` (the same predicate the backend's own
|
|
196
|
+
// command-canonicalization logic uses to pick a subcommand) -- read that
|
|
187
197
|
// instead of re-deriving an approximation here, so the CLI's wording can
|
|
188
198
|
// never drift from what the backend actually decided. The local fallback
|
|
189
199
|
// only matters for a response predating this field (e.g. a cached fixture).
|
|
@@ -274,7 +284,7 @@ function _printResult(result, chalk) {
|
|
|
274
284
|
// verification step below is still capped (it's just a readiness
|
|
275
285
|
// check), but the resulting `badgr serve` endpoint keeps running and
|
|
276
286
|
// billing until stopped, or an explicit --max-cost/--max-runtime is
|
|
277
|
-
// supplied. See backend
|
|
287
|
+
// supplied. See the backend's own command-canonicalization logic.
|
|
278
288
|
console.log(` ${chalk.bold('Recommended action:')} Open the Run link below to verify readiness and start the service`);
|
|
279
289
|
console.log(` ${chalk.dim('The service keeps running after verification -- stop it with `badgr down`, or launch it with --max-cost/--persistent.')}`);
|
|
280
290
|
} else {
|
|
@@ -332,12 +342,13 @@ function _localPathCandidates(e) {
|
|
|
332
342
|
// Pure presentation over data /run-issue/extract already returns -- no new
|
|
333
343
|
// endpoint, no new GPU behaviour. Diagnosis alone can reach SMOKE CHECKED
|
|
334
344
|
// or INVALID (never VERIFIED, see AGENTS.md §12 and _printCopySummary
|
|
335
|
-
// below) -- `badgr diagnose` IS
|
|
336
|
-
//
|
|
337
|
-
//
|
|
345
|
+
// below) -- `badgr diagnose` IS Badgr Preflight, so applicable free checks
|
|
346
|
+
// always run and always count toward the printed verdict; there is no
|
|
347
|
+
// separate opt-in step for them. The real, bounded GPU run is the distinct
|
|
348
|
+
// "Smoke Test" step (--approve / run_smoke_test), never this free path.
|
|
338
349
|
//
|
|
339
350
|
// The evidence-only judgment (needs_info/invalid/smoke_checked) is computed
|
|
340
|
-
// once, server-side, in
|
|
351
|
+
// once, server-side, in the backend's own smoke-status computation
|
|
341
352
|
// and read here via `e.smoke_status` -- this and /run-issue's `smokeStatus()`
|
|
342
353
|
// (frontend/app/run-issue/page.tsx) both just render that one backend
|
|
343
354
|
// verdict rather than each re-deriving pass/fail from mechanical_checks, so
|
|
@@ -355,7 +366,7 @@ function _localPathCandidates(e) {
|
|
|
355
366
|
function _statusWord(result) {
|
|
356
367
|
const e = result.extraction || {};
|
|
357
368
|
if (result.static_incompatibilities?.length || e.missing_information?.length) return 'NEEDS INFO';
|
|
358
|
-
// Mirrors backend
|
|
369
|
+
// Mirrors the backend's own smoke-status computation exactly: a
|
|
359
370
|
// mechanical FAIL is invalid; otherwise only real command-scope evidence
|
|
360
371
|
// (smoke_check.proof_level === "smoke_checked" -- the repository/image/
|
|
361
372
|
// service's own documented default, not just a syntax-only pass) earns
|
|
@@ -373,24 +384,29 @@ function _statusWord(result) {
|
|
|
373
384
|
}
|
|
374
385
|
|
|
375
386
|
const _CHECK_LABELS = {
|
|
376
|
-
command_syntax:
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
387
|
+
command_syntax: 'command syntax',
|
|
388
|
+
subcommand_exists: 'known subcommand',
|
|
389
|
+
required_positional_args: 'required positional args',
|
|
390
|
+
supported_flags: 'known CLI flags',
|
|
391
|
+
cli_entrypoint: 'CLI entrypoint',
|
|
392
|
+
referenced_files: 'referenced repo files',
|
|
393
|
+
dockerfile_expected: 'Dockerfile present',
|
|
394
|
+
image_availability: 'image exists / accessible',
|
|
395
|
+
docker_entrypoint: 'Docker metadata',
|
|
396
|
+
required_env_vars: 'required env vars',
|
|
397
|
+
required_ports: 'required ports',
|
|
398
|
+
runtime_workload_compatibility: 'runtime supports workload type',
|
|
383
399
|
};
|
|
384
400
|
|
|
385
|
-
// Compact, copy-pasteable result -- the literal
|
|
386
|
-
// to forward to someone else. Additive to the detailed report above
|
|
387
|
-
// does not replace it. Never prints "Verified" -- SMOKE CHECKED is the
|
|
401
|
+
// Compact, copy-pasteable result -- the literal Verdict word plus just
|
|
402
|
+
// enough to forward to someone else. Additive to the detailed report above
|
|
403
|
+
// it; does not replace it. Never prints "Verified" -- SMOKE CHECKED is the
|
|
388
404
|
// ceiling for this free path.
|
|
389
405
|
function _printCopySummary(result, chalk, caseData, config) {
|
|
390
406
|
const e = result.extraction || {};
|
|
391
407
|
const status = _statusWord(result);
|
|
392
408
|
|
|
393
|
-
console.log(` ${chalk.bold(`
|
|
409
|
+
console.log(` ${chalk.bold(`Verdict: ${status}`)}`);
|
|
394
410
|
console.log();
|
|
395
411
|
|
|
396
412
|
if (status === 'NEEDS INFO') {
|
|
@@ -409,7 +425,7 @@ function _printCopySummary(result, chalk, caseData, config) {
|
|
|
409
425
|
// --approve would run) is preferred over the raw source command --
|
|
410
426
|
// /confirm can only compute it once a case exists (READY/SMOKE CHECKED),
|
|
411
427
|
// and only when there's enough evidence to convert (see
|
|
412
|
-
//
|
|
428
|
+
// the backend's own command-canonicalization logic for
|
|
413
429
|
// what it can't yet convert, e.g. training/ComfyUI). Falls back to the
|
|
414
430
|
// raw extracted command otherwise -- still true and still runnable input,
|
|
415
431
|
// just not guaranteed to already be `badgr`-shaped.
|
|
@@ -441,9 +457,12 @@ function _printCopySummary(result, chalk, caseData, config) {
|
|
|
441
457
|
if (status === 'INVALID') {
|
|
442
458
|
console.log(' A check above actually failed -- this command is not runnable as-is.');
|
|
443
459
|
} else {
|
|
444
|
-
console.log(
|
|
460
|
+
console.log(` ${chalk.bold('Smoke test:')} Not run yet.`);
|
|
445
461
|
}
|
|
446
462
|
console.log();
|
|
463
|
+
} else if (status === 'READY') {
|
|
464
|
+
console.log(` ${chalk.bold('Smoke test:')} Not run yet.`);
|
|
465
|
+
console.log();
|
|
447
466
|
}
|
|
448
467
|
}
|
|
449
468
|
|
|
@@ -463,9 +482,10 @@ function _printSmokeChecks(result, chalk) {
|
|
|
463
482
|
// final verdict (_statusWord can land on READY, NEEDS INFO, or INVALID
|
|
464
483
|
// just as easily as SMOKE CHECKED; see its own comment for why a
|
|
465
484
|
// syntax-only pass isn't proof of scope). A fixed "Smoke Checked" header
|
|
466
|
-
// here previously disagreed with a `
|
|
467
|
-
// after it -- this section is
|
|
468
|
-
|
|
485
|
+
// here previously disagreed with a `Verdict: READY` line printed right
|
|
486
|
+
// after it -- this section is Preflight itself (static + resource checks),
|
|
487
|
+
// not the verdict, and not the (separate, real-GPU) Smoke Test step.
|
|
488
|
+
console.log(` ${chalk.bold('Preflight (no GPU, no login):')}`);
|
|
469
489
|
|
|
470
490
|
const icon = (status) => (
|
|
471
491
|
status === 'pass' ? chalk.green('✓') : status === 'fail' ? chalk.red('✗') : chalk.dim('-')
|
|
@@ -569,7 +589,7 @@ async function _createRunPage(config, e, chalk) {
|
|
|
569
589
|
}
|
|
570
590
|
}
|
|
571
591
|
|
|
572
|
-
async function _doApprove(config, caseData, chalk) {
|
|
592
|
+
async function _doApprove(config, caseData, chalk, paidFallbackConfirmed) {
|
|
573
593
|
const { case_id: caseId, status, test_plan: plan, workload_plan: workloadPlan, missing_information: missing } = caseData;
|
|
574
594
|
|
|
575
595
|
if (status === 'incompatible') {
|
|
@@ -594,9 +614,9 @@ async function _doApprove(config, caseData, chalk) {
|
|
|
594
614
|
return;
|
|
595
615
|
}
|
|
596
616
|
|
|
597
|
-
// Display the canonical WorkloadPlan
|
|
598
|
-
//
|
|
599
|
-
//
|
|
617
|
+
// Display the canonical WorkloadPlan -- the same object creation ->
|
|
618
|
+
// routing -> Jobs -> watchdog all consume -- rather than a second,
|
|
619
|
+
// independently-derived estimate. Falls back to the
|
|
600
620
|
// raw test_plan only for fields the canonical plan doesn't carry
|
|
601
621
|
// (requires_reduced_test / requires_funding_approval are smoke-check-only
|
|
602
622
|
// concerns, never part of WorkloadPlan).
|
|
@@ -609,16 +629,16 @@ async function _doApprove(config, caseData, chalk) {
|
|
|
609
629
|
if (plan?.requires_funding_approval) console.log(` ${chalk.yellow('!')} Large download — funding required`);
|
|
610
630
|
}
|
|
611
631
|
|
|
612
|
-
await _approveAndRun(config, caseId, plan, chalk);
|
|
632
|
+
await _approveAndRun(config, caseId, plan, chalk, paidFallbackConfirmed);
|
|
613
633
|
}
|
|
614
634
|
|
|
615
635
|
// Claim → approve → run for a case that already exists (just created via
|
|
616
636
|
// /confirm above, or resumed via an existing_case input). Auth is required
|
|
617
637
|
// from here on — diagnosis and case creation stay anonymous and free, but a
|
|
618
638
|
// real GPU test needs a signed-in account. The CLI never grants a free-run
|
|
619
|
-
// entitlement (web admin only,
|
|
639
|
+
// entitlement (web admin only, granted server-side);
|
|
620
640
|
// it only redeems one a case already carries, same as normal billing.
|
|
621
|
-
async function _approveAndRun(config, caseId, plan, chalk) {
|
|
641
|
+
async function _approveAndRun(config, caseId, plan, chalk, paidFallbackConfirmed) {
|
|
622
642
|
const printResumeHint = () =>
|
|
623
643
|
console.error(chalk.dim(` Resume this case: ${webBaseUrl(config)}/run-issue?case_id=${caseId}\n`));
|
|
624
644
|
|
|
@@ -706,7 +726,11 @@ async function _approveAndRun(config, caseId, plan, chalk) {
|
|
|
706
726
|
let ran;
|
|
707
727
|
try {
|
|
708
728
|
ran = await callApi(`/run-issue/cases/${caseId}/run`, {
|
|
709
|
-
method: 'POST',
|
|
729
|
+
method: 'POST',
|
|
730
|
+
apiKey: authConfig.apiKey,
|
|
731
|
+
baseUrl: authConfig.baseUrl,
|
|
732
|
+
body: { paid_fallback_confirmed: Boolean(paidFallbackConfirmed) },
|
|
733
|
+
timeoutMs: 30_000,
|
|
710
734
|
});
|
|
711
735
|
} catch (err) {
|
|
712
736
|
if (err.isPaymentRequired) {
|
|
@@ -719,6 +743,29 @@ async function _approveAndRun(config, caseId, plan, chalk) {
|
|
|
719
743
|
return;
|
|
720
744
|
}
|
|
721
745
|
|
|
746
|
+
// The free entitlement existed but couldn't cover this proof (over the
|
|
747
|
+
// per-proof cap, or the daily subsidy budget exhausted) -- nothing ran
|
|
748
|
+
// and nothing was charged (the backend's own smoke-test execution
|
|
749
|
+
// stop-and-ask branch). Never auto-continue as paid: require the caller
|
|
750
|
+
// to explicitly re-run with --paid-ok once they've seen this and the
|
|
751
|
+
// real cap, exactly like the reduced-test block above.
|
|
752
|
+
if (ran?.paid_execution_required && !ran?.free_run_used) {
|
|
753
|
+
console.log();
|
|
754
|
+
console.log(chalk.yellow(` Smoke test not run.`));
|
|
755
|
+
console.log();
|
|
756
|
+
console.log(` ${chalk.bold('Free verification unavailable:')} ${
|
|
757
|
+
ran.free_run_skipped_reason === 'daily_budget_exhausted'
|
|
758
|
+
? 'Daily free-verification budget exhausted.'
|
|
759
|
+
: 'The smallest useful proof exceeds the free verification limit.'
|
|
760
|
+
}`);
|
|
761
|
+
console.log();
|
|
762
|
+
if (ran.max_cost_usd != null) console.log(` ${chalk.bold('Max cost:')} $${Number(ran.max_cost_usd).toFixed(2)}`);
|
|
763
|
+
console.log(chalk.dim(` Continue with a paid Smoke Test: badgr diagnose ${caseId} --approve --paid-ok`));
|
|
764
|
+
console.log();
|
|
765
|
+
process.exitCode = 1;
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
722
769
|
console.log();
|
|
723
770
|
console.log(chalk.green(' Run started.'));
|
|
724
771
|
console.log(` ${chalk.bold('Case:')} ${caseId}`);
|
|
@@ -746,7 +793,7 @@ async function _diagnoseExistingCase(config, caseId, flags, chalk) {
|
|
|
746
793
|
|
|
747
794
|
if (flags.json) {
|
|
748
795
|
console.log(JSON.stringify(caseData, null, 2));
|
|
749
|
-
if (flags.approve) await _approveAndRun(config, caseId, caseData.test_plan, chalk);
|
|
796
|
+
if (flags.approve) await _approveAndRun(config, caseId, caseData.test_plan, chalk, flags.paidOk);
|
|
750
797
|
return;
|
|
751
798
|
}
|
|
752
799
|
|
|
@@ -770,7 +817,7 @@ async function _diagnoseExistingCase(config, caseId, flags, chalk) {
|
|
|
770
817
|
|| caseData.status === 'verified' || caseData.status === 'completed'
|
|
771
818
|
? 'VERIFIED'
|
|
772
819
|
: (caseData.missing_information || []).length ? 'NEEDS INFO' : 'READY';
|
|
773
|
-
console.log(` ${chalk.bold(`
|
|
820
|
+
console.log(` ${chalk.bold(`Verdict: ${existingStatus}`)}`);
|
|
774
821
|
if (existingStatus === 'VERIFIED') {
|
|
775
822
|
console.log();
|
|
776
823
|
console.log(` ${chalk.bold('Command:')}`);
|
|
@@ -798,7 +845,7 @@ async function _diagnoseExistingCase(config, caseId, flags, chalk) {
|
|
|
798
845
|
console.log();
|
|
799
846
|
|
|
800
847
|
if (flags.approve) {
|
|
801
|
-
await _approveAndRun(config, caseId, caseData.test_plan, chalk);
|
|
848
|
+
await _approveAndRun(config, caseId, caseData.test_plan, chalk, flags.paidOk);
|
|
802
849
|
} else {
|
|
803
850
|
console.log(` ${chalk.bold('Resume:')} badgr diagnose ${caseId} --approve`);
|
|
804
851
|
console.log();
|
|
@@ -829,6 +876,7 @@ export async function diagnoseCommand(config, args, chalk) {
|
|
|
829
876
|
if (a === '--comfyui') { flags.comfyui = args[++i]; i++; continue; }
|
|
830
877
|
if (a === '--github') { flags.github = args[++i]; i++; continue; }
|
|
831
878
|
if (a === '--approve') { flags.approve = true; i++; continue; }
|
|
879
|
+
if (a === '--paid-ok') { flags.paidOk = true; i++; continue; }
|
|
832
880
|
if (a === '--smoke') { flags.smoke = true; i++; continue; }
|
|
833
881
|
if (a === '--json') { flags.json = true; i++; continue; }
|
|
834
882
|
if (!a.startsWith('-')) positionalParts.push(a);
|
|
@@ -952,7 +1000,7 @@ export async function diagnoseCommand(config, args, chalk) {
|
|
|
952
1000
|
console.error(chalk.yellow('\n Cannot approve — missing information above.\n'));
|
|
953
1001
|
return;
|
|
954
1002
|
}
|
|
955
|
-
await _doApprove(config, caseData, chalk);
|
|
1003
|
+
await _doApprove(config, caseData, chalk, flags.paidOk);
|
|
956
1004
|
}
|
|
957
1005
|
return;
|
|
958
1006
|
}
|
|
@@ -982,6 +1030,6 @@ export async function diagnoseCommand(config, args, chalk) {
|
|
|
982
1030
|
console.log(chalk.yellow('\n Cannot approve — missing information above.\n'));
|
|
983
1031
|
return;
|
|
984
1032
|
}
|
|
985
|
-
await _doApprove(config, caseData, chalk);
|
|
1033
|
+
await _doApprove(config, caseData, chalk, flags.paidOk);
|
|
986
1034
|
}
|
|
987
1035
|
}
|
package/src/commands/down.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ensureLoggedInReady, withReauthRetry } from '../onboarding.js';
|
|
2
2
|
import { findDeployment, removeDeployment, addReceipt, generateReceiptId } from '../store.js';
|
|
3
|
-
import { terminateDeployment, listDeployments
|
|
3
|
+
import { terminateDeployment, listDeployments } from '../api.js';
|
|
4
4
|
|
|
5
5
|
function formatRuntime(minutes) {
|
|
6
6
|
if (minutes < 60) return `${minutes}m`;
|
|
@@ -32,13 +32,22 @@ export async function downCommand(config, args, chalk) {
|
|
|
32
32
|
|
|
33
33
|
config = await ensureLoggedInReady(config, chalk);
|
|
34
34
|
|
|
35
|
-
// --admin bypasses per-user ownership (
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
35
|
+
// --admin bypasses per-user ownership (gated server-side by an internal
|
|
36
|
+
// admin allowlist) so a deployment found via `badgr status --admin`
|
|
37
|
+
// (owned by a different account) can actually be stopped. Local name
|
|
38
|
+
// lookup only applies to the caller's own deployments, so it's skipped
|
|
39
|
+
// here -- an admin shutdown always targets a real deployment_id from
|
|
40
|
+
// `badgr status --admin`. See ../admin.js for why this is a separate,
|
|
41
|
+
// never-publicly-exported module.
|
|
41
42
|
if (isAdmin) {
|
|
43
|
+
let adminTerminateDeployment;
|
|
44
|
+
try {
|
|
45
|
+
({ adminTerminateDeployment } = await import('../admin.js')); // oss-safe: guarded by try/catch, no-ops when admin.js is absent from a public export
|
|
46
|
+
} catch {
|
|
47
|
+
console.error(chalk.red('\n Admin mode is not available in this build.\n'));
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
42
51
|
process.stdout.write(chalk.dim(` [admin] Stopping ${idOrName}...`));
|
|
43
52
|
try {
|
|
44
53
|
const refreshed = await withReauthRetry(config, chalk, cfg => adminTerminateDeployment(cfg, idOrName));
|
package/src/commands/job.js
CHANGED
|
@@ -8,7 +8,7 @@ import { ensureBadgrReady } from '../onboarding.js';
|
|
|
8
8
|
* Submits a bounded coding-agent job via POST /v1/jobs (type: "agent"),
|
|
9
9
|
* which creates a Job record and runs it on a GPU/CPU VM.
|
|
10
10
|
* All three interfaces (website, CLI, POST /v1/jobs) share the same
|
|
11
|
-
* execution path on the backend
|
|
11
|
+
* execution path on the backend.
|
|
12
12
|
*
|
|
13
13
|
* Usage:
|
|
14
14
|
* badgr job cline "Fix the checkout bug" --check "npm test"
|
|
@@ -46,15 +46,15 @@ function parseJobArgs(rawArgs) {
|
|
|
46
46
|
return { flags, positional };
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
// Providers the agent Job type accepts (
|
|
49
|
+
// Providers the agent Job type accepts (server-side allowlist).
|
|
50
50
|
// The BYOK/OpenAI-compatible providers `badgr launch --provider` supports
|
|
51
51
|
// (openrouter, deepseek, glm, custom) are NOT among them: the Jobs API
|
|
52
52
|
// resolves credentials server-side from stored provider credentials and has
|
|
53
53
|
// nowhere to put a custom base URL, so those must be rejected in the CLI
|
|
54
54
|
// rather than sent on to a 400 from the server.
|
|
55
|
-
export const JOB_API_PROVIDERS = ['badgr', 'openai', 'anthropic'];
|
|
55
|
+
export const JOB_API_PROVIDERS = ['badgr', 'openai', 'anthropic', 'commandcode'];
|
|
56
56
|
|
|
57
|
-
export const JOB_AGENTS = ['cline', 'claude', 'claude-code', 'codex', 'playwright'];
|
|
57
|
+
export const JOB_AGENTS = ['cline', 'claude', 'claude-code', 'codex', 'opencode', 'commandcode', 'playwright'];
|
|
58
58
|
|
|
59
59
|
export async function jobCommand(config, args, chalk) {
|
|
60
60
|
// Support both:
|
|
@@ -77,7 +77,7 @@ export async function jobCommand(config, args, chalk) {
|
|
|
77
77
|
|
|
78
78
|
if (!instructionText || !agentName) {
|
|
79
79
|
console.error(chalk.red('\nUsage: badgr job <agent> "<instruction>" --check "<command>"\n'));
|
|
80
|
-
console.error(chalk.dim(' Agents: cline (default), claude-code, codex, playwright'));
|
|
80
|
+
console.error(chalk.dim(' Agents: cline (default), claude-code, codex, opencode, commandcode, playwright'));
|
|
81
81
|
console.error(chalk.dim(' Example: badgr job cline "Fix the checkout bug" --check "npm test"'));
|
|
82
82
|
console.error(chalk.dim(' Example: badgr job claude-code "Add pagination" --check "pytest tests/" --max-cost 3\n'));
|
|
83
83
|
process.exitCode = 1;
|
package/src/commands/launch.js
CHANGED
|
@@ -9,9 +9,16 @@ import { VM_CLASSES, LAUNCH_VM_SIZES, vmClassForWorkload } from '../spec.js';
|
|
|
9
9
|
// explicit `badgr launch <source> -- <command>` form (kept as the advanced
|
|
10
10
|
// escape hatch), including --image for a fully custom runtime.
|
|
11
11
|
//
|
|
12
|
+
// `opencode` rejoins this list (it was dropped from the bare-shorthand set
|
|
13
|
+
// in Phase 1's third revision — see docs/badgr-execution-roadmap.md's
|
|
14
|
+
// history note) now that it has a real image and a wrapper script verified
|
|
15
|
+
// against the live CLI (images/badgr-agent-opencode/badgr-opencode-run). It
|
|
16
|
+
// is a BYOK-only lane like claude/codex, not a Badgr-native default like
|
|
17
|
+
// cline — opencode has no Badgr-hosted model-token integration.
|
|
18
|
+
//
|
|
12
19
|
// `cline` is the Badgr-native default: no user credential required, since
|
|
13
20
|
// Badgr mints a short-lived, job-scoped token pointing at its own
|
|
14
|
-
// OpenAI-compatible coding-model endpoint (see backend
|
|
21
|
+
// OpenAI-compatible coding-model endpoint (see the backend's own
|
|
15
22
|
// `_cline_model_token` / `_build_worker_command`). `claude`/`codex` are
|
|
16
23
|
// bring-your-own-provider lanes — if no credential is stored yet, an
|
|
17
24
|
// interactive terminal prompts for one inline and stores it via `badgr
|
|
@@ -40,13 +47,19 @@ import { VM_CLASSES, LAUNCH_VM_SIZES, vmClassForWorkload } from '../spec.js';
|
|
|
40
47
|
// always exactly one array element, so it reaches the agent unchanged
|
|
41
48
|
// regardless of spaces, quotes, punctuation, or shell metacharacters: there
|
|
42
49
|
// is no shell-string layer left to round-trip it through (see
|
|
43
|
-
// command_argv on the backend and BADGR_COMMAND_JSON in
|
|
44
|
-
//
|
|
50
|
+
// command_argv on the backend and BADGR_COMMAND_JSON in the worker
|
|
51
|
+
// image's own entrypoint, which subprocess.run executes
|
|
45
52
|
// directly with shell=False — no shlex.split() involved for this path).
|
|
46
53
|
const AGENT_WORKLOADS = {
|
|
47
|
-
cline:
|
|
48
|
-
claude:
|
|
49
|
-
codex:
|
|
54
|
+
cline: { image: process.env.BADGR_AGENT_IMAGE_CLINE || 'ghcr.io/michaelmanly/badgr-agent-cline:latest', buildCmd: task => ['badgr-cline-run', task], provider: null },
|
|
55
|
+
claude: { image: process.env.BADGR_AGENT_IMAGE_CLAUDE || 'ghcr.io/michaelmanly/badgr-agent-claude:latest', buildCmd: task => ['badgr-claude-run', task], provider: 'anthropic' },
|
|
56
|
+
codex: { image: process.env.BADGR_AGENT_IMAGE_CODEX || 'ghcr.io/michaelmanly/badgr-agent-codex:latest', buildCmd: task => ['badgr-codex-run', task], provider: 'openai' },
|
|
57
|
+
opencode: { image: process.env.BADGR_AGENT_IMAGE_OPENCODE || 'ghcr.io/michaelmanly/badgr-agent-opencode:latest', buildCmd: task => ['badgr-opencode-run', task], provider: 'openai' },
|
|
58
|
+
// Command Code is a single hosted account (bundled model catalog reached
|
|
59
|
+
// through its own API), not an OpenAI/Anthropic-compatible endpoint —
|
|
60
|
+
// there is no BYOK provider-swap lane for it, unlike claude/codex/cline
|
|
61
|
+
// (see the flags.provider guard in launchAgentWorkload below).
|
|
62
|
+
commandcode: { image: process.env.BADGR_AGENT_IMAGE_COMMANDCODE || 'ghcr.io/michaelmanly/badgr-agent-commandcode:latest', buildCmd: task => ['badgr-commandcode-run', task], provider: 'commandcode' },
|
|
50
63
|
};
|
|
51
64
|
|
|
52
65
|
// Non-agent workloads: no LLM credential, and the task string (if given) is
|
|
@@ -101,8 +114,9 @@ const _VALUE_FLAGS = {
|
|
|
101
114
|
'--max-runtime': 'maxRuntime', '--max-cost': 'maxCost', '--save': 'save',
|
|
102
115
|
'--workspace': 'workspace', '--output': 'output', '--checkpoint': 'checkpoint',
|
|
103
116
|
'--resume-cmd': 'resumeCmd', '--size': 'size',
|
|
104
|
-
// BYOK / OpenAI-compatible model selection — `badgr launch cline`
|
|
105
|
-
// see resolveModelProviderEnv
|
|
117
|
+
// BYOK / OpenAI-compatible model selection — `badgr launch cline` (BYOK
|
|
118
|
+
// opt-in) and `codex`/`opencode` (BYOK-only) — see resolveModelProviderEnv
|
|
119
|
+
// below.
|
|
106
120
|
'--provider': 'provider', '--model': 'model', '--base-url': 'baseUrl',
|
|
107
121
|
// When present, routes to POST /v1/jobs (type: agent) instead of the
|
|
108
122
|
// plain badgr-launch GPU/CPU run path.
|
|
@@ -194,6 +208,8 @@ function warnIfTaskMayHaveSwallowedAFlag(task, chalk) {
|
|
|
194
208
|
* badgr launch cline "<task>" — Badgr-hosted model, no credential needed
|
|
195
209
|
* badgr launch claude "<task>" — prompts inline for an Anthropic key if not connected
|
|
196
210
|
* badgr launch codex "<task>" — prompts inline for an OpenAI key if not connected
|
|
211
|
+
* badgr launch opencode "<task>" — prompts inline for an OpenAI key if not connected
|
|
212
|
+
* badgr launch commandcode "<task>" — prompts inline for a Command Code key if not connected
|
|
197
213
|
* badgr launch playwright ["<task>"] — no credential; task is a display label only
|
|
198
214
|
* badgr launch <source> -- <command> — explicit form / advanced escape hatch
|
|
199
215
|
* badgr launch <source> --cmd "<command>"
|
|
@@ -210,6 +226,8 @@ export async function launchCommand(config, args, chalk) {
|
|
|
210
226
|
console.error(chalk.dim(' badgr launch cline "Fix the checkout bug"'));
|
|
211
227
|
console.error(chalk.dim(' badgr launch claude "Fix the checkout bug"'));
|
|
212
228
|
console.error(chalk.dim(' badgr launch codex "Fix the checkout bug"'));
|
|
229
|
+
console.error(chalk.dim(' badgr launch opencode "Fix the checkout bug"'));
|
|
230
|
+
console.error(chalk.dim(' badgr launch commandcode "Fix the checkout bug"'));
|
|
213
231
|
console.error(chalk.dim(' badgr launch playwright "Test the checkout flow"'));
|
|
214
232
|
console.error(chalk.dim(' badgr launch . --max-cost 1 -- npm test (explicit escape hatch)'));
|
|
215
233
|
console.error('');
|
|
@@ -282,7 +300,7 @@ async function resolveMissingCredential(provider, chalk, label) {
|
|
|
282
300
|
/**
|
|
283
301
|
* BYOK / OpenAI-compatible or Anthropic-compatible model selection.
|
|
284
302
|
*
|
|
285
|
-
* `badgr launch <cline|codex> --provider <name> --model <id> [--base-url <url>]`
|
|
303
|
+
* `badgr launch <cline|codex|opencode> --provider <name> --model <id> [--base-url <url>]`
|
|
286
304
|
* uses apiKind='openai' (injects OPENAI_API_KEY/OPENAI_BASE_URL/MODEL).
|
|
287
305
|
* `badgr launch claude --provider <name> --model <id> [--base-url <url>]`
|
|
288
306
|
* uses apiKind='anthropic' (injects ANTHROPIC_API_KEY/ANTHROPIC_BASE_URL/MODEL).
|
|
@@ -376,6 +394,13 @@ async function launchAgentWorkload(config, agentName, rawArgs, chalk) {
|
|
|
376
394
|
|
|
377
395
|
const spec = AGENT_WORKLOADS[agentName];
|
|
378
396
|
|
|
397
|
+
if (agentName === 'commandcode' && flags.provider) {
|
|
398
|
+
console.error(chalk.red(`\n ✗ --provider is not supported for \`badgr launch commandcode\` — it has no BYOK/swappable-endpoint lane, only its own hosted account.\n`));
|
|
399
|
+
console.error(chalk.dim(` Run \`badgr connect commandcode\` instead.\n`));
|
|
400
|
+
process.exitCode = 1;
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
379
404
|
if (spec.provider && flags.provider) {
|
|
380
405
|
// BYOK override for a fixed-provider workload (claude → anthropic,
|
|
381
406
|
// codex → openai). Skip the managed credential flow and inject the
|
|
@@ -422,7 +447,7 @@ async function launchAgentWorkload(config, agentName, rawArgs, chalk) {
|
|
|
422
447
|
} else if (flags.provider) {
|
|
423
448
|
// BYOK for cline (spec.provider is null) — injects OPENAI_API_KEY,
|
|
424
449
|
// OPENAI_BASE_URL, MODEL. The backend skips its managed model token
|
|
425
|
-
// when it sees an existing OPENAI_API_KEY (
|
|
450
|
+
// when it sees an existing OPENAI_API_KEY (handled server-side).
|
|
426
451
|
const modelEnv = await resolveModelProviderEnv(flags, chalk, 'openai');
|
|
427
452
|
if (!modelEnv) {
|
|
428
453
|
process.exitCode = 1;
|
|
@@ -435,7 +460,7 @@ async function launchAgentWorkload(config, agentName, rawArgs, chalk) {
|
|
|
435
460
|
flags.authRequired = { provider: null, status: 'none' };
|
|
436
461
|
}
|
|
437
462
|
// cline with no --provider: Badgr mints a short-lived job-scoped model
|
|
438
|
-
// token server-side
|
|
463
|
+
// token server-side; the CLI never handles
|
|
439
464
|
// or displays that token. --provider switches cline to BYOK instead.
|
|
440
465
|
|
|
441
466
|
const cmdArgv = spec.buildCmd(task);
|
|
@@ -460,7 +485,7 @@ async function launchDirectWorkload(config, workloadName, rawArgs, chalk) {
|
|
|
460
485
|
|
|
461
486
|
if (flags.provider) {
|
|
462
487
|
console.error(chalk.red(`\n ✗ --provider is not supported for \`badgr launch ${workloadName}\` — it is a test runner with no LLM component.\n`));
|
|
463
|
-
console.error(chalk.dim(` Use \`badgr launch claude/cline/codex\` for AI agent workloads.\n`));
|
|
488
|
+
console.error(chalk.dim(` Use \`badgr launch claude/cline/codex/opencode/commandcode\` for AI agent workloads.\n`));
|
|
464
489
|
process.exitCode = 1;
|
|
465
490
|
return;
|
|
466
491
|
}
|