@fastino-ai/pioneer-cli 0.2.9 → 0.2.10

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.
Files changed (4) hide show
  1. package/README.md +21 -12
  2. package/package.json +1 -1
  3. package/src/api.ts +108 -7
  4. package/src/index.tsx +1625 -711
package/src/index.tsx CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import React, { useState, useEffect, useRef } from "react";
8
- import { render, Box, Text, useApp, useInput, useStdin, Static } from "ink";
8
+ import { render, Box, Text, useApp, useInput, useStdin, useStdout, Static } from "ink";
9
9
  import Spinner from "ink-spinner";
10
10
  import TextInput from "ink-text-input";
11
11
  import * as fs from "fs";
@@ -167,6 +167,8 @@ const BOOLEAN_FLAGS = new Set([
167
167
  "format-results",
168
168
  "reasoning-trace",
169
169
  "use-meta-felix",
170
+ "json",
171
+ "all",
170
172
  ]);
171
173
 
172
174
  function parseArgs(argv: string[]): {
@@ -2091,7 +2093,7 @@ function ModelCreateInteractive({
2091
2093
  ) : (
2092
2094
  <Box flexDirection="column">
2093
2095
  {topMatches.map((model, index) => {
2094
- const suffix = [model.label, model.task_type, model.type].filter(Boolean).join(" · ");
2096
+ const suffix = [model.label, model.task_type].filter(Boolean).join(" · ");
2095
2097
  const modelLine = `${model.id}${suffix ? ` (${suffix})` : ""}`;
2096
2098
  const isHighlighted = index === highlightIndex;
2097
2099
  return (
@@ -2111,6 +2113,1210 @@ function ModelCreateInteractive({
2111
2113
  );
2112
2114
  }
2113
2115
 
2116
+ // ─────────────────────────────────────────────────────────────────────────────
2117
+ // Job ID Resolver
2118
+ // ─────────────────────────────────────────────────────────────────────────────
2119
+
2120
+ function WithResolvedJobId({
2121
+ rawId,
2122
+ render,
2123
+ }: {
2124
+ rawId: string;
2125
+ render: (jobId: string) => React.ReactElement;
2126
+ }) {
2127
+ const initiallyResolved = isUuid(rawId);
2128
+ const [state, setState] = useState<"resolving" | "resolved" | "error">(
2129
+ initiallyResolved ? "resolved" : "resolving"
2130
+ );
2131
+ const [resolvedId, setResolvedId] = useState(initiallyResolved ? rawId : "");
2132
+ const [error, setError] = useState("");
2133
+
2134
+ useEffect(() => {
2135
+ if (initiallyResolved) {
2136
+ return;
2137
+ }
2138
+ let isActive = true;
2139
+ (async () => {
2140
+ const result = await api.listJobs();
2141
+ if (!isActive) return;
2142
+ if (!result.ok) {
2143
+ setError(result.error ?? "Failed to look up training jobs.");
2144
+ setState("error");
2145
+ return;
2146
+ }
2147
+ const jobs = result.data?.training_jobs ?? [];
2148
+ const norm = rawId.toLowerCase();
2149
+ const matches = jobs.filter((job) =>
2150
+ typeof job.id === "string" && job.id.toLowerCase().startsWith(norm)
2151
+ );
2152
+ if (matches.length === 0) {
2153
+ setError(
2154
+ `No training job matches "${rawId}". Run \`pioneer job list\` to see available IDs.`
2155
+ );
2156
+ setState("error");
2157
+ return;
2158
+ }
2159
+ if (matches.length > 1) {
2160
+ const preview = matches
2161
+ .slice(0, 5)
2162
+ .map((j) => `${j.id} (${j.model_name ?? "—"})`)
2163
+ .join(", ");
2164
+ setError(
2165
+ `Ambiguous job id "${rawId}". Matches: ${preview}${matches.length > 5 ? "…" : ""}. Use a longer prefix or the full UUID.`
2166
+ );
2167
+ setState("error");
2168
+ return;
2169
+ }
2170
+ setResolvedId(matches[0]!.id!);
2171
+ setState("resolved");
2172
+ })();
2173
+
2174
+ return () => {
2175
+ isActive = false;
2176
+ };
2177
+ }, [rawId, initiallyResolved]);
2178
+
2179
+ if (state === "resolving") {
2180
+ return <Loading message={`Resolving job id ${rawId}...`} />;
2181
+ }
2182
+ if (state === "error") {
2183
+ return <ErrorMessage error={error} />;
2184
+ }
2185
+ return render(resolvedId);
2186
+ }
2187
+
2188
+ // ─────────────────────────────────────────────────────────────────────────────
2189
+ // Job List Command (tabular output)
2190
+ // ─────────────────────────────────────────────────────────────────────────────
2191
+
2192
+ function shortJobId(id?: string): string {
2193
+ if (!id) return "—";
2194
+ return id.length > 8 ? id.slice(0, 8) : id;
2195
+ }
2196
+
2197
+ function shortBaseModel(baseModel?: string): string {
2198
+ if (!baseModel) return "—";
2199
+ const parts = baseModel.split("/");
2200
+ return parts[parts.length - 1] || baseModel;
2201
+ }
2202
+
2203
+ function relativeTime(iso?: string): string {
2204
+ if (!iso) return "—";
2205
+ const ts = Date.parse(iso);
2206
+ if (Number.isNaN(ts)) return iso;
2207
+ const diffMs = Date.now() - ts;
2208
+ const seconds = Math.round(diffMs / 1000);
2209
+ if (seconds < 60) return `${Math.max(seconds, 0)}s ago`;
2210
+ const minutes = Math.round(seconds / 60);
2211
+ if (minutes < 60) return `${minutes}m ago`;
2212
+ const hours = Math.round(minutes / 60);
2213
+ if (hours < 24) return `${hours}h ago`;
2214
+ const days = Math.round(hours / 24);
2215
+ if (days < 30) return `${days}d ago`;
2216
+ const months = Math.round(days / 30);
2217
+ if (months < 12) return `${months}mo ago`;
2218
+ const years = Math.round(days / 365);
2219
+ return `${years}y ago`;
2220
+ }
2221
+
2222
+ function statusColor(status?: string): string | undefined {
2223
+ const normalized = (status ?? "").toLowerCase();
2224
+ if (normalized === "complete" || normalized === "completed" || normalized === "succeeded") {
2225
+ return "green";
2226
+ }
2227
+ if (normalized === "failed" || normalized === "error" || normalized === "cancelled" || normalized === "canceled") {
2228
+ return "red";
2229
+ }
2230
+ if (normalized === "running" || normalized === "training" || normalized === "in_progress") {
2231
+ return "cyan";
2232
+ }
2233
+ if (normalized === "pending" || normalized === "queued" || normalized === "starting") {
2234
+ return "yellow";
2235
+ }
2236
+ return undefined;
2237
+ }
2238
+
2239
+ function padCell(value: string, width: number): string {
2240
+ if (value.length === width) return value;
2241
+ if (value.length > width) {
2242
+ if (width <= 1) return value.slice(0, width);
2243
+ return `${value.slice(0, Math.max(width - 1, 1))}…`;
2244
+ }
2245
+ return value + " ".repeat(width - value.length);
2246
+ }
2247
+
2248
+ interface JobRow {
2249
+ id: string;
2250
+ model: string;
2251
+ baseModel: string;
2252
+ task: string;
2253
+ status: string;
2254
+ created: string;
2255
+ [key: string]: string;
2256
+ }
2257
+
2258
+ function jobsToRows(jobs: api.TrainingJob[]): JobRow[] {
2259
+ return jobs.map((job) => ({
2260
+ id: shortJobId(job.id),
2261
+ model: job.model_name?.trim() || "—",
2262
+ baseModel: shortBaseModel(job.base_model),
2263
+ task: job.task_type?.trim() || "—",
2264
+ status: job.status?.trim() || "—",
2265
+ created: relativeTime(job.created_at),
2266
+ }));
2267
+ }
2268
+
2269
+ // ─────────────────────────────────────────────────────────────────────────────
2270
+ // Reusable Table + DetailView primitives
2271
+ // Used by: job list/get, dataset list/get, model base-models, model endpoints get,
2272
+ // model endpoints quality-metrics, model artifacts download.
2273
+ // ─────────────────────────────────────────────────────────────────────────────
2274
+
2275
+ interface TableColumn<TRow> {
2276
+ key: keyof TRow & string;
2277
+ header: string;
2278
+ minWidth: number;
2279
+ maxWidth: number;
2280
+ flexible?: boolean;
2281
+ color?: (row: TRow) => string | undefined;
2282
+ }
2283
+
2284
+ function computeTableWidths<TRow extends Record<string, string>>(
2285
+ columns: TableColumn<TRow>[],
2286
+ rows: TRow[],
2287
+ terminalWidth: number
2288
+ ): number[] {
2289
+ const desired = columns.map((column) => {
2290
+ const headerLength = column.header.length;
2291
+ const dataLength = rows.reduce(
2292
+ (max, row) => Math.max(max, ((row[column.key] as string | undefined) ?? "").length),
2293
+ 0
2294
+ );
2295
+ const want = Math.max(headerLength, dataLength);
2296
+ return Math.max(column.minWidth, Math.min(column.maxWidth, want));
2297
+ });
2298
+
2299
+ const separators = (columns.length - 1) * 2;
2300
+ const available = Math.max(40, terminalWidth - 1);
2301
+ const total = desired.reduce((sum, w) => sum + w, 0) + separators;
2302
+
2303
+ if (total <= available) return desired;
2304
+
2305
+ let overflow = total - available;
2306
+ const flexIndices = columns
2307
+ .map((column, idx) => (column.flexible ? idx : -1))
2308
+ .filter((idx) => idx >= 0);
2309
+
2310
+ while (overflow > 0) {
2311
+ let trimmedThisPass = false;
2312
+ for (const idx of flexIndices) {
2313
+ if (overflow <= 0) break;
2314
+ if (desired[idx]! > columns[idx]!.minWidth) {
2315
+ desired[idx] = desired[idx]! - 1;
2316
+ overflow -= 1;
2317
+ trimmedThisPass = true;
2318
+ }
2319
+ }
2320
+ if (!trimmedThisPass) break;
2321
+ }
2322
+
2323
+ return desired;
2324
+ }
2325
+
2326
+ function useTerminalWidth(): number {
2327
+ const { stdout } = useStdout();
2328
+ const [width, setWidth] = useState(() => {
2329
+ const cols = stdout?.columns;
2330
+ if (typeof cols === "number" && cols > 0) return cols;
2331
+ const env = parseInt(process.env.COLUMNS ?? "", 10);
2332
+ return Number.isFinite(env) && env > 0 ? env : 80;
2333
+ });
2334
+ useEffect(() => {
2335
+ if (!stdout) return;
2336
+ const handler = () => {
2337
+ if (typeof stdout.columns === "number" && stdout.columns > 0) {
2338
+ setWidth(stdout.columns);
2339
+ }
2340
+ };
2341
+ stdout.on("resize", handler);
2342
+ return () => {
2343
+ stdout.off("resize", handler);
2344
+ };
2345
+ }, [stdout]);
2346
+ return width;
2347
+ }
2348
+
2349
+ function DataTable<TRow extends Record<string, string>>({
2350
+ columns,
2351
+ rows,
2352
+ footer,
2353
+ }: {
2354
+ columns: TableColumn<TRow>[];
2355
+ rows: TRow[];
2356
+ footer?: string;
2357
+ }) {
2358
+ const terminalWidth = useTerminalWidth();
2359
+ const widths = computeTableWidths(columns, rows, terminalWidth);
2360
+
2361
+ const headerCells = columns.map((column, idx) =>
2362
+ padCell(column.header, widths[idx]!)
2363
+ );
2364
+
2365
+ return (
2366
+ <Box flexDirection="column">
2367
+ <Text bold dimColor>{headerCells.join(" ")}</Text>
2368
+ {rows.map((row, rowIdx) => {
2369
+ const cells = columns.map((column, idx) => {
2370
+ const raw = (row[column.key] as string | undefined) ?? "";
2371
+ return padCell(String(raw), widths[idx]!);
2372
+ });
2373
+ return (
2374
+ <Text key={rowIdx}>
2375
+ {columns.map((column, idx) => {
2376
+ const cell = cells[idx]!;
2377
+ const color = column.color?.(row);
2378
+ return (
2379
+ <React.Fragment key={String(column.key)}>
2380
+ {idx > 0 ? " " : ""}
2381
+ {color ? <Text color={color}>{cell}</Text> : cell}
2382
+ </React.Fragment>
2383
+ );
2384
+ })}
2385
+ </Text>
2386
+ );
2387
+ })}
2388
+ {footer ? (
2389
+ <Box marginTop={1}>
2390
+ <Text dimColor>{footer}</Text>
2391
+ </Box>
2392
+ ) : null}
2393
+ </Box>
2394
+ );
2395
+ }
2396
+
2397
+ interface DetailRow {
2398
+ label: string;
2399
+ value: string;
2400
+ valueColor?: string;
2401
+ }
2402
+
2403
+ function DetailView({
2404
+ rows,
2405
+ footer,
2406
+ }: {
2407
+ rows: DetailRow[];
2408
+ footer?: string;
2409
+ }) {
2410
+ if (rows.length === 0) {
2411
+ return <Text dimColor>No data.</Text>;
2412
+ }
2413
+ // Render single-record details through the same DataTable primitive used for
2414
+ // list views, so `<thing> get` and `<thing> list` share the exact same
2415
+ // visual style (uppercase headers, column padding, ellipsizing).
2416
+ const tableRows = rows.map((row) => ({
2417
+ field: row.label.toUpperCase(),
2418
+ value: row.value,
2419
+ __valueColor: row.valueColor ?? "",
2420
+ }));
2421
+ const columns: TableColumn<(typeof tableRows)[number]>[] = [
2422
+ { key: "field", header: "FIELD", minWidth: 6, maxWidth: 24 },
2423
+ {
2424
+ key: "value",
2425
+ header: "VALUE",
2426
+ minWidth: 10,
2427
+ maxWidth: 80,
2428
+ flexible: true,
2429
+ color: (row) => row.__valueColor || undefined,
2430
+ },
2431
+ ];
2432
+ return <DataTable columns={columns} rows={tableRows} footer={footer} />;
2433
+ }
2434
+
2435
+ const JOB_COLUMNS: TableColumn<JobRow>[] = [
2436
+ { key: "id", header: "ID", minWidth: 8, maxWidth: 8 },
2437
+ { key: "model", header: "MODEL", minWidth: 8, maxWidth: 32, flexible: true },
2438
+ { key: "baseModel", header: "BASE MODEL", minWidth: 10, maxWidth: 30, flexible: true },
2439
+ { key: "task", header: "TASK", minWidth: 4, maxWidth: 14, flexible: true },
2440
+ { key: "status", header: "STATUS", minWidth: 6, maxWidth: 10, color: (row) => statusColor(row.status) },
2441
+ { key: "created", header: "CREATED", minWidth: 7, maxWidth: 10 },
2442
+ ];
2443
+
2444
+ export function JobListCommand() {
2445
+ const { exit } = useApp();
2446
+ const [state, setState] = useState<"loading" | "done" | "error">("loading");
2447
+ const [jobs, setJobs] = useState<api.TrainingJob[]>([]);
2448
+ const [error, setError] = useState("");
2449
+
2450
+ useEffect(() => {
2451
+ (async () => {
2452
+ const result = await api.listJobs();
2453
+ if (!result.ok) {
2454
+ setError(result.error ?? "Failed to load training jobs.");
2455
+ setState("error");
2456
+ } else {
2457
+ setJobs(result.data?.training_jobs ?? []);
2458
+ setState("done");
2459
+ }
2460
+ setTimeout(() => exit(), 500);
2461
+ })();
2462
+ }, [exit]);
2463
+
2464
+ if (state === "loading") return <Loading message="Loading training jobs..." />;
2465
+ if (state === "error") return <ErrorMessage error={error} />;
2466
+ if (jobs.length === 0) {
2467
+ return (
2468
+ <Box flexDirection="column">
2469
+ <Text>No training jobs found.</Text>
2470
+ <Text dimColor>Create one with `pioneer agent` — it will guide you through picking a base model and datasets.</Text>
2471
+ </Box>
2472
+ );
2473
+ }
2474
+
2475
+ return (
2476
+ <DataTable
2477
+ columns={JOB_COLUMNS}
2478
+ rows={jobsToRows(jobs)}
2479
+ footer={`${jobs.length} job${jobs.length === 1 ? "" : "s"} · the short ID prefix above is enough for \`pioneer job get/logs/delete\` · use \`--json\` for raw JSON`}
2480
+ />
2481
+ );
2482
+ }
2483
+
2484
+ // ─────────────────────────────────────────────────────────────────────────────
2485
+ // Job Get Command (single-job key/value layout)
2486
+ // ─────────────────────────────────────────────────────────────────────────────
2487
+
2488
+ function formatTimestamp(iso?: string): string {
2489
+ if (!iso) return "—";
2490
+ const ts = Date.parse(iso);
2491
+ if (Number.isNaN(ts)) return iso;
2492
+ const date = new Date(ts);
2493
+ const pad = (n: number) => n.toString().padStart(2, "0");
2494
+ const yyyy = date.getUTCFullYear();
2495
+ const mm = pad(date.getUTCMonth() + 1);
2496
+ const dd = pad(date.getUTCDate());
2497
+ const hh = pad(date.getUTCHours());
2498
+ const mi = pad(date.getUTCMinutes());
2499
+ const ss = pad(date.getUTCSeconds());
2500
+ return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss} UTC (${relativeTime(iso)})`;
2501
+ }
2502
+
2503
+ function formatDuration(startIso?: string, endIso?: string): string {
2504
+ if (!startIso || !endIso) return "—";
2505
+ const start = Date.parse(startIso);
2506
+ const end = Date.parse(endIso);
2507
+ if (Number.isNaN(start) || Number.isNaN(end) || end < start) return "—";
2508
+ const seconds = Math.floor((end - start) / 1000);
2509
+ const days = Math.floor(seconds / 86400);
2510
+ const hours = Math.floor((seconds % 86400) / 3600);
2511
+ const minutes = Math.floor((seconds % 3600) / 60);
2512
+ const remSeconds = seconds % 60;
2513
+ const parts: string[] = [];
2514
+ if (days) parts.push(`${days}d`);
2515
+ if (hours) parts.push(`${hours}h`);
2516
+ if (minutes) parts.push(`${minutes}m`);
2517
+ if (!parts.length || remSeconds) parts.push(`${remSeconds}s`);
2518
+ return parts.join(" ");
2519
+ }
2520
+
2521
+ function formatDatasetList(
2522
+ datasets?: Array<{ name?: string; version?: string | number }>
2523
+ ): string {
2524
+ if (!datasets || datasets.length === 0) return "—";
2525
+ return datasets
2526
+ .map((ds) => {
2527
+ const name = ds?.name ?? "(unnamed)";
2528
+ const version = ds?.version != null && ds.version !== "" ? ` (v${ds.version})` : "";
2529
+ return `${name}${version}`;
2530
+ })
2531
+ .join(", ");
2532
+ }
2533
+
2534
+ function formatLabels(labels?: string[]): string {
2535
+ if (!labels || labels.length === 0) return "—";
2536
+ return labels.join(", ");
2537
+ }
2538
+
2539
+ function formatBoolean(value: unknown): string {
2540
+ if (value === true) return "yes";
2541
+ if (value === false) return "no";
2542
+ return "—";
2543
+ }
2544
+
2545
+ function formatNumber(value: unknown): string {
2546
+ if (typeof value !== "number" || Number.isNaN(value)) return "—";
2547
+ return Number.isInteger(value) ? String(value) : value.toString();
2548
+ }
2549
+
2550
+ function formatPercent(value: unknown): string {
2551
+ if (typeof value !== "number" || Number.isNaN(value)) return "—";
2552
+ const pct = value > 1 ? value : value * 100;
2553
+ return `${Number.isInteger(pct) ? pct : pct.toFixed(1)}%`;
2554
+ }
2555
+
2556
+ function formatNonEmpty(value: unknown): string {
2557
+ if (typeof value === "string") {
2558
+ const trimmed = value.trim();
2559
+ return trimmed.length ? trimmed : "—";
2560
+ }
2561
+ return "—";
2562
+ }
2563
+
2564
+ function jobToDetailRows(job: api.TrainingJob): DetailRow[] {
2565
+ const record = job as unknown as Record<string, unknown>;
2566
+ const provider =
2567
+ typeof record.provider_name === "string" ? record.provider_name : undefined;
2568
+ const progressPercent = typeof record.progress_percent === "number" ? record.progress_percent : undefined;
2569
+ const currentEpoch = typeof record.current_epoch === "number" ? record.current_epoch : undefined;
2570
+
2571
+ const rows: DetailRow[] = [
2572
+ { label: "ID", value: formatNonEmpty(job.id) },
2573
+ { label: "Model", value: formatNonEmpty(job.model_name) },
2574
+ { label: "Base model", value: formatNonEmpty(job.base_model) },
2575
+ { label: "Task", value: formatNonEmpty(job.task_type) },
2576
+ { label: "Status", value: formatNonEmpty(job.status), valueColor: statusColor(job.status) },
2577
+ { label: "Provider", value: formatNonEmpty(provider) },
2578
+ { label: "Datasets", value: formatDatasetList(job.datasets) },
2579
+ { label: "Labels", value: formatLabels(job.labels) },
2580
+ { label: "Epochs", value: formatNumber(job.nr_epochs) },
2581
+ { label: "Batch size", value: formatNumber(job.batch_size) },
2582
+ { label: "Learning rate", value: formatNumber(job.learning_rate) },
2583
+ { label: "Validation split", value: formatPercent(job.validation_data_percentage) },
2584
+ { label: "Auto-selected", value: formatBoolean(job.model_auto_selected) },
2585
+ { label: "Version", value: formatNonEmpty(job.version_number) },
2586
+ { label: "Created", value: formatTimestamp(job.created_at) },
2587
+ { label: "Started", value: formatTimestamp(job.started_at) },
2588
+ { label: "Completed", value: formatTimestamp(job.completed_at) },
2589
+ { label: "Duration", value: formatDuration(job.started_at, job.completed_at) },
2590
+ { label: "Updated", value: formatTimestamp(job.updated_at) },
2591
+ ];
2592
+
2593
+ if (progressPercent !== undefined) {
2594
+ rows.splice(5, 0, {
2595
+ label: "Progress",
2596
+ value: `${progressPercent.toFixed(1)}%${currentEpoch != null ? ` (epoch ${currentEpoch})` : ""}`,
2597
+ });
2598
+ }
2599
+ if (job.error_message) {
2600
+ rows.push({
2601
+ label: "Error",
2602
+ value: job.error_message,
2603
+ valueColor: "red",
2604
+ });
2605
+ }
2606
+
2607
+ return rows;
2608
+ }
2609
+
2610
+ function JobGetCommand({ jobId }: { jobId: string }) {
2611
+ const { exit } = useApp();
2612
+ const [state, setState] = useState<"loading" | "done" | "error">("loading");
2613
+ const [job, setJob] = useState<api.TrainingJob | null>(null);
2614
+ const [error, setError] = useState("");
2615
+
2616
+ useEffect(() => {
2617
+ let isActive = true;
2618
+ (async () => {
2619
+ const result = await api.getJob(jobId);
2620
+ if (!isActive) return;
2621
+ if (!result.ok) {
2622
+ setError(result.error ?? "Failed to load training job.");
2623
+ setState("error");
2624
+ } else {
2625
+ setJob((result.data as api.TrainingJob | undefined) ?? null);
2626
+ setState("done");
2627
+ }
2628
+ setTimeout(() => exit(), 500);
2629
+ })();
2630
+ return () => {
2631
+ isActive = false;
2632
+ };
2633
+ }, [jobId, exit]);
2634
+
2635
+ if (state === "loading") {
2636
+ return <Loading message={`Loading training job ${jobId}...`} />;
2637
+ }
2638
+ if (state === "error") {
2639
+ return <ErrorMessage error={error} />;
2640
+ }
2641
+ if (!job) {
2642
+ return <ErrorMessage error="Training job response was empty." />;
2643
+ }
2644
+
2645
+ return (
2646
+ <DetailView
2647
+ rows={jobToDetailRows(job)}
2648
+ footer={`Use \`pioneer job get ${jobId} --json\` for the raw JSON payload.`}
2649
+ />
2650
+ );
2651
+ }
2652
+
2653
+ // ─────────────────────────────────────────────────────────────────────────────
2654
+ // Base Models List Command (tabular)
2655
+ // ─────────────────────────────────────────────────────────────────────────────
2656
+
2657
+ interface BaseModelRow {
2658
+ id: string;
2659
+ name: string;
2660
+ type: string;
2661
+ context: string;
2662
+ inference: string;
2663
+ training: string;
2664
+ [key: string]: string;
2665
+ }
2666
+
2667
+ const BASE_MODEL_COLUMNS: TableColumn<BaseModelRow>[] = [
2668
+ { key: "id", header: "ID", minWidth: 12, maxWidth: 36, flexible: true },
2669
+ { key: "name", header: "NAME", minWidth: 8, maxWidth: 30, flexible: true },
2670
+ { key: "type", header: "TYPE", minWidth: 4, maxWidth: 8 },
2671
+ { key: "context", header: "CONTEXT", minWidth: 7, maxWidth: 8 },
2672
+ { key: "inference", header: "INFERENCE", minWidth: 9, maxWidth: 9, color: (row) => (row.inference === "yes" ? "green" : "red") },
2673
+ { key: "training", header: "TRAINING", minWidth: 8, maxWidth: 8, color: (row) => (row.training === "yes" ? "green" : "red") },
2674
+ ];
2675
+
2676
+ function formatContextWindow(n?: number): string {
2677
+ if (typeof n !== "number" || !Number.isFinite(n) || n <= 0) return "—";
2678
+ if (n >= 1_000_000) {
2679
+ const v = n / 1_000_000;
2680
+ return `${Number.isInteger(v) ? v : v.toFixed(1)}M`;
2681
+ }
2682
+ if (n >= 1000) {
2683
+ const v = n / 1000;
2684
+ return `${Number.isInteger(v) ? v : v.toFixed(0)}K`;
2685
+ }
2686
+ return String(n);
2687
+ }
2688
+
2689
+ function BaseModelsListCommand() {
2690
+ const { exit } = useApp();
2691
+ const [state, setState] = useState<"loading" | "done" | "error">("loading");
2692
+ const [models, setModels] = useState<api.BaseModelInfo[]>([]);
2693
+ const [error, setError] = useState("");
2694
+
2695
+ useEffect(() => {
2696
+ (async () => {
2697
+ const result = await api.listBaseModels();
2698
+ if (!result.ok) {
2699
+ setError(result.error ?? "Failed to load base models.");
2700
+ setState("error");
2701
+ } else {
2702
+ setModels(normalizeBaseModels(result.data));
2703
+ setState("done");
2704
+ }
2705
+ setTimeout(() => exit(), 500);
2706
+ })();
2707
+ }, [exit]);
2708
+
2709
+ if (state === "loading") return <Loading message="Loading base models..." />;
2710
+ if (state === "error") return <ErrorMessage error={error} />;
2711
+ if (models.length === 0) {
2712
+ return <Text>No base models available.</Text>;
2713
+ }
2714
+
2715
+ const rows: BaseModelRow[] = models.map((m) => {
2716
+ const inference = m.supports_inference ?? m.supports_on_demand_inference;
2717
+ return {
2718
+ id: m.id || "—",
2719
+ name: (m.label || m.name || m.id || "—").trim() || "—",
2720
+ type: m.task_type || "—",
2721
+ context: formatContextWindow(m.context_window),
2722
+ inference: inference === undefined ? "—" : inference ? "yes" : "no",
2723
+ training: m.supports_training === undefined ? "—" : m.supports_training ? "yes" : "no",
2724
+ };
2725
+ });
2726
+
2727
+ return (
2728
+ <DataTable
2729
+ columns={BASE_MODEL_COLUMNS}
2730
+ rows={rows}
2731
+ footer={`${models.length} base model${models.length === 1 ? "" : "s"} · use \`--json\` for raw JSON`}
2732
+ />
2733
+ );
2734
+ }
2735
+
2736
+ // ─────────────────────────────────────────────────────────────────────────────
2737
+ // Model Endpoint Get Command (single-resource key/value layout, with dataset count)
2738
+ // ─────────────────────────────────────────────────────────────────────────────
2739
+
2740
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2741
+
2742
+ function ModelEndpointGetCommand({ modelId }: { modelId: string }) {
2743
+ const { exit } = useApp();
2744
+ const [state, setState] = useState<"loading" | "done" | "error">("loading");
2745
+ const [project, setProject] = useState<api.ProjectResponse | null>(null);
2746
+ const [datasetCount, setDatasetCount] = useState<api.ProjectDatasetCountResponse | null>(null);
2747
+ const [activeJob, setActiveJob] = useState<api.TrainingJob | null>(null);
2748
+ const [error, setError] = useState("");
2749
+
2750
+ useEffect(() => {
2751
+ let active = true;
2752
+ (async () => {
2753
+ // Sequential to dodge Bun concurrent-fetch keep-alive flakiness.
2754
+ const projectResult = await api.getProject(modelId);
2755
+ if (!active) return;
2756
+ if (!projectResult.ok) {
2757
+ setError(projectResult.error ?? "Failed to load model endpoint.");
2758
+ setState("error");
2759
+ setTimeout(() => exit(), 500);
2760
+ return;
2761
+ }
2762
+ setProject(projectResult.data ?? null);
2763
+
2764
+ const countResult = await api.getProjectDatasetCount(modelId);
2765
+ if (!active) return;
2766
+ if (countResult.ok) setDatasetCount(countResult.data ?? null);
2767
+
2768
+ // If the selected model id is a UUID, it's a training job (a deployed
2769
+ // fine-tuned checkpoint). Hydrate it so we can show a friendly name
2770
+ // instead of just the UUID.
2771
+ const selected = projectResult.data?.selected_model_id ?? "";
2772
+ if (UUID_REGEX.test(selected)) {
2773
+ const jobResult = await api.getJob(selected);
2774
+ if (!active) return;
2775
+ if (jobResult.ok) setActiveJob(jobResult.data ?? null);
2776
+ }
2777
+
2778
+ setState("done");
2779
+ setTimeout(() => exit(), 500);
2780
+ })();
2781
+ return () => {
2782
+ active = false;
2783
+ };
2784
+ }, [modelId, exit]);
2785
+
2786
+ if (state === "loading") return <Loading message={`Loading model endpoint ${modelId}...`} />;
2787
+ if (state === "error") return <ErrorMessage error={error} />;
2788
+ if (!project) return <ErrorMessage error="Model endpoint response was empty." />;
2789
+
2790
+ const selected = project.selected_model_id ?? "";
2791
+ const selectedDisplay = (() => {
2792
+ if (!selected) return "—";
2793
+ if (activeJob) {
2794
+ const parts: string[] = [];
2795
+ if (activeJob.model_name) parts.push(activeJob.model_name);
2796
+ if (activeJob.base_model) parts.push(`base: ${activeJob.base_model}`);
2797
+ const tail = parts.length ? ` (${parts.join(", ")})` : "";
2798
+ return `${shortJobId(selected)}${tail}`;
2799
+ }
2800
+ // Stock model id (or UUID we couldn't resolve) — render as-is.
2801
+ return UUID_REGEX.test(selected) ? shortJobId(selected) : selected;
2802
+ })();
2803
+
2804
+ const rows: DetailRow[] = [
2805
+ { label: "ID", value: formatNonEmpty(project.id) },
2806
+ { label: "Name", value: formatNonEmpty(project.name) },
2807
+ { label: "Description", value: formatNonEmpty(project.description) },
2808
+ { label: "Icon", value: formatNonEmpty(project.icon) },
2809
+ { label: "Repo", value: formatNonEmpty(project.repo) },
2810
+ { label: "Selected model", value: selectedDisplay },
2811
+ { label: "Created", value: formatTimestamp(project.created_at) },
2812
+ { label: "Updated", value: formatTimestamp(project.updated_at) },
2813
+ ];
2814
+ if (datasetCount) {
2815
+ rows.push(
2816
+ { label: "Datasets", value: formatNumber(datasetCount.dataset_count) },
2817
+ {
2818
+ label: "Can delete",
2819
+ value: formatBoolean(datasetCount.can_delete),
2820
+ valueColor: datasetCount.can_delete ? "green" : "yellow",
2821
+ }
2822
+ );
2823
+ }
2824
+
2825
+ return (
2826
+ <DetailView
2827
+ rows={rows}
2828
+ footer={`Use \`pioneer model endpoints get ${modelId} --json\` for the raw JSON payload.`}
2829
+ />
2830
+ );
2831
+ }
2832
+
2833
+ // ─────────────────────────────────────────────────────────────────────────────
2834
+ // Model Endpoint Quality Metrics Command (compact key/value layout)
2835
+ // ─────────────────────────────────────────────────────────────────────────────
2836
+
2837
+ function ModelQualityMetricsCommand({ modelId }: { modelId: string }) {
2838
+ const { exit } = useApp();
2839
+ const [state, setState] = useState<"loading" | "done" | "error">("loading");
2840
+ const [data, setData] = useState<api.QualityMetricsResponse | null>(null);
2841
+ const [error, setError] = useState("");
2842
+
2843
+ useEffect(() => {
2844
+ let active = true;
2845
+ (async () => {
2846
+ const result = await api.getProjectQualityMetrics(modelId);
2847
+ if (!active) return;
2848
+ if (!result.ok) {
2849
+ setError(result.error ?? "Failed to load quality metrics.");
2850
+ setState("error");
2851
+ } else {
2852
+ setData(result.data ?? null);
2853
+ setState("done");
2854
+ }
2855
+ setTimeout(() => exit(), 500);
2856
+ })();
2857
+ return () => {
2858
+ active = false;
2859
+ };
2860
+ }, [modelId, exit]);
2861
+
2862
+ if (state === "loading") return <Loading message={`Loading quality metrics for ${modelId}...`} />;
2863
+ if (state === "error") return <ErrorMessage error={error} />;
2864
+ if (!data) return <ErrorMessage error="Quality metrics response was empty." />;
2865
+
2866
+ const rows: DetailRow[] = [
2867
+ { label: "Model ID", value: formatNonEmpty(data.project_id) },
2868
+ { label: "Total judged", value: formatNumber(data.total_judged) },
2869
+ { label: "Pass count", value: formatNumber(data.pass_count), valueColor: "green" },
2870
+ { label: "Fail count", value: formatNumber(data.fail_count), valueColor: data.fail_count > 0 ? "red" : undefined },
2871
+ { label: "Uncertain count", value: formatNumber(data.uncertain_count), valueColor: data.uncertain_count > 0 ? "yellow" : undefined },
2872
+ { label: "Pass rate", value: formatPercent(data.pass_rate), valueColor: "green" },
2873
+ { label: "Fail rate", value: formatPercent(data.fail_rate), valueColor: data.fail_rate > 0 ? "red" : undefined },
2874
+ ];
2875
+
2876
+ return (
2877
+ <DetailView
2878
+ rows={rows}
2879
+ footer={`Use \`pioneer model endpoints quality-metrics ${modelId} --json\` for the raw JSON payload.`}
2880
+ />
2881
+ );
2882
+ }
2883
+
2884
+ // ─────────────────────────────────────────────────────────────────────────────
2885
+ // Deploy Job Picker (interactive picker for `model endpoints deploy` with no --job)
2886
+ // ─────────────────────────────────────────────────────────────────────────────
2887
+
2888
+ function isJobDeployable(status?: string): boolean {
2889
+ const s = (status ?? "").trim().toLowerCase();
2890
+ return s === "complete" || s === "completed" || s === "succeeded" || s === "deployed";
2891
+ }
2892
+
2893
+ /**
2894
+ * Normalize a model identifier to a "family" key for matching.
2895
+ * Strips the org/namespace prefix, lowercases, and drops common
2896
+ * variant suffixes (-instruct, -chat, -base, -it, -hf) so that, e.g.,
2897
+ * "qwen/Qwen3-8B-Instruct" → "qwen3-8b"
2898
+ * "Qwen/Qwen3-8B" → "qwen3-8b"
2899
+ * "meta-llama/Llama-3.1-8B-Instruct" → "llama-3.1-8b"
2900
+ * are treated as the same family.
2901
+ */
2902
+ function modelFamilyKey(modelId?: string | null): string {
2903
+ if (!modelId) return "";
2904
+ const last = modelId.toLowerCase().split("/").pop() ?? "";
2905
+ return last
2906
+ .replace(/-instruct$/, "")
2907
+ .replace(/-chat$/, "")
2908
+ .replace(/-base$/, "")
2909
+ .replace(/-it$/, "")
2910
+ .replace(/-hf$/, "")
2911
+ .trim();
2912
+ }
2913
+
2914
+ function jobMatchesEndpoint(job: api.TrainingJob, selectedModelId?: string): boolean {
2915
+ if (!selectedModelId) return true;
2916
+ const want = modelFamilyKey(selectedModelId);
2917
+ const have = modelFamilyKey(job.base_model);
2918
+ if (!want || !have) return true;
2919
+ return want === have || want.startsWith(have) || have.startsWith(want);
2920
+ }
2921
+
2922
+ export function DeployJobPickerCommand({
2923
+ modelId,
2924
+ reason,
2925
+ showAll,
2926
+ }: {
2927
+ modelId: string;
2928
+ reason?: string;
2929
+ showAll?: boolean;
2930
+ }) {
2931
+ const { exit } = useApp();
2932
+ const { isRawModeSupported } = useStdin();
2933
+ const [phase, setPhase] = useState<"loading" | "picking" | "deploying" | "done" | "error">("loading");
2934
+ const [jobs, setJobs] = useState<api.TrainingJob[]>([]);
2935
+ const [endpointModel, setEndpointModel] = useState<string | undefined>(undefined);
2936
+ const [filterApplied, setFilterApplied] = useState<boolean>(false);
2937
+ const [filterSource, setFilterSource] = useState<"project" | "family" | "none">("none");
2938
+ const [error, setError] = useState("");
2939
+ const [searchQuery, setSearchQuery] = useState("");
2940
+ const [highlightIndex, setHighlightIndex] = useState(0);
2941
+ const [selectedJobId, setSelectedJobId] = useState<string | null>(null);
2942
+ const [deployResult, setDeployResult] = useState<unknown>(null);
2943
+
2944
+ useEffect(() => {
2945
+ let active = true;
2946
+ (async () => {
2947
+ // Run sequentially to avoid Bun concurrent-fetch keep-alive flakiness
2948
+ // ("socket connection was closed unexpectedly"). Retry once on transient
2949
+ // network errors.
2950
+ const isTransient = (err?: string) =>
2951
+ !!err && /socket|ECONNRESET|fetch failed|network|EAI_AGAIN|timeout/i.test(err);
2952
+
2953
+ const fetchWithRetry = async <T,>(fn: () => Promise<api.ApiResult<T>>): Promise<api.ApiResult<T>> => {
2954
+ let r = await fn();
2955
+ if (!r.ok && (r.status === 0 || isTransient(r.error))) {
2956
+ await new Promise((res) => setTimeout(res, 250));
2957
+ r = await fn();
2958
+ }
2959
+ return r;
2960
+ };
2961
+
2962
+ const sortByRecent = (list: api.TrainingJob[]) =>
2963
+ list.slice().sort((a, b) => {
2964
+ const at = Date.parse(a.completed_at ?? a.updated_at ?? a.created_at ?? "");
2965
+ const bt = Date.parse(b.completed_at ?? b.updated_at ?? b.created_at ?? "");
2966
+ if (Number.isNaN(at) || Number.isNaN(bt)) return 0;
2967
+ return bt - at;
2968
+ });
2969
+
2970
+ // 1. Resolve the endpoint. A hard 404/403 means the endpoint doesn't exist
2971
+ // on this backend — fail fast rather than open a picker for a target
2972
+ // that any subsequent deploy call will reject. Other errors (5xx,
2973
+ // transient network) degrade gracefully: we just can't apply the
2974
+ // family filter.
2975
+ const projectResult = await fetchWithRetry(() => api.getProject(modelId));
2976
+ if (!active) return;
2977
+ if (!projectResult.ok && (projectResult.status === 404 || projectResult.status === 403)) {
2978
+ const reason =
2979
+ projectResult.status === 403
2980
+ ? "You do not have access to this endpoint."
2981
+ : "This endpoint does not exist on the current backend.";
2982
+ setError(
2983
+ `${reason} (model id: ${modelId})\nList valid endpoints with: pioneer model endpoints list`
2984
+ );
2985
+ setPhase("error");
2986
+ setTimeout(() => exit(), 500);
2987
+ return;
2988
+ }
2989
+ const selected = projectResult.ok ? projectResult.data?.selected_model_id : undefined;
2990
+ setEndpointModel(selected ?? undefined);
2991
+
2992
+ // 2. Try server-side filter by project_id. Today most jobs have project_id=null
2993
+ // so this often returns []; we fall back to all-jobs + family match below.
2994
+ let list: api.TrainingJob[] = [];
2995
+ let didFilter = false;
2996
+ let filterSource: "project" | "family" | "none" = "none";
2997
+
2998
+ if (!showAll) {
2999
+ const scoped = await fetchWithRetry(() => api.listJobs({ project_id: modelId }));
3000
+ if (!active) return;
3001
+ if (scoped.ok) {
3002
+ const scopedDeployable = sortByRecent(
3003
+ (scoped.data?.training_jobs ?? []).filter((j) => isJobDeployable(j.status))
3004
+ );
3005
+ if (scopedDeployable.length > 0) {
3006
+ list = scopedDeployable;
3007
+ didFilter = true;
3008
+ filterSource = "project";
3009
+ }
3010
+ }
3011
+ }
3012
+
3013
+ // 3. Fall back to all jobs (and optionally family-match against the endpoint).
3014
+ if (list.length === 0) {
3015
+ const allResult = await fetchWithRetry(() => api.listJobs());
3016
+ if (!active) return;
3017
+ if (!allResult.ok) {
3018
+ setError(allResult.error ?? "Failed to load training jobs.");
3019
+ setPhase("error");
3020
+ setTimeout(() => exit(), 500);
3021
+ return;
3022
+ }
3023
+ const allDeployable = sortByRecent(
3024
+ (allResult.data?.training_jobs ?? []).filter((j) => isJobDeployable(j.status))
3025
+ );
3026
+
3027
+ if (!showAll && selected) {
3028
+ const familyMatched = allDeployable.filter((j) => jobMatchesEndpoint(j, selected));
3029
+ if (familyMatched.length > 0) {
3030
+ list = familyMatched;
3031
+ didFilter = true;
3032
+ filterSource = "family";
3033
+ } else {
3034
+ list = allDeployable;
3035
+ }
3036
+ } else {
3037
+ list = allDeployable;
3038
+ }
3039
+ }
3040
+
3041
+ setFilterApplied(didFilter);
3042
+ setFilterSource(filterSource);
3043
+ setJobs(list);
3044
+ setPhase("picking");
3045
+ })();
3046
+ return () => {
3047
+ active = false;
3048
+ };
3049
+ }, [exit, modelId, showAll]);
3050
+
3051
+ const matching = jobs.filter((job) => {
3052
+ const q = searchQuery.trim().toLowerCase();
3053
+ if (!q) return true;
3054
+ return (
3055
+ (job.id ?? "").toLowerCase().includes(q) ||
3056
+ (job.model_name ?? "").toLowerCase().includes(q) ||
3057
+ (job.base_model ?? "").toLowerCase().includes(q)
3058
+ );
3059
+ });
3060
+
3061
+ const deploy = async (jobId: string) => {
3062
+ setSelectedJobId(jobId);
3063
+ setPhase("deploying");
3064
+
3065
+ // The deploy endpoint requires `training_jobs.project_id == project_id`
3066
+ // (server-side WHERE clause). Most jobs are created with project_id=null,
3067
+ // so we PATCH first to link the job to the target project. The PATCH is
3068
+ // a no-op if the job is already linked.
3069
+ const job = jobs.find((j) => j.id === jobId);
3070
+ if (!job || job.project_id !== modelId) {
3071
+ const patch = await api.updateTrainingJob(jobId, { project_id: modelId });
3072
+ if (!patch.ok) {
3073
+ const detail =
3074
+ patch.status === 403
3075
+ ? "You do not have permission to assign this job to this project."
3076
+ : patch.error ?? "Failed to link job to project.";
3077
+ setError(`Could not assign job to project before deploy: ${detail}`);
3078
+ setPhase("error");
3079
+ setTimeout(() => exit(), 1500);
3080
+ return;
3081
+ }
3082
+ }
3083
+
3084
+ const result = await api.deployTrainingJobToProject(modelId, {
3085
+ training_job_id: jobId,
3086
+ ...(reason ? { reason } : {}),
3087
+ });
3088
+ if (!result.ok) {
3089
+ let msg = result.error ?? "Deployment failed.";
3090
+ if (result.status === 404) {
3091
+ msg =
3092
+ `Backend rejected the deploy with 404 "${result.error ?? "Resource not found."}".\n` +
3093
+ ` • Project ${modelId} and training job ${jobId} both exist and are linked.\n` +
3094
+ ` • Inspect job state: pioneer job get ${jobId}`;
3095
+ }
3096
+ setError(msg);
3097
+ setPhase("error");
3098
+ } else {
3099
+ // CLI-side enrichment: the deploy record's `base_model` is null for
3100
+ // training-job deploys (mutually-exclusive with `training_job_id` in
3101
+ // the current schema), but users want to see what base the fine-tune
3102
+ // is built on. Populate it from the picked job until the backend
3103
+ // change lands.
3104
+ const enriched =
3105
+ result.data && typeof result.data === "object" && job?.base_model
3106
+ ? { ...result.data, base_model: (result.data as { base_model?: string | null }).base_model ?? job.base_model }
3107
+ : result.data;
3108
+ setDeployResult(enriched);
3109
+ setPhase("done");
3110
+ }
3111
+ setTimeout(() => exit(), 1500);
3112
+ };
3113
+
3114
+ useInput(
3115
+ (input, key) => {
3116
+ if (phase !== "picking" || !matching.length) {
3117
+ if (key.return && phase === "picking" && !matching.length) {
3118
+ setError("No matching jobs.");
3119
+ }
3120
+ return;
3121
+ }
3122
+ if (key.upArrow) {
3123
+ setHighlightIndex((idx) => (idx === 0 ? matching.length - 1 : idx - 1));
3124
+ return;
3125
+ }
3126
+ if (key.downArrow) {
3127
+ setHighlightIndex((idx) => (idx === matching.length - 1 ? 0 : idx + 1));
3128
+ return;
3129
+ }
3130
+ if (key.return) {
3131
+ const selected = matching[highlightIndex];
3132
+ if (selected?.id) {
3133
+ void deploy(selected.id);
3134
+ }
3135
+ return;
3136
+ }
3137
+ if (key.backspace || key.delete) {
3138
+ setSearchQuery((q) => q.slice(0, -1));
3139
+ setHighlightIndex(0);
3140
+ return;
3141
+ }
3142
+ if (input && !key.ctrl && !key.meta) {
3143
+ setSearchQuery((q) => q + input);
3144
+ setHighlightIndex(0);
3145
+ }
3146
+ },
3147
+ { isActive: isRawModeSupported && phase === "picking" }
3148
+ );
3149
+
3150
+ if (!isRawModeSupported) {
3151
+ return (
3152
+ <Box flexDirection="column">
3153
+ <ErrorMessage error="Interactive job picker requires a TTY." />
3154
+ <Text>List trained jobs with `pioneer job list` and pass an explicit ID:</Text>
3155
+ <Text dimColor> pioneer model endpoints deploy {modelId} --job &lt;training-job-id&gt;</Text>
3156
+ </Box>
3157
+ );
3158
+ }
3159
+
3160
+ if (phase === "loading") return <Loading message="Loading deployable training jobs..." />;
3161
+ if (phase === "error") return <ErrorMessage error={error} />;
3162
+
3163
+ if (phase === "deploying") {
3164
+ return <Loading message={`Deploying job ${selectedJobId} to endpoint ${modelId}...`} />;
3165
+ }
3166
+
3167
+ if (phase === "done") {
3168
+ return (
3169
+ <Box flexDirection="column">
3170
+ <Success message={`Deployment initiated for endpoint ${modelId} from job ${selectedJobId}`} />
3171
+ {deployResult ? (
3172
+ <Text dimColor>{JSON.stringify(deployResult, null, 2)}</Text>
3173
+ ) : null}
3174
+ </Box>
3175
+ );
3176
+ }
3177
+
3178
+ if (jobs.length === 0) {
3179
+ return (
3180
+ <Box flexDirection="column">
3181
+ <ErrorMessage error="No deployable training jobs found." />
3182
+ <Text dimColor>Only jobs with status `complete` / `succeeded` / `deployed` are eligible.</Text>
3183
+ <Text dimColor>Run `pioneer job list` to inspect job statuses, or start training with `pioneer agent`.</Text>
3184
+ </Box>
3185
+ );
3186
+ }
3187
+
3188
+ const visible = matching.slice(0, 12);
3189
+ const offsetIndex = Math.max(0, Math.min(highlightIndex - visible.length + 1, matching.length - visible.length));
3190
+ const window = matching.slice(offsetIndex, offsetIndex + visible.length);
3191
+
3192
+ const navHint = "Type to filter · ↑/↓ navigate · Enter to deploy";
3193
+ const headerLine = (() => {
3194
+ const n = jobs.length;
3195
+ const noun = `job${n === 1 ? "" : "s"}`;
3196
+ if (showAll) {
3197
+ const tail = endpointModel ? ` Endpoint base model: "${endpointModel}".` : "";
3198
+ return `Showing all ${n} deployable ${noun} (--all).${tail} ${navHint}.`;
3199
+ }
3200
+ if (filterApplied && filterSource === "project") {
3201
+ return `Showing ${n} ${noun} scoped to this endpoint via project_id. ${navHint} · pass --all to see every job.`;
3202
+ }
3203
+ if (filterApplied && filterSource === "family" && endpointModel) {
3204
+ return `Showing ${n} ${noun} matching base model "${endpointModel}". ${navHint} · pass --all to disable family match.`;
3205
+ }
3206
+ if (endpointModel) {
3207
+ return `No jobs scoped to this endpoint and none match base model "${endpointModel}". Showing all ${n} deployable ${noun} as fallback. ${navHint}.`;
3208
+ }
3209
+ return `Showing ${n} deployable ${noun}. ${navHint}.`;
3210
+ })();
3211
+
3212
+ return (
3213
+ <Box flexDirection="column">
3214
+ <Text bold>Pick a training job to deploy to endpoint <Text color="cyan">{modelId}</Text>:</Text>
3215
+ <Text dimColor>{headerLine}</Text>
3216
+ <Text> </Text>
3217
+ {searchQuery ? (
3218
+ <Text>
3219
+ <Text dimColor>Filter: </Text>
3220
+ <Text>{searchQuery}</Text>
3221
+ </Text>
3222
+ ) : null}
3223
+ {window.length === 0 ? (
3224
+ <Text color="yellow">No jobs match `{searchQuery}`.</Text>
3225
+ ) : (
3226
+ window.map((job, idx) => {
3227
+ const realIndex = offsetIndex + idx;
3228
+ const isHighlighted = realIndex === highlightIndex;
3229
+ const idShort = shortJobId(job.id);
3230
+ const modelName = job.model_name?.trim() || "—";
3231
+ const baseModel = shortBaseModel(job.base_model);
3232
+ const status = job.status ?? "—";
3233
+ const completed = relativeTime(job.completed_at ?? job.updated_at ?? job.created_at);
3234
+ return (
3235
+ <Text key={job.id || realIndex} color={isHighlighted ? "cyan" : undefined} bold={isHighlighted}>
3236
+ {isHighlighted ? "▶ " : " "}
3237
+ {padCell(idShort, 8)} {padCell(modelName, 28)} {padCell(baseModel, 22)} <Text color={statusColor(status)}>{padCell(status, 10)}</Text> {completed}
3238
+ </Text>
3239
+ );
3240
+ })
3241
+ )}
3242
+ {error ? (
3243
+ <Box marginTop={1}>
3244
+ <Text color="red">{error}</Text>
3245
+ </Box>
3246
+ ) : null}
3247
+ </Box>
3248
+ );
3249
+ }
3250
+
3251
+ // ─────────────────────────────────────────────────────────────────────────────
3252
+ // Model Artifact Download Command (key/value layout for a download URL response)
3253
+ // ─────────────────────────────────────────────────────────────────────────────
3254
+
3255
+ function ModelArtifactDownloadCommand({ jobId }: { jobId: string }) {
3256
+ const { exit } = useApp();
3257
+ const [state, setState] = useState<"loading" | "done" | "error">("loading");
3258
+ const [data, setData] = useState<Record<string, unknown> | null>(null);
3259
+ const [error, setError] = useState("");
3260
+
3261
+ useEffect(() => {
3262
+ let active = true;
3263
+ (async () => {
3264
+ const result = await api.downloadModel(jobId);
3265
+ if (!active) return;
3266
+ if (!result.ok) {
3267
+ setError(result.error ?? "Failed to fetch model artifact download URL.");
3268
+ setState("error");
3269
+ } else {
3270
+ setData((result.data as Record<string, unknown> | undefined) ?? null);
3271
+ setState("done");
3272
+ }
3273
+ setTimeout(() => exit(), 500);
3274
+ })();
3275
+ return () => {
3276
+ active = false;
3277
+ };
3278
+ }, [jobId, exit]);
3279
+
3280
+ if (state === "loading") return <Loading message={`Fetching download URL for ${jobId}...`} />;
3281
+ if (state === "error") return <ErrorMessage error={error} />;
3282
+ if (!data) return <ErrorMessage error="Download response was empty." />;
3283
+
3284
+ const stringField = (key: string) => {
3285
+ const v = data[key];
3286
+ return typeof v === "string" && v.trim() ? v : undefined;
3287
+ };
3288
+ const numberField = (key: string) => {
3289
+ const v = data[key];
3290
+ return typeof v === "number" ? v : undefined;
3291
+ };
3292
+
3293
+ const downloadUrl = stringField("download_url") || stringField("url");
3294
+ const expiresAt = stringField("expires_at") || stringField("expiry");
3295
+ const sizeBytes = numberField("size") ?? numberField("size_bytes");
3296
+ const filename = stringField("filename") || stringField("file_name");
3297
+ const contentType = stringField("content_type") || stringField("mime_type");
3298
+
3299
+ const rows: DetailRow[] = [
3300
+ { label: "Job ID", value: jobId },
3301
+ { label: "Filename", value: formatNonEmpty(filename) },
3302
+ { label: "Content type", value: formatNonEmpty(contentType) },
3303
+ { label: "Size", value: sizeBytes !== undefined ? `${sizeBytes.toLocaleString()} bytes` : "—" },
3304
+ { label: "Expires", value: expiresAt ? formatTimestamp(expiresAt) : "—" },
3305
+ { label: "Download URL", value: downloadUrl || "—", valueColor: downloadUrl ? "cyan" : undefined },
3306
+ ];
3307
+
3308
+ return (
3309
+ <DetailView
3310
+ rows={rows}
3311
+ footer={
3312
+ downloadUrl
3313
+ ? `Tip: pipe the URL into curl, e.g. \`curl -L -o artifact.zip "$URL"\` · use \`--json\` for raw JSON`
3314
+ : `Use \`pioneer model artifacts download ${jobId} --json\` for the raw JSON payload.`
3315
+ }
3316
+ />
3317
+ );
3318
+ }
3319
+
2114
3320
  // ─────────────────────────────────────────────────────────────────────────────
2115
3321
  // Job Logs Command (prettified output)
2116
3322
  // ─────────────────────────────────────────────────────────────────────────────
@@ -2305,469 +3511,202 @@ function GenerateCommand<T extends GenerateResult>({
2305
3511
  {savedDatasetId && (
2306
3512
  <Text color="green">Saved to remote: {savedDatasetId}</Text>
2307
3513
  )}
2308
- {remoteSaveFailed && (
2309
- <Text color="yellow">⚠ Remote save failed (check server logs). Saved locally instead.</Text>
2310
- )}
2311
- {savedPath && (
2312
- <Text color="cyan">Saved to: {savedPath}</Text>
2313
- )}
2314
- </Box>
2315
- );
2316
- }
2317
-
2318
- // ─────────────────────────────────────────────────────────────────────────────
2319
- // Helper: Infer format from file extension
2320
- // ─────────────────────────────────────────────────────────────────────────────
2321
-
2322
- function inferFormatFromPath(
2323
- path: string | undefined,
2324
- defaultFormat: string = "jsonl"
2325
- ): "csv" | "jsonl" | "parquet" {
2326
- if (!path) return defaultFormat as "csv" | "jsonl" | "parquet";
2327
-
2328
- const ext = path.toLowerCase().split(".").pop();
2329
- if (ext === "csv" || ext === "jsonl" || ext === "parquet") {
2330
- return ext;
2331
- }
2332
- return defaultFormat as "csv" | "jsonl" | "parquet";
2333
- }
2334
-
2335
- // ─────────────────────────────────────────────────────────────────────────────
2336
- // Dataset List Command (shows both remote and local)
2337
- // ─────────────────────────────────────────────────────────────────────────────
2338
-
2339
- function DatasetListCommand() {
2340
- const { exit } = useApp();
2341
- const [state, setState] = useState<"loading" | "done">("loading");
2342
- const [remoteData, setRemoteData] = useState<api.DatasetListResponse | null>(null);
2343
- const [remoteError, setRemoteError] = useState<string | null>(null);
2344
- const [localDatasets, setLocalDatasets] = useState<LocalDataset[]>([]);
2345
-
2346
- useEffect(() => {
2347
- (async () => {
2348
- // Fetch remote and local datasets
2349
- const result = await api.listDatasets();
2350
- const local = listLocalDatasets();
2351
- setLocalDatasets(local);
2352
-
2353
- if (result.ok) {
2354
- setRemoteData(result.data ?? null);
2355
- } else {
2356
- setRemoteError(result.error ?? "Unknown error");
2357
- }
2358
- setState("done");
2359
- setTimeout(() => exit(), 500);
2360
- })();
2361
- }, [exit]);
2362
-
2363
- if (state === "loading") {
2364
- return <Loading />;
2365
- }
2366
-
2367
- const remoteDatasets = remoteData?.datasets ?? [];
2368
-
2369
- return (
2370
- <Box flexDirection="column">
2371
- <Text bold color="cyan">Remote Datasets {remoteError ? "" : `(${remoteDatasets.length})`}</Text>
2372
- {remoteError ? (
2373
- <Box flexDirection="column">
2374
- {remoteError.split("\n").map((line, idx) => (
2375
- <Text key={idx} color="red"> {line}</Text>
2376
- ))}
2377
- </Box>
2378
- ) : remoteDatasets.length === 0 ? (
2379
- <Text dimColor> No remote datasets</Text>
2380
- ) : (
2381
- remoteDatasets.map((ds) => (
2382
- <Box key={ds.id} flexDirection="column">
2383
- <Text>
2384
- {" "}<Text color="yellow">{ds.dataset_name}:{ds.version_number || "v1"}</Text> <Text dimColor>({ds.dataset_type}, {ds.sample_size} examples)</Text>
2385
- </Text>
2386
- <Text dimColor> {ds.id}</Text>
2387
- </Box>
2388
- ))
2389
- )}
2390
- <Text> </Text>
2391
- <Text bold color="cyan">Local Datasets ({localDatasets.length})</Text>
2392
- {localDatasets.length === 0 ? (
2393
- <Text dimColor> No local datasets in ./datasets/</Text>
2394
- ) : (
2395
- localDatasets.map((ds) => (
2396
- <Text key={ds.id}>
2397
- {" "}<Text color="green">{ds.name}</Text> <Text dimColor>({ds.type}, {ds.sample_size} examples)</Text>
2398
- </Text>
2399
- ))
2400
- )}
2401
- </Box>
2402
- );
2403
- }
2404
-
2405
- // ─────────────────────────────────────────────────────────────────────────────
2406
- // Job Create Command (with auto-upload for local datasets)
2407
- // ─────────────────────────────────────────────────────────────────────────────
2408
-
2409
- interface ParsedDataset {
2410
- type: "local" | "remote";
2411
- name?: string; // for remote name:version format
2412
- version?: string; // for remote name:version format
2413
- id?: string; // for remote UUID format
2414
- }
2415
-
2416
- interface JobCreateCommandProps {
2417
- modelName: string;
2418
- datasets: ParsedDataset[];
2419
- baseModel?: string;
2420
- epochs?: number;
2421
- }
2422
-
2423
- interface DatasetUploadStatus {
2424
- name: string;
2425
- path: string;
2426
- status: "pending" | "uploading" | "done" | "error";
2427
- error?: string;
2428
- uploadedRef?: api.DatasetRef;
2429
- }
2430
-
2431
- // UUID pattern: 8-4-4-4-12 hex characters
2432
- const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2433
-
2434
- function isUUID(str: string): boolean {
2435
- return UUID_PATTERN.test(str);
2436
- }
2437
-
2438
- function parseDatasetString(ds: string): ParsedDataset | { error: string } {
2439
- const trimmed = ds.trim();
2440
-
2441
- if (trimmed.startsWith("local:")) {
2442
- const name = trimmed.slice(6); // remove "local:"
2443
- if (!name) {
2444
- return { error: `Invalid local dataset format: ${ds}. Use local:<name>` };
2445
- }
2446
- return { type: "local", name };
2447
- }
2448
-
2449
- if (trimmed.startsWith("remote:")) {
2450
- const rest = trimmed.slice(7); // remove "remote:"
2451
-
2452
- // Check if it's a UUID
2453
- if (isUUID(rest)) {
2454
- return { type: "remote", id: rest };
2455
- }
2456
-
2457
- // Otherwise parse as name:version
2458
- const colonIndex = rest.lastIndexOf(":");
2459
- if (colonIndex === -1) {
2460
- // No version, default to latest
2461
- return { type: "remote", name: rest, version: "latest" };
2462
- }
2463
- return {
2464
- type: "remote",
2465
- name: rest.slice(0, colonIndex),
2466
- version: rest.slice(colonIndex + 1),
2467
- };
2468
- }
2469
-
2470
- return { error: `Invalid dataset format: ${ds}. Use remote:<name>:<version>, remote:<uuid>, or local:<name>` };
2471
- }
2472
-
2473
- function resolveLocalDatasetPath(name: string): string | null {
2474
- // Check if it's a local dataset by name (in ./datasets/ directory)
2475
- const localPath = path.join(DATASETS_DIR, `${name}.json`);
2476
- if (fs.existsSync(localPath)) {
2477
- return localPath;
2478
- }
2479
- // Check if it's a direct file path
2480
- if (fs.existsSync(name) && (name.endsWith(".json") || name.endsWith(".jsonl"))) {
2481
- return name;
2482
- }
2483
- return null;
2484
- }
2485
-
2486
- function getLocalDatasetType(filePath: string): "ner" | "classification" | "custom" {
2487
- try {
2488
- const content = fs.readFileSync(filePath, "utf-8");
2489
- const parsed = JSON.parse(content);
2490
- const data = Array.isArray(parsed) ? parsed : parsed.data ?? [];
2491
- if (parsed.task_type) {
2492
- return parsed.task_type as "ner" | "classification" | "custom";
2493
- }
2494
- const firstItem = data[0] as Record<string, unknown> | undefined;
2495
- if (firstItem?.spans) return "ner";
2496
- if (firstItem?.label) return "classification";
2497
- return "custom";
2498
- } catch {
2499
- return "custom";
2500
- }
2501
- }
2502
-
2503
- function convertJsonToJsonl(filePath: string): string {
2504
- // Convert JSON array to JSONL format for upload
2505
- const content = fs.readFileSync(filePath, "utf-8");
2506
- const parsed = JSON.parse(content);
2507
- const data = Array.isArray(parsed) ? parsed : parsed.data ?? [];
2508
-
2509
- // Create temp JSONL file
2510
- const tempPath = filePath.replace(".json", ".jsonl");
2511
- const jsonlContent = data.map((item: unknown) => JSON.stringify(item)).join("\n");
2512
- fs.writeFileSync(tempPath, jsonlContent);
2513
- return tempPath;
2514
- }
2515
-
2516
- function JobCreateCommand({ modelName, datasets, baseModel, epochs }: JobCreateCommandProps) {
2517
- const { exit } = useApp();
2518
- const [state, setState] = useState<"resolving" | "uploading" | "creating" | "done" | "error">("resolving");
2519
- const [uploadStatuses, setUploadStatuses] = useState<DatasetUploadStatus[]>([]);
2520
- const [jobResult, setJobResult] = useState<api.TrainingJob | null>(null);
2521
- const [error, setError] = useState("");
2522
-
2523
- useEffect(() => {
2524
- (async () => {
2525
- // Separate local and remote datasets
2526
- const localDatasets = datasets.filter((d): d is ParsedDataset & { type: "local"; name: string } =>
2527
- d.type === "local" && !!d.name
2528
- );
2529
- const remoteByNameVersion = datasets.filter((d): d is ParsedDataset & { type: "remote"; name: string } =>
2530
- d.type === "remote" && !!d.name
2531
- );
2532
- const remoteByUUID = datasets.filter((d): d is ParsedDataset & { type: "remote"; id: string } =>
2533
- d.type === "remote" && !!d.id
2534
- );
2535
-
2536
- // Resolve UUIDs to name:version
2537
- const remoteDatasets: api.DatasetRef[] = remoteByNameVersion.map(d => ({
2538
- name: d.name,
2539
- version: d.version ?? "latest"
2540
- }));
2541
-
2542
- if (remoteByUUID.length > 0) {
2543
- // Fetch dataset list to resolve UUIDs
2544
- const listResult = await api.listDatasets();
2545
- if (!listResult.ok || !listResult.data) {
2546
- setError(`Failed to resolve dataset UUIDs: ${listResult.error ?? "Unknown error"}`);
2547
- setState("error");
2548
- setTimeout(() => exit(), 500);
2549
- return;
2550
- }
2551
-
2552
- for (const uuidDataset of remoteByUUID) {
2553
- const found = listResult.data.datasets.find(ds => ds.id === uuidDataset.id);
2554
- if (!found) {
2555
- setError(`Dataset not found with UUID: ${uuidDataset.id}`);
2556
- setState("error");
2557
- setTimeout(() => exit(), 500);
2558
- return;
2559
- }
2560
- remoteDatasets.push({
2561
- name: found.dataset_name,
2562
- version: found.version_number ?? "latest",
2563
- });
2564
- }
2565
- }
2566
-
2567
- // Validate local datasets exist
2568
- const statuses: DatasetUploadStatus[] = [];
2569
- for (const local of localDatasets) {
2570
- const localPath = resolveLocalDatasetPath(local.name);
2571
- if (!localPath) {
2572
- setError(`Local dataset not found: ${local.name}`);
2573
- setState("error");
2574
- setTimeout(() => exit(), 500);
2575
- return;
2576
- }
2577
- statuses.push({ name: local.name, path: localPath, status: "pending" });
2578
- }
2579
-
2580
- if (statuses.length === 0) {
2581
- // No local datasets, proceed directly to job creation
2582
- setState("creating");
2583
- const result = await api.createJob({
2584
- model_name: modelName,
2585
- datasets: remoteDatasets,
2586
- base_model: baseModel,
2587
- nr_epochs: epochs,
2588
- });
2589
- if (result.ok && result.data) {
2590
- setJobResult(result.data);
2591
- setState("done");
2592
- } else {
2593
- setError(result.error ?? "Failed to create job");
2594
- setState("error");
2595
- }
2596
- setTimeout(() => exit(), 500);
2597
- return;
2598
- }
2599
-
2600
- // Upload local datasets
2601
- setUploadStatuses(statuses);
2602
- setState("uploading");
2603
-
2604
- const uploadedDatasets: api.DatasetRef[] = [...remoteDatasets];
2605
- let hasError = false;
3514
+ {remoteSaveFailed && (
3515
+ <Text color="yellow">⚠ Remote save failed (check server logs). Saved locally instead.</Text>
3516
+ )}
3517
+ {savedPath && (
3518
+ <Text color="cyan">Saved to: {savedPath}</Text>
3519
+ )}
3520
+ </Box>
3521
+ );
3522
+ }
2606
3523
 
2607
- for (let i = 0; i < statuses.length; i++) {
2608
- const status = statuses[i];
3524
+ // ─────────────────────────────────────────────────────────────────────────────
3525
+ // Helper: Infer format from file extension
3526
+ // ─────────────────────────────────────────────────────────────────────────────
2609
3527
 
2610
- // Update status to uploading
2611
- setUploadStatuses(prev => prev.map((s, idx) =>
2612
- idx === i ? { ...s, status: "uploading" as const } : s
2613
- ));
3528
+ function inferFormatFromPath(
3529
+ path: string | undefined,
3530
+ defaultFormat: string = "jsonl"
3531
+ ): "csv" | "jsonl" | "parquet" {
3532
+ if (!path) return defaultFormat as "csv" | "jsonl" | "parquet";
2614
3533
 
2615
- try {
2616
- // Convert JSON to JSONL if needed
2617
- let uploadPath = status.path;
2618
- let isTemp = false;
2619
- if (status.path.endsWith(".json")) {
2620
- uploadPath = convertJsonToJsonl(status.path);
2621
- isTemp = true;
2622
- }
3534
+ const ext = path.toLowerCase().split(".").pop();
3535
+ if (ext === "csv" || ext === "jsonl" || ext === "parquet") {
3536
+ return ext;
3537
+ }
3538
+ return defaultFormat as "csv" | "jsonl" | "parquet";
3539
+ }
2623
3540
 
2624
- const result = await api.uploadDataset(uploadPath, {
2625
- dataset_name: status.name,
2626
- dataset_type: getLocalDatasetType(status.path),
2627
- format: "jsonl",
2628
- });
3541
+ // ─────────────────────────────────────────────────────────────────────────────
3542
+ // Dataset List Command (tabular: remote + local in one table)
3543
+ // ─────────────────────────────────────────────────────────────────────────────
2629
3544
 
2630
- // Clean up temp file
2631
- if (isTemp && uploadPath !== status.path) {
2632
- try { fs.unlinkSync(uploadPath); } catch { /* ignore */ }
2633
- }
3545
+ interface DatasetRow {
3546
+ source: string;
3547
+ id: string;
3548
+ name: string;
3549
+ version: string;
3550
+ type: string;
3551
+ samples: string;
3552
+ created: string;
3553
+ [key: string]: string;
3554
+ }
2634
3555
 
2635
- if (result.ok && result.data) {
2636
- const uploadedRef: api.DatasetRef = {
2637
- name: result.data.dataset_name,
2638
- version: result.data.version_number ?? "latest",
2639
- };
2640
- uploadedDatasets.push(uploadedRef);
2641
- setUploadStatuses(prev => prev.map((s, idx) =>
2642
- idx === i ? { ...s, status: "done" as const, uploadedRef } : s
2643
- ));
2644
- } else {
2645
- hasError = true;
2646
- setUploadStatuses(prev => prev.map((s, idx) =>
2647
- idx === i ? { ...s, status: "error" as const, error: result.error } : s
2648
- ));
2649
- }
2650
- } catch (err) {
2651
- hasError = true;
2652
- setUploadStatuses(prev => prev.map((s, idx) =>
2653
- idx === i ? { ...s, status: "error" as const, error: err instanceof Error ? err.message : String(err) } : s
2654
- ));
2655
- }
2656
- }
3556
+ function shortDatasetId(id?: string): string {
3557
+ if (!id) return "—";
3558
+ return id.length > 8 ? id.slice(0, 8) : id;
3559
+ }
2657
3560
 
2658
- if (hasError) {
2659
- setError("Some datasets failed to upload");
2660
- setState("error");
2661
- setTimeout(() => exit(), 500);
2662
- return;
2663
- }
3561
+ function formatSampleCount(n?: number): string {
3562
+ if (typeof n !== "number" || Number.isNaN(n)) return "—";
3563
+ return n.toLocaleString();
3564
+ }
2664
3565
 
2665
- // Create the job with all datasets
2666
- setState("creating");
3566
+ const DATASET_COLUMNS: TableColumn<DatasetRow>[] = [
3567
+ { key: "source", header: "SOURCE", minWidth: 6, maxWidth: 6, color: (row) => (row.source === "remote" ? "cyan" : "green") },
3568
+ { key: "id", header: "ID", minWidth: 8, maxWidth: 8 },
3569
+ { key: "name", header: "NAME", minWidth: 10, maxWidth: 38, flexible: true },
3570
+ { key: "version", header: "VERSION", minWidth: 4, maxWidth: 8 },
3571
+ { key: "type", header: "TYPE", minWidth: 4, maxWidth: 14, flexible: true },
3572
+ { key: "samples", header: "SAMPLES", minWidth: 7, maxWidth: 10 },
3573
+ { key: "created", header: "CREATED", minWidth: 7, maxWidth: 10 },
3574
+ ];
2667
3575
 
2668
- const result = await api.createJob({
2669
- model_name: modelName,
2670
- datasets: uploadedDatasets,
2671
- base_model: baseModel,
2672
- nr_epochs: epochs,
2673
- });
3576
+ function DatasetListCommand() {
3577
+ const { exit } = useApp();
3578
+ const [state, setState] = useState<"loading" | "done">("loading");
3579
+ const [remoteData, setRemoteData] = useState<api.DatasetListResponse | null>(null);
3580
+ const [remoteError, setRemoteError] = useState<string | null>(null);
3581
+ const [localDatasets, setLocalDatasets] = useState<LocalDataset[]>([]);
2674
3582
 
2675
- if (result.ok && result.data) {
2676
- setJobResult(result.data);
2677
- setState("done");
2678
- } else {
2679
- setError(result.error ?? "Failed to create job");
2680
- setState("error");
2681
- }
3583
+ useEffect(() => {
3584
+ (async () => {
3585
+ const result = await api.listDatasets();
3586
+ setLocalDatasets(listLocalDatasets());
3587
+ if (result.ok) setRemoteData(result.data ?? null);
3588
+ else setRemoteError(result.error ?? "Unknown error");
3589
+ setState("done");
2682
3590
  setTimeout(() => exit(), 500);
2683
3591
  })();
2684
- }, [modelName, datasets, baseModel, epochs, exit]);
2685
-
2686
- if (state === "resolving") {
2687
- return <Loading message="Resolving datasets..." />;
2688
- }
2689
-
2690
- if (state === "uploading") {
2691
- return (
2692
- <Box flexDirection="column">
2693
- <Text>
2694
- <Text color="blue"><Spinner type="dots" /></Text>
2695
- {" "}Uploading local datasets...
2696
- </Text>
2697
- {uploadStatuses.map((status, idx) => (
2698
- <Box key={idx}>
2699
- <Text>
2700
- {" "}
2701
- {status.status === "pending" && <Text color="gray">○</Text>}
2702
- {status.status === "uploading" && <Text color="yellow"><Spinner type="dots" /></Text>}
2703
- {status.status === "done" && <Text color="green">✓</Text>}
2704
- {status.status === "error" && <Text color="red">✗</Text>}
2705
- {" "}{status.name}
2706
- {status.status === "done" && status.uploadedRef && (
2707
- <Text color="gray"> → {status.uploadedRef.name}:{status.uploadedRef.version}</Text>
2708
- )}
2709
- {status.status === "error" && status.error && (
2710
- <Text color="red"> ({status.error})</Text>
2711
- )}
2712
- </Text>
2713
- </Box>
2714
- ))}
2715
- </Box>
2716
- );
2717
- }
3592
+ }, [exit]);
2718
3593
 
2719
- if (state === "creating") {
2720
- return <Loading message="Creating training job..." />;
2721
- }
3594
+ if (state === "loading") return <Loading />;
2722
3595
 
2723
- if (state === "error") {
3596
+ const remoteDatasets = remoteData?.datasets ?? [];
3597
+ const remoteRows: DatasetRow[] = remoteDatasets.map((ds) => ({
3598
+ source: "remote",
3599
+ id: shortDatasetId(ds.id),
3600
+ name: ds.dataset_name || "—",
3601
+ version: ds.version_number ? `v${ds.version_number}` : "v1",
3602
+ type: ds.dataset_type || "—",
3603
+ samples: formatSampleCount(ds.sample_size),
3604
+ created: relativeTime(ds.created_at),
3605
+ }));
3606
+ const localRows: DatasetRow[] = localDatasets.map((ds) => ({
3607
+ source: "local",
3608
+ id: "—",
3609
+ name: ds.name,
3610
+ version: "—",
3611
+ type: ds.type || "—",
3612
+ samples: formatSampleCount(ds.sample_size),
3613
+ created: "—",
3614
+ }));
3615
+ const rows = [...remoteRows, ...localRows];
3616
+
3617
+ if (rows.length === 0 && !remoteError) {
2724
3618
  return (
2725
3619
  <Box flexDirection="column">
2726
- {uploadStatuses.length > 0 && uploadStatuses.some(s => s.status === "done") && (
2727
- <Box flexDirection="column" marginBottom={1}>
2728
- <Text color="green">✓ Uploaded datasets:</Text>
2729
- {uploadStatuses.filter(s => s.status === "done").map((status, idx) => (
2730
- <Text key={idx} color="gray">
2731
- {" "}{status.name} → {status.uploadedRef?.name}:{status.uploadedRef?.version}
2732
- </Text>
2733
- ))}
2734
- </Box>
2735
- )}
2736
- <ErrorMessage error={error} />
3620
+ <Text>No datasets found.</Text>
3621
+ <Text dimColor>Use the Pioneer web app at agent.pioneer.ai to create datasets, or drop JSON/JSONL files in ./datasets/.</Text>
2737
3622
  </Box>
2738
3623
  );
2739
3624
  }
2740
3625
 
2741
3626
  return (
2742
3627
  <Box flexDirection="column">
2743
- {uploadStatuses.length > 0 && (
2744
- <Box flexDirection="column" marginBottom={1}>
2745
- <Text color="green">✓ Uploaded local datasets:</Text>
2746
- {uploadStatuses.map((status, idx) => (
2747
- <Text key={idx} color="gray">
2748
- {" "}{status.name} → {status.uploadedRef?.name}:{status.uploadedRef?.version}
2749
- </Text>
3628
+ {remoteError ? (
3629
+ <Box flexDirection="column" marginBottom={rows.length ? 1 : 0}>
3630
+ {remoteError.split("\n").map((line, idx) => (
3631
+ <Text key={idx} color="red">{line}</Text>
2750
3632
  ))}
2751
3633
  </Box>
2752
- )}
2753
- <Success message="Training job created" />
2754
- {jobResult && (
2755
- <Box flexDirection="column">
2756
- <Text>
2757
- {" "}Job ID: <Text color="cyan">{jobResult.id}</Text>
2758
- </Text>
2759
- <Text>
2760
- {" "}Model: <Text color="yellow">{jobResult.model_name}</Text>
2761
- </Text>
2762
- <Text>
2763
- {" "}Status: <Text color="blue">{jobResult.status}</Text>
2764
- </Text>
2765
- </Box>
2766
- )}
3634
+ ) : null}
3635
+ {rows.length > 0 ? (
3636
+ <DataTable
3637
+ columns={DATASET_COLUMNS}
3638
+ rows={rows}
3639
+ footer={`${remoteRows.length} remote · ${localRows.length} local · use \`pioneer dataset get <name[:version]>\` for details · use \`--json\` for raw JSON`}
3640
+ />
3641
+ ) : null}
2767
3642
  </Box>
2768
3643
  );
2769
3644
  }
2770
3645
 
3646
+ // ─────────────────────────────────────────────────────────────────────────────
3647
+ // Dataset Get Command (single-resource key/value layout)
3648
+ // ─────────────────────────────────────────────────────────────────────────────
3649
+
3650
+ function datasetToDetailRows(ds: api.Dataset): DetailRow[] {
3651
+ const rows: DetailRow[] = [
3652
+ { label: "ID", value: formatNonEmpty(ds.id) },
3653
+ { label: "Name", value: formatNonEmpty(ds.dataset_name) },
3654
+ { label: "Version", value: ds.version_number ? `v${ds.version_number}` : "—" },
3655
+ { label: "Type", value: formatNonEmpty(ds.dataset_type) },
3656
+ { label: "Status", value: formatNonEmpty(ds.status), valueColor: statusColor(ds.status) },
3657
+ { label: "Samples", value: formatSampleCount(ds.sample_size) },
3658
+ { label: "Train ratio", value: ds.train_ratio != null ? formatPercent(ds.train_ratio) : "—" },
3659
+ { label: "Visibility", value: formatNonEmpty(ds.visibility) },
3660
+ { label: "Labels", value: formatLabels(ds.labels) },
3661
+ { label: "Annotation status", value: formatNonEmpty(ds.annotation_status) },
3662
+ { label: "Project ID", value: formatNonEmpty(ds.project_id) },
3663
+ { label: "Root dataset ID", value: formatNonEmpty(ds.root_dataset_id) },
3664
+ { label: "Created", value: formatTimestamp(ds.created_at) },
3665
+ { label: "Updated", value: formatTimestamp(ds.updated_at) },
3666
+ ];
3667
+ if (ds.processing_error) {
3668
+ rows.push({ label: "Error", value: ds.processing_error, valueColor: "red" });
3669
+ }
3670
+ return rows;
3671
+ }
3672
+
3673
+ function DatasetGetCommand({ dataset }: { dataset: api.DatasetRef }) {
3674
+ const { exit } = useApp();
3675
+ const [state, setState] = useState<"loading" | "done" | "error">("loading");
3676
+ const [data, setData] = useState<api.Dataset | null>(null);
3677
+ const [error, setError] = useState("");
3678
+
3679
+ useEffect(() => {
3680
+ let active = true;
3681
+ (async () => {
3682
+ const result = await api.getDataset(dataset);
3683
+ if (!active) return;
3684
+ if (!result.ok) {
3685
+ setError(result.error ?? "Failed to load dataset.");
3686
+ setState("error");
3687
+ } else {
3688
+ setData(result.data ?? null);
3689
+ setState("done");
3690
+ }
3691
+ setTimeout(() => exit(), 500);
3692
+ })();
3693
+ return () => {
3694
+ active = false;
3695
+ };
3696
+ }, [dataset.name, dataset.version, exit]);
3697
+
3698
+ if (state === "loading") return <Loading message={`Loading dataset ${dataset.name}:${dataset.version}...`} />;
3699
+ if (state === "error") return <ErrorMessage error={error} />;
3700
+ if (!data) return <ErrorMessage error="Dataset response was empty." />;
3701
+
3702
+ return (
3703
+ <DetailView
3704
+ rows={datasetToDetailRows(data)}
3705
+ footer={`Use \`pioneer dataset get ${dataset.name}:${dataset.version} --json\` for the raw JSON payload.`}
3706
+ />
3707
+ );
3708
+ }
3709
+
2771
3710
  // ─────────────────────────────────────────────────────────────────────────────
2772
3711
  // Dataset Download Command
2773
3712
  // ─────────────────────────────────────────────────────────────────────────────
@@ -2943,13 +3882,6 @@ function TrainedModelCard({ model, index }: TrainedModelCardProps) {
2943
3882
  <Box width={24}>
2944
3883
  <Text color="blue">{datasetInfo.substring(0, 22)}</Text>
2945
3884
  </Box>
2946
- <Box width={14}>
2947
- <Text>
2948
- <Text color="yellow">{model.nr_epochs}</Text>
2949
- <Text dimColor>e </Text>
2950
- <Text color="yellow">{model.learning_rate}</Text>
2951
- </Text>
2952
- </Box>
2953
3885
  <Box width={30}>
2954
3886
  <Text color="green">{metricsDisplay || "N/A"}</Text>
2955
3887
  </Box>
@@ -2960,11 +3892,6 @@ function TrainedModelCard({ model, index }: TrainedModelCardProps) {
2960
3892
  <Text dimColor>{formatDateShort(model.completed_at || model.started_at || model.created_at)}</Text>
2961
3893
  </Box>
2962
3894
  </Box>
2963
- {model.error_message && (
2964
- <Box marginLeft={2}>
2965
- <Text color="red">Error: {model.error_message}</Text>
2966
- </Box>
2967
- )}
2968
3895
  </Box>
2969
3896
  );
2970
3897
  }
@@ -3036,15 +3963,33 @@ function DeployedModelCard({ model, index }: DeployedModelCardProps) {
3036
3963
  interface ProjectModelCardProps {
3037
3964
  model: api.ProjectResponse;
3038
3965
  index: number;
3966
+ jobIndex?: Map<string, api.TrainingJob>;
3039
3967
  }
3040
3968
 
3041
- function ProjectModelCard({ model, index }: ProjectModelCardProps) {
3969
+ function ProjectModelCard({ model, index, jobIndex }: ProjectModelCardProps) {
3042
3970
  const formatDateShort = (dateStr: string | null | undefined) => {
3043
3971
  if (!dateStr) return "N/A";
3044
3972
  const date = new Date(dateStr);
3045
3973
  return date.toLocaleDateString() + " " + date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
3046
3974
  };
3047
3975
 
3976
+ // Drop "<org>/" prefix from HF model ids for compact list display.
3977
+ const stripOrg = (modelId: string) => {
3978
+ const idx = modelId.indexOf("/");
3979
+ return idx >= 0 ? modelId.slice(idx + 1) : modelId;
3980
+ };
3981
+ const selected = model.selected_model_id ?? "";
3982
+ const activeJob = selected && UUID_REGEX.test(selected) ? jobIndex?.get(selected) : undefined;
3983
+ const selectedDisplay = (() => {
3984
+ if (!selected) return "N/A";
3985
+ if (activeJob) {
3986
+ const name = activeJob.model_name ?? shortJobId(selected);
3987
+ const base = activeJob.base_model ? ` ← ${stripOrg(activeJob.base_model)}` : "";
3988
+ return `${name}${base}`;
3989
+ }
3990
+ return UUID_REGEX.test(selected) ? shortJobId(selected) : stripOrg(selected);
3991
+ })();
3992
+
3048
3993
  return (
3049
3994
  <Box flexDirection="column" marginTop={index > 0 ? 0 : 0}>
3050
3995
  <Box>
@@ -3056,8 +4001,8 @@ function ProjectModelCard({ model, index }: ProjectModelCardProps) {
3056
4001
  <Box width={38}>
3057
4002
  <Text dimColor>{model.id}</Text>
3058
4003
  </Box>
3059
- <Box width={24}>
3060
- <Text color="magenta">{model.selected_model_id || "N/A"}</Text>
4004
+ <Box width={36}>
4005
+ <Text color="magenta" wrap="truncate-end">{selectedDisplay}</Text>
3061
4006
  </Box>
3062
4007
  <Box width={18}>
3063
4008
  <Text dimColor>{formatDateShort(model.created_at)}</Text>
@@ -3086,21 +4031,41 @@ function ModelListCommand({ filter }: ModelListCommandProps) {
3086
4031
  const { exit } = useApp();
3087
4032
  const [state, setState] = useState<"loading" | "done" | "error">("loading");
3088
4033
  const [data, setData] = useState<api.AllModelsResponse | null>(null);
4034
+ const [jobIndex, setJobIndex] = useState<Map<string, api.TrainingJob>>(new Map());
3089
4035
  const [error, setError] = useState("");
3090
4036
 
3091
4037
  useEffect(() => {
3092
4038
  (async () => {
3093
4039
  const result = await api.listAllModels();
3094
- if (result.ok) {
3095
- setData(result.data ?? null);
3096
- setState("done");
3097
- } else {
4040
+ if (!result.ok) {
3098
4041
  setError(result.error ?? "Unknown error");
3099
4042
  setState("error");
4043
+ setTimeout(() => exit(), 500);
4044
+ return;
4045
+ }
4046
+ setData(result.data ?? null);
4047
+
4048
+ // For "registered" (Model Entries), each project's selected_model_id may
4049
+ // be a training-job UUID. Pull all jobs once and build a lookup so we
4050
+ // can render `model_name (base_model)` in place of the bare UUID.
4051
+ const projects = result.data?.projects ?? [];
4052
+ const needsHydration = projects.some((p) =>
4053
+ p.selected_model_id && UUID_REGEX.test(p.selected_model_id),
4054
+ );
4055
+ if (filter === "registered" && needsHydration) {
4056
+ const jobsResult = await api.listJobs();
4057
+ if (jobsResult.ok) {
4058
+ const map = new Map<string, api.TrainingJob>();
4059
+ for (const j of jobsResult.data?.training_jobs ?? []) {
4060
+ if (j.id) map.set(j.id, j);
4061
+ }
4062
+ setJobIndex(map);
4063
+ }
3100
4064
  }
4065
+ setState("done");
3101
4066
  setTimeout(() => exit(), 500);
3102
4067
  })();
3103
- }, [exit]);
4068
+ }, [exit, filter]);
3104
4069
 
3105
4070
  if (state === "loading") {
3106
4071
  return <Loading />;
@@ -3131,15 +4096,20 @@ function ModelListCommand({ filter }: ModelListCommandProps) {
3131
4096
  <Box width={38}>
3132
4097
  <Text bold dimColor>Model ID</Text>
3133
4098
  </Box>
3134
- <Box width={24}>
3135
- <Text bold dimColor>Base Model</Text>
4099
+ <Box width={36}>
4100
+ <Text bold dimColor>Active Model</Text>
3136
4101
  </Box>
3137
4102
  <Box width={18}>
3138
4103
  <Text bold dimColor>Created</Text>
3139
4104
  </Box>
3140
4105
  </Box>
3141
4106
  {data?.projects.map((model, index) => (
3142
- <ProjectModelCard key={model.id || index} model={model} index={index} />
4107
+ <ProjectModelCard
4108
+ key={model.id || index}
4109
+ model={model}
4110
+ index={index}
4111
+ jobIndex={jobIndex}
4112
+ />
3143
4113
  ))}
3144
4114
  </Box>
3145
4115
  )}
@@ -3200,9 +4170,6 @@ function ModelListCommand({ filter }: ModelListCommandProps) {
3200
4170
  <Box width={24}>
3201
4171
  <Text bold dimColor>Dataset</Text>
3202
4172
  </Box>
3203
- <Box width={14}>
3204
- <Text bold dimColor>Config</Text>
3205
- </Box>
3206
4173
  <Box width={30}>
3207
4174
  <Text bold dimColor>Metrics</Text>
3208
4175
  </Box>
@@ -3265,7 +4232,7 @@ function normalizeModelId(modelId: string): string {
3265
4232
 
3266
4233
  function getDecoderTaskType(model: api.BaseModelInfo | null | undefined): string {
3267
4234
  if (!model) return "";
3268
- return `${model.task_type ?? model.type ?? ""}`.trim().toLowerCase();
4235
+ return `${model.task_type ?? ""}`.trim().toLowerCase();
3269
4236
  }
3270
4237
 
3271
4238
  function modelSupportsDecoderInference(model: api.BaseModelInfo | null | undefined): boolean {
@@ -3542,112 +4509,11 @@ const Help: React.FC<HelpProps> = ({ context = "root" }) => {
3542
4509
  <Text dimColor> Dataset format: name[:version] (version defaults to "latest")</Text>
3543
4510
  <Text dimColor> Examples: my-dataset, my-dataset:v1, my-dataset:latest</Text>
3544
4511
  <Text> </Text>
3545
- <Text> dataset list List all datasets</Text>
3546
- <Text> dataset get {"<name[:version]>"} Get dataset details</Text>
3547
- <Text> dataset delete {"<name[:version]>"} Delete a dataset</Text>
3548
- <Text> dataset analyze {"<name[:version]>"} Analyze a dataset</Text>
3549
- <Text> dataset analyze-llm {"<name[:version]>"} LLM-only dataset analysis</Text>
4512
+ <Text> dataset list List all datasets (tabular). Add --json for raw JSON.</Text>
4513
+ <Text> dataset get {"<name[:version]>"} Get dataset details (key/value layout). Add --json for raw JSON.</Text>
3550
4514
  <Text> </Text>
3551
- <Text bold> Generate:</Text>
3552
- <Text> dataset generate ner</Text>
3553
- <Text> --labels {"<l1,l2,...>"} Entity labels (required)</Text>
3554
- <Text> --num {"<n>"} Number of examples (default: 10)</Text>
3555
- <Text> --domain {"<desc>"} Domain description</Text>
3556
- <Text> --save true Save to database</Text>
3557
- <Text> --name {"<name>"} Dataset name (required if --save)</Text>
3558
- <Text> </Text>
3559
- <Text> dataset generate classification</Text>
3560
- <Text> --labels {"<l1,l2,...>"} Class labels (required)</Text>
3561
- <Text> --num {"<n>"} Number of examples (default: 10)</Text>
3562
- <Text> --domain {"<desc>"} Domain description</Text>
3563
- <Text> --multi-label true Enable multi-label</Text>
3564
- <Text> --save true Save to database</Text>
3565
- <Text> --name {"<name>"} Dataset name (required if --save)</Text>
3566
- <Text> </Text>
3567
- <Text> dataset generate custom</Text>
3568
- <Text> --prompt {"<prompt>"} Task description (required)</Text>
3569
- <Text> --format {"<json>"} Output format as JSON (required)</Text>
3570
- <Text> --num {"<n>"} Number of examples (default: 10)</Text>
3571
- <Text> --save true Save to database</Text>
3572
- <Text> --name {"<name>"} Dataset name (required if --save)</Text>
3573
- <Text> </Text>
3574
- <Text> dataset generate decoder</Text>
3575
- <Text> --domain {"<desc>"} Domain/task description (required)</Text>
3576
- <Text> --instruction {"<text>"} System instruction (optional, auto-inferred)</Text>
3577
- <Text> --num {"<n>"} Number of examples (default: 10)</Text>
3578
- <Text> --save true Save to database</Text>
3579
- <Text> --name {"<name>"} Dataset name (required if --save)</Text>
3580
- <Text> Advanced generation flags:</Text>
3581
- <Text> --quality {"<light|medium|heavy>"} Generation quality profile</Text>
3582
- <Text> --generation-profile {"<auto|fast|balanced|quality>"} Runtime profile</Text>
3583
- <Text> --reasoning-trace {"true|false"} Include reasoning traces (decoder only)</Text>
3584
- <Text> --reasoning-effort {"<low|medium|high>"} Reasoning effort</Text>
3585
- <Text> --multiplicator {"<json>"} Multiplicator settings</Text>
3586
- <Text> --use-meta-felix {"true|false"} Use MetaFelix metadata</Text>
3587
- <Text> --min-criteria {"<n>"} Minimum diversity criteria</Text>
3588
- <Text> --target-choices {"<n>"} Diversity target choices</Text>
3589
- <Text> --project-id {"<id>"} Project ID</Text>
3590
- <Text> --type {"training|evaluation|split"} Dataset type</Text>
3591
- <Text> --visibility {"private|public"} Dataset visibility</Text>
3592
- <Text> --split-ratio {"<train:eval>|{json>}"} Split dataset ratio</Text>
3593
- <Text> --negative-ratio {"<n>"} Percent negative samples</Text>
3594
- <Text> --classified-examples {"<json>"} Classified examples with feedback</Text>
3595
- <Text> </Text>
3596
- <Text bold> Infer Labels:</Text>
3597
- <Text> dataset infer ner Infer NER labels from description</Text>
3598
- <Text> dataset infer classification Infer classification labels</Text>
3599
- <Text> dataset infer fields Infer input/output fields</Text>
3600
- <Text> --domain {"<desc>"} Domain description (required)</Text>
3601
- <Text> dataset infer infer-advanced Infer constraints and multiplicator from a prompt</Text>
3602
- <Text> --prompt {"<prompt>"} Prompt for inference</Text>
3603
- <Text> --labels {"<l1,l2,...>"} Optional labels to guide suggestions</Text>
3604
- <Text> --data-type {"<type>"} entity_extraction|classification|json_extraction</Text>
3605
- <Text> dataset infer improve-prompt Improve a generation prompt</Text>
3606
- <Text> --prompt {"<prompt>"} Prompt to improve</Text>
3607
- <Text> --data-type {"<type>"} Optional prompt domain hint</Text>
3608
- <Text> dataset label-existing ner Label existing NER texts</Text>
3609
- <Text> --labels {"<l1,l2,...>"} Labels for entities</Text>
3610
- <Text> --inputs {"[{\"text\":\"...\"},...]"} Input texts JSON array (required)</Text>
3611
- <Text> --name {"<name>"} Output dataset name (optional if --save false)</Text>
3612
- <Text> --project-id {"<project_id>"} Assign output dataset to project</Text>
3613
- <Text> --save {"<true|false>"} Save dataset (default: false)</Text>
3614
- <Text> dataset label-existing classification Label existing classification texts</Text>
3615
- <Text> --labels {"<l1,l2,...>"} Labels for classes</Text>
3616
- <Text> --inputs {"[{\"text\":\"...\"},...]"} Input texts JSON array (required)</Text>
3617
- <Text> --name {"<name>"} Output dataset name (optional if --save false)</Text>
3618
- <Text> --project-id {"<project_id>"} Assign output dataset to project</Text>
3619
- <Text> --save {"<true|false>"} Save dataset (default: false)</Text>
3620
- <Text> dataset label-existing fields Label existing structured records</Text>
3621
- <Text> --input-fields {"[{\"name\":\"...\"},...]"} Input schema fields (required)</Text>
3622
- <Text> --output-fields {"[{\"name\":\"...\"},...]"} Output schema fields (required)</Text>
3623
- <Text> --inputs {"[{\"f1\":\"v\"},...]"} Input records JSON array (required)</Text>
3624
- <Text> --name {"<name>"} Output dataset name (optional if --save false)</Text>
3625
- <Text> --project-id {"<project_id>"} Assign output dataset to project</Text>
3626
- <Text> --save {"<true|false>"} Save dataset (default: false)</Text>
3627
- <Text> </Text>
3628
- <Text bold> Upload/Download:</Text>
3629
- <Text> dataset upload {"<file>"} Upload local file to Pioneer</Text>
3630
- <Text> --name {"<name>"} Dataset name (required)</Text>
3631
- <Text> --type {"<type>"} Type: ner, classification, custom</Text>
3632
- <Text> dataset upload {"<name[:version]>"} --to hf Upload Pioneer dataset to Hugging Face</Text>
3633
- <Text> --repo {"<repo>"} HF repo (required, e.g., username/dataset)</Text>
3634
- <Text> --private Make repo private</Text>
3635
- <Text dimColor> Note: Set HF token with 'pioneer auth hf' first</Text>
3636
- <Text> dataset download {"<name[:version]>"} Download from Pioneer to local file</Text>
3637
- <Text> --format {"<type>"} Format: jsonl, csv, parquet (default: jsonl)</Text>
3638
- <Text> --output {"<path>"} Output file path</Text>
3639
- <Text> dataset download --from hf Download from Hugging Face to Pioneer</Text>
3640
- <Text> --repo {"<repo>"} HF repo (required, e.g., username/dataset)</Text>
3641
- <Text> --name {"<name>"} Local dataset name (optional)</Text>
3642
- <Text> --revision {"<rev>"} Git revision/branch (optional)</Text>
3643
- <Text dimColor> Note: For private repos, set HF token with 'pioneer auth hf'</Text>
3644
- <Text> </Text>
3645
- <Text bold> Data Editing:</Text>
3646
- <Text> dataset edit --help Show data editing commands</Text>
3647
- <Text> dataset edit scan-pii {"<name[:version]>"} Scan for PII</Text>
3648
- <Text> dataset edit dismiss-outlier {"<name[:version]>"} Dismiss an outlier fingerprint</Text>
3649
- <Text> --fingerprint {"<hash>"} Outlier fingerprint from dataset analysis</Text>
3650
- <Text> dataset edit subsample {"<name[:version]>"} Create a subsample</Text>
4515
+ <Text dimColor> Other dataset subcommands (generate/edit/upload/etc.) are temporarily hidden in this version.</Text>
4516
+ <Text dimColor> Use the Pioneer web app at agent.pioneer.ai to create or edit datasets.</Text>
3651
4517
  </Box>
3652
4518
  );
3653
4519
  }
@@ -3710,18 +4576,13 @@ const Help: React.FC<HelpProps> = ({ context = "root" }) => {
3710
4576
  return (
3711
4577
  <Box flexDirection="column">
3712
4578
  <Text bold>Job Commands:</Text>
3713
- <Text> job list List training jobs</Text>
3714
- <Text> job get {"<id>"} Get job details</Text>
4579
+ <Text> job list List training jobs (tabular). Add --json for raw JSON.</Text>
4580
+ <Text> job get {"<id>"} Get job details (key/value layout). Add --json for raw JSON. Accepts a unique short prefix from `job list`.</Text>
3715
4581
  <Text> job logs {"<id>"} Get job logs</Text>
3716
4582
  <Text> job delete {"<id>"} Delete a training job</Text>
3717
- <Text> job create Create training job</Text>
3718
- <Text> --model-name {"<name>"} Model name (required)</Text>
3719
- <Text> --dataset-ids {"<ids>"} Comma-separated datasets (required)</Text>
3720
- <Text> remote:{"<name>"}:{"<version>"} - by name and version</Text>
3721
- <Text> remote:{"<uuid>"} - by UUID</Text>
3722
- <Text> local:{"<name>"} - auto-upload from ./datasets/</Text>
3723
- <Text> --base-model {"<model>"} Base model (default: fastino/gliner2-base-v1)</Text>
3724
- <Text> --epochs {"<n>"} Number of epochs (default: 5)</Text>
4583
+ <Text> </Text>
4584
+ <Text dimColor> To create a training job, use `pioneer agent` — it will help you pick a base model,</Text>
4585
+ <Text dimColor> select datasets, and configure training conversationally.</Text>
3725
4586
  </Box>
3726
4587
  );
3727
4588
  }
@@ -3740,13 +4601,13 @@ const Help: React.FC<HelpProps> = ({ context = "root" }) => {
3740
4601
  <Text> --description {"<text>"} Optional</Text>
3741
4602
  <Text> --model {"<base-model-id>"} Optional (starts interactive picker when omitted)</Text>
3742
4603
  <Text> --example {"<json>"} Optional</Text>
3743
- <Text> model endpoints get {"<model-id>"} Get endpoint/model entry details</Text>
4604
+ <Text> model endpoints get {"<model-id>"} Get endpoint details (key/value layout, includes dataset count). Add --json for raw JSON.</Text>
3744
4605
  <Text> model endpoints update {"<model-id>"} Update endpoint metadata</Text>
3745
4606
  <Text> --name {"<name>"} --icon {"<icon>"} --repo {"<repo-url>"} --description {"<text>"} --model-id {"<id>"}</Text>
3746
4607
  <Text> model endpoints delete {"<model-id>"} Delete an endpoint/model entry</Text>
3747
- <Text> model endpoints dataset-count {"<model-id>"} Get attached dataset count</Text>
3748
- <Text> model endpoints quality-metrics {"<model-id>"} Show LLMAJ pass/fail metrics</Text>
3749
- <Text> model endpoints deploy {"<model-id>"} --job {"<training-job-id>"} [--reason {"<text>"}] Deploy a trained job to the endpoint</Text>
4608
+ <Text> model endpoints quality-metrics {"<model-id>"} Show LLMAJ pass/fail metrics. Add --json for raw JSON.</Text>
4609
+ <Text> model endpoints deploy {"<model-id>"} [--job {"<training-job-id>"}] [--reason {"<text>"}] [--all]</Text>
4610
+ <Text dimColor> If --job is omitted, pick from a list of deployable jobs interactively. By default the list is filtered to jobs whose base model matches the endpoint; pass --all to bypass that filter.</Text>
3750
4611
  <Text> model endpoints rollback {"<model-id>"} {"<deployment-id>"} Rollback endpoint to previous deployment</Text>
3751
4612
  </Box>
3752
4613
  );
@@ -3761,7 +4622,7 @@ const Help: React.FC<HelpProps> = ({ context = "root" }) => {
3761
4622
  <Text> model artifacts list Show both trained and deployed artifacts</Text>
3762
4623
  <Text> model artifacts trained List trained artifacts</Text>
3763
4624
  <Text> model artifacts deployed List deployed artifacts</Text>
3764
- <Text> model artifacts download {"<job-id>"} Download model artifact</Text>
4625
+ <Text> model artifacts download {"<job-id>"} Get a signed download URL for the artifact (key/value layout). Add --json for raw JSON.</Text>
3765
4626
  <Text> model artifacts delete {"<job-id>"} Delete deployed artifact record</Text>
3766
4627
  <Text> model artifacts upload {"<job-id>"} --to hf Upload trained model artifact to Hugging Face</Text>
3767
4628
  <Text> --repo {"<repo>"} HF repo (required, e.g., username/model)</Text>
@@ -3779,17 +4640,20 @@ const Help: React.FC<HelpProps> = ({ context = "root" }) => {
3779
4640
  <Text bold>Model Commands:</Text>
3780
4641
  <Text> model endpoints ... (alias: model_endpoints) Manage model catalog entries (from /projects)</Text>
3781
4642
  <Text> model artifacts ... (alias: model_artifacts) Manage trained/deployed artifacts (from /felix)</Text>
4643
+ <Text> model base-models List available base models (tabular)</Text>
3782
4644
  <Text> </Text>
3783
4645
  <Text> model endpoints list</Text>
3784
4646
  <Text> model endpoints create</Text>
3785
4647
  <Text> model endpoints get {"<model-id>"}</Text>
3786
- <Text> model endpoints deploy {"<model-id>"} --job {"<training-job-id>"} [--reason {"<text>"}]</Text>
4648
+ <Text> model endpoints quality-metrics {"<model-id>"}</Text>
4649
+ <Text> model endpoints deploy {"<model-id>"} [--job {"<training-job-id>"}] [--reason {"<text>"}] [--all]</Text>
3787
4650
  <Text> model endpoints rollback {"<model-id>"} {"<deployment-id>"}</Text>
3788
4651
  <Text> model artifacts list</Text>
3789
4652
  <Text> model artifacts trained</Text>
3790
- <Text> model artifacts deployed</Text>
3791
- <Text> model artifacts download {"<job-id>"}</Text>
4653
+ <Text> model artifacts deployed</Text>
4654
+ <Text> model artifacts download {"<job-id>"}</Text>
3792
4655
  <Text> </Text>
4656
+ <Text dimColor> Most read commands accept `--json` for the raw JSON payload.</Text>
3793
4657
  </Box>
3794
4658
  );
3795
4659
  }
@@ -3921,13 +4785,8 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
3921
4785
  rest[0] === "create" &&
3922
4786
  parseErrors.length === 1 &&
3923
4787
  parseErrors[0] === "--model";
3924
- const isModelEndpointsDeployMissingJob =
3925
- group === "model" &&
3926
- normalizedAction === "endpoints" &&
3927
- rest[0] === "deploy" &&
3928
- !flags["job"];
3929
4788
 
3930
- if (group === "dataset" || group === "inference" || group === "eval" || group === "benchmark") {
4789
+ if (group === "inference" || group === "eval" || group === "benchmark") {
3931
4790
  return (
3932
4791
  <ErrorMessage
3933
4792
  error={`The '${group}' command group is temporarily hidden for this version. Use 'pioneer --help' to see available commands.`}
@@ -3935,6 +4794,19 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
3935
4794
  );
3936
4795
  }
3937
4796
 
4797
+ if (group === "dataset") {
4798
+ const allowedDatasetActions = new Set(["list", "get", "help", undefined, ""]);
4799
+ const subAction = action ?? "";
4800
+ const isHelp = flags["help"] === "true" || subAction === "" || subAction === "help";
4801
+ if (!isHelp && !allowedDatasetActions.has(subAction)) {
4802
+ return (
4803
+ <ErrorMessage
4804
+ error={`'dataset ${subAction}' is temporarily hidden for this version. Available: pioneer dataset list, pioneer dataset get <name[:version]>.`}
4805
+ />
4806
+ );
4807
+ }
4808
+ }
4809
+
3938
4810
  if (hasParseErrors && !isModelCreateMissingModel) {
3939
4811
  const missingValueHints: Record<string, string> = {
3940
4812
  "--model": "<base-model-id>",
@@ -3960,7 +4832,6 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
3960
4832
  "--labels": "<json-array>",
3961
4833
  "--label-column": "<column>",
3962
4834
  "--text-column": "<column>",
3963
- "--dataset-ids": "<comma-separated-ids>",
3964
4835
  "--output": "<path>",
3965
4836
  "--format-results": "<true|false>",
3966
4837
  "--include-confidence": "<true|false>",
@@ -3975,28 +4846,6 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
3975
4846
  return missingValueHints[flag] ?? "<value>";
3976
4847
  };
3977
4848
 
3978
- if (isModelEndpointsDeployMissingJob) {
3979
- const errorMessage = rest[1]
3980
- ? `Training job ID required: model endpoints deploy ${rest[1]} --job <training-job-id>`
3981
- : "Training job ID required: model endpoints deploy <model-id> --job <training-job-id>";
3982
-
3983
- return (
3984
- <ApiCommand
3985
- action={() =>
3986
- Promise.resolve<api.ApiResult<{ message: string }>>({
3987
- ok: false,
3988
- status: 400,
3989
- error: errorMessage,
3990
- data: {
3991
- message: errorMessage,
3992
- },
3993
- })
3994
- }
3995
- successMessage="Validation failed"
3996
- />
3997
- );
3998
- }
3999
-
4000
4849
  return (
4001
4850
  <Box flexDirection="column">
4002
4851
  <ErrorMessage error="One or more flags are missing values. Please provide values for: " />
@@ -4360,6 +5209,9 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
4360
5209
  }
4361
5210
 
4362
5211
  if (action === "list") {
5212
+ if (flags.json === "true") {
5213
+ return <ApiCommand action={api.listDatasets} />;
5214
+ }
4363
5215
  return <DatasetListCommand />;
4364
5216
  }
4365
5217
  if (action === "get" && rest[0]) {
@@ -4367,7 +5219,10 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
4367
5219
  if (!dataset) {
4368
5220
  return <ErrorMessage error={`Invalid dataset format: ${rest[0]}. Use name[:version] format (e.g., my-dataset or my-dataset:v1).`} />;
4369
5221
  }
4370
- return <ApiCommand action={() => api.getDataset(dataset)} />;
5222
+ if (flags.json === "true") {
5223
+ return <ApiCommand action={() => api.getDataset(dataset)} />;
5224
+ }
5225
+ return <DatasetGetCommand dataset={dataset} />;
4371
5226
  }
4372
5227
  if (action === "delete" && rest[0]) {
4373
5228
  const dataset = parseDatasetRef(rest[0]);
@@ -4884,48 +5739,55 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
4884
5739
  return <Help context="job" />;
4885
5740
  }
4886
5741
  if (action === "list") {
4887
- return <ApiCommand action={api.listJobs} />;
5742
+ if (flags.json === "true") {
5743
+ return <ApiCommand action={api.listJobs} />;
5744
+ }
5745
+ return <JobListCommand />;
4888
5746
  }
4889
5747
  if (action === "get" && rest[0]) {
4890
- return <ApiCommand action={() => api.getJob(rest[0])} />;
5748
+ const wantsJson = flags.json === "true";
5749
+ return (
5750
+ <WithResolvedJobId
5751
+ rawId={rest[0]}
5752
+ render={(jobId) =>
5753
+ wantsJson ? (
5754
+ <ApiCommand action={() => api.getJob(jobId)} />
5755
+ ) : (
5756
+ <JobGetCommand jobId={jobId} />
5757
+ )
5758
+ }
5759
+ />
5760
+ );
4891
5761
  }
4892
5762
  if (action === "logs" && rest[0]) {
4893
- return <JobLogsCommand jobId={rest[0]} />;
5763
+ return (
5764
+ <WithResolvedJobId
5765
+ rawId={rest[0]}
5766
+ render={(jobId) => <JobLogsCommand jobId={jobId} />}
5767
+ />
5768
+ );
4894
5769
  }
4895
5770
  if (action === "delete" && rest[0]) {
4896
5771
  return (
4897
- <ApiCommand
4898
- action={() => api.deleteJob(rest[0])}
4899
- successMessage={`Training job ${rest[0]} deleted`}
5772
+ <WithResolvedJobId
5773
+ rawId={rest[0]}
5774
+ render={(jobId) => (
5775
+ <ApiCommand
5776
+ action={() => api.deleteJob(jobId)}
5777
+ successMessage={`Training job ${jobId} deleted`}
5778
+ />
5779
+ )}
4900
5780
  />
4901
5781
  );
4902
5782
  }
4903
5783
  if (action === "create") {
4904
- const modelName = flags["model-name"];
4905
- const datasetStrings = flags["dataset-ids"]?.split(",").filter(Boolean) ?? [];
4906
- const baseModel = flags["base-model"];
4907
- const epochs = flags["epochs"] ? parseInt(flags["epochs"], 10) : undefined;
4908
-
4909
- if (!modelName || datasetStrings.length === 0) {
4910
- return <ErrorMessage error="--model-name and --dataset-ids are required" />;
4911
- }
4912
-
4913
- // Parse dataset strings with explicit local:/remote: prefixes
4914
- const datasets: ParsedDataset[] = [];
4915
- for (const ds of datasetStrings) {
4916
- const parsed = parseDatasetString(ds);
4917
- if ("error" in parsed) {
4918
- return <ErrorMessage error={parsed.error} />;
4919
- }
4920
- datasets.push(parsed);
4921
- }
4922
-
4923
5784
  return (
4924
- <JobCreateCommand
4925
- modelName={modelName}
4926
- datasets={datasets}
4927
- baseModel={baseModel}
4928
- epochs={epochs}
5785
+ <ErrorMessage
5786
+ error={
5787
+ "`pioneer job create` has been removed.\n" +
5788
+ "Use `pioneer agent` to create training jobs conversationally — it will help you pick a base model,\n" +
5789
+ "select datasets, and configure training without needing to remember flags or dataset IDs."
5790
+ }
4929
5791
  />
4930
5792
  );
4931
5793
  }
@@ -4939,7 +5801,10 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
4939
5801
  }
4940
5802
 
4941
5803
  if (action === "base-models" || action === "models" || action === "list") {
4942
- return <ApiCommand action={api.listBaseModels} />;
5804
+ if (flags.json === "true") {
5805
+ return <ApiCommand action={api.listBaseModels} />;
5806
+ }
5807
+ return <BaseModelsListCommand />;
4943
5808
  }
4944
5809
 
4945
5810
  if (action === "encoder") {
@@ -5137,6 +6002,13 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
5137
6002
  return <Help context="model" />;
5138
6003
  }
5139
6004
 
6005
+ if (normalizedAction === "base-models" || normalizedAction === "models") {
6006
+ if (flags.json === "true") {
6007
+ return <ApiCommand action={api.listBaseModels} />;
6008
+ }
6009
+ return <BaseModelsListCommand />;
6010
+ }
6011
+
5140
6012
  if (normalizedAction === "endpoints") {
5141
6013
  const endpointAction = rest[0];
5142
6014
  const endpointArgs = rest.slice(1);
@@ -5213,7 +6085,10 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
5213
6085
  if (!modelId) {
5214
6086
  return <ErrorMessage error="Model ID required: model endpoints get <model-id>" />;
5215
6087
  }
5216
- return <ApiCommand action={() => api.getProject(modelId)} />;
6088
+ if (flags.json === "true") {
6089
+ return <ApiCommand action={() => api.getProject(modelId)} />;
6090
+ }
6091
+ return <ModelEndpointGetCommand modelId={modelId} />;
5217
6092
  }
5218
6093
 
5219
6094
  if (endpointAction === "update") {
@@ -5264,11 +6139,14 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
5264
6139
  }
5265
6140
 
5266
6141
  if (endpointAction === "dataset-count" || endpointAction === "count") {
5267
- const modelId = endpointArgs[0];
5268
- if (!modelId) {
5269
- return <ErrorMessage error="Model ID required: model endpoints dataset-count <model-id>" />;
5270
- }
5271
- return <ApiCommand action={() => api.getProjectDatasetCount(modelId)} />;
6142
+ return (
6143
+ <ErrorMessage
6144
+ error={
6145
+ "`pioneer model endpoints dataset-count` has been removed.\n" +
6146
+ "The dataset count and `can_delete` flag are now surfaced inline in `pioneer model endpoints get <model-id>`."
6147
+ }
6148
+ />
6149
+ );
5272
6150
  }
5273
6151
 
5274
6152
  if (endpointAction === "quality-metrics" || endpointAction === "quality") {
@@ -5276,40 +6154,75 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
5276
6154
  if (!modelId) {
5277
6155
  return <ErrorMessage error="Model ID required: model endpoints quality-metrics <model-id>" />;
5278
6156
  }
5279
- return <ApiCommand action={() => api.getProjectQualityMetrics(modelId)} />;
6157
+ if (flags.json === "true") {
6158
+ return <ApiCommand action={() => api.getProjectQualityMetrics(modelId)} />;
6159
+ }
6160
+ return <ModelQualityMetricsCommand modelId={modelId} />;
5280
6161
  }
5281
6162
 
5282
6163
  if (endpointAction === "deploy") {
5283
6164
  const modelId = endpointArgs[0];
5284
- const jobId = flags["job"];
6165
+ const rawJobId = flags["job"];
5285
6166
 
5286
6167
  if (!modelId) {
5287
- return <ErrorMessage error="Model ID required: model endpoints deploy <model-id> --job <training-job-id>" />;
5288
- }
5289
- if (!jobId) {
5290
- return <ErrorMessage error="Training job ID required: model endpoints deploy <model-id> --job <training-job-id>" />;
5291
- }
5292
- if (jobId.length !== 36) {
5293
- return (
5294
- <Box flexDirection="column">
5295
- <ErrorMessage error="Invalid job ID: must be full UUID (36 characters)" />
5296
- <Text dimColor> Provided: {jobId} ({jobId.length} characters)</Text>
5297
- <Text dimColor> Tip: Use 'pioneer model artifacts list' and 'pioneer model artifacts trained' to see full job IDs</Text>
5298
- </Box>
5299
- );
6168
+ return <ErrorMessage error="Model ID required: model endpoints deploy <model-id> [--job <training-job-id>]" />;
5300
6169
  }
5301
6170
 
5302
6171
  const reason = flags["reason"];
5303
6172
 
6173
+ if (!rawJobId) {
6174
+ if (!isRawModeSupported) {
6175
+ return (
6176
+ <Box flexDirection="column">
6177
+ <ErrorMessage error="Interactive job picker requires a TTY." />
6178
+ <Text>List trained jobs with `pioneer job list` and pass an explicit ID:</Text>
6179
+ <Text dimColor> pioneer model endpoints deploy {modelId} --job &lt;training-job-id&gt;</Text>
6180
+ </Box>
6181
+ );
6182
+ }
6183
+ const showAll = flags["all"] === "true";
6184
+ return <DeployJobPickerCommand modelId={modelId} reason={reason} showAll={showAll} />;
6185
+ }
6186
+
5304
6187
  return (
5305
- <ApiCommand
5306
- action={() =>
5307
- api.deployTrainingJobToProject(modelId, {
5308
- training_job_id: jobId,
5309
- ...(reason ? { reason } : {}),
5310
- })
5311
- }
5312
- successMessage={`Deployment initiated for project ${modelId} from job ${jobId}`}
6188
+ <WithResolvedJobId
6189
+ rawId={rawJobId}
6190
+ render={(jobId) => (
6191
+ <ApiCommand
6192
+ action={async () => {
6193
+ // Deploy requires the job's project_id to match the target.
6194
+ // PATCH first to link them; the backend treats a no-op assign
6195
+ // as success. While we have the job around, capture its
6196
+ // base_model so we can enrich the deployment record (which
6197
+ // currently returns base_model=null for training-job deploys).
6198
+ const jobInfo = await api.getJob(jobId);
6199
+ const patch = await api.updateTrainingJob(jobId, {
6200
+ project_id: modelId,
6201
+ });
6202
+ if (!patch.ok) {
6203
+ return patch as api.ApiResult<unknown>;
6204
+ }
6205
+ const result = await api.deployTrainingJobToProject(modelId, {
6206
+ training_job_id: jobId,
6207
+ ...(reason ? { reason } : {}),
6208
+ });
6209
+ if (
6210
+ result.ok &&
6211
+ result.data &&
6212
+ jobInfo.ok &&
6213
+ jobInfo.data?.base_model &&
6214
+ !result.data.base_model
6215
+ ) {
6216
+ return {
6217
+ ...result,
6218
+ data: { ...result.data, base_model: jobInfo.data.base_model },
6219
+ };
6220
+ }
6221
+ return result;
6222
+ }}
6223
+ successMessage={`Deployment initiated for project ${modelId} from job ${jobId}`}
6224
+ />
6225
+ )}
5313
6226
  />
5314
6227
  );
5315
6228
  }
@@ -5370,37 +6283,33 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
5370
6283
  if (!artifactArgs[0]) {
5371
6284
  return <ErrorMessage error="Job ID required: model artifacts download <job-id>" />;
5372
6285
  }
5373
- const jobId = artifactArgs[0];
5374
- if (jobId.length !== 36) {
5375
- return (
5376
- <Box flexDirection="column">
5377
- <ErrorMessage error="Invalid job ID: must be full UUID (36 characters)" />
5378
- <Text dimColor> Provided: {jobId} ({jobId.length} characters)</Text>
5379
- <Text dimColor> Tip: Use 'pioneer model artifacts trained' or 'pioneer model artifacts deployed' to see full job IDs</Text>
5380
- </Box>
5381
- );
5382
- }
5383
- return <ApiCommand action={() => api.downloadModel(jobId)} />;
6286
+ return (
6287
+ <WithResolvedJobId
6288
+ rawId={artifactArgs[0]}
6289
+ render={(jobId) =>
6290
+ flags.json === "true" ? (
6291
+ <ApiCommand action={() => api.downloadModel(jobId)} />
6292
+ ) : (
6293
+ <ModelArtifactDownloadCommand jobId={jobId} />
6294
+ )
6295
+ }
6296
+ />
6297
+ );
5384
6298
  }
5385
6299
 
5386
6300
  if (artifactsAction === "delete") {
5387
6301
  if (!artifactArgs[0]) {
5388
6302
  return <ErrorMessage error="Model ID required: model artifacts delete <job-id>" />;
5389
6303
  }
5390
- const jobId = artifactArgs[0];
5391
- if (jobId.length !== 36) {
5392
- return (
5393
- <Box flexDirection="column">
5394
- <ErrorMessage error="Invalid job ID: must be full UUID (36 characters)" />
5395
- <Text dimColor> Provided: {jobId} ({jobId.length} characters)</Text>
5396
- <Text dimColor> Tip: Use 'pioneer model artifacts list' to see full job IDs</Text>
5397
- </Box>
5398
- );
5399
- }
5400
6304
  return (
5401
- <ApiCommand
5402
- action={() => api.deleteModel(jobId)}
5403
- successMessage={`Model ${jobId} deleted`}
6305
+ <WithResolvedJobId
6306
+ rawId={artifactArgs[0]}
6307
+ render={(jobId) => (
6308
+ <ApiCommand
6309
+ action={() => api.deleteModel(jobId)}
6310
+ successMessage={`Model ${jobId} deleted`}
6311
+ />
6312
+ )}
5404
6313
  />
5405
6314
  );
5406
6315
  }
@@ -5426,7 +6335,7 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
5426
6335
  if (!artifactArgs[0]) {
5427
6336
  return <ErrorMessage error="Job ID required: model artifacts upload <job-id> --to hf --repo <repo>" />;
5428
6337
  }
5429
- const jobId = artifactArgs[0];
6338
+ const rawJobId = artifactArgs[0];
5430
6339
  const repo = flags["repo"];
5431
6340
  const hfTokenFlag = flags["hf-token"];
5432
6341
  const isPrivate = flags["private"]?.toLowerCase() === "true";
@@ -5450,15 +6359,20 @@ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
5450
6359
  }
5451
6360
 
5452
6361
  return (
5453
- <ApiCommand
5454
- action={() =>
5455
- api.pushModelToHub(jobId, {
5456
- hf_token: hfToken,
5457
- repo_id: repo,
5458
- private: isPrivate,
5459
- })
5460
- }
5461
- successMessage={`Model uploaded to Hugging Face: ${repo}`}
6362
+ <WithResolvedJobId
6363
+ rawId={rawJobId}
6364
+ render={(jobId) => (
6365
+ <ApiCommand
6366
+ action={() =>
6367
+ api.pushModelToHub(jobId, {
6368
+ hf_token: hfToken,
6369
+ repo_id: repo,
6370
+ private: isPrivate,
6371
+ })
6372
+ }
6373
+ successMessage={`Model uploaded to Hugging Face: ${repo}`}
6374
+ />
6375
+ )}
5462
6376
  />
5463
6377
  );
5464
6378
  }