@fastino-ai/pioneer-cli 0.2.7 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.tsx CHANGED
@@ -4,7 +4,7 @@
4
4
  * Uses Ink (React) for terminal UI.
5
5
  */
6
6
 
7
- import React, { useState, useEffect } from "react";
7
+ import React, { useState, useEffect, useRef } from "react";
8
8
  import { render, Box, Text, useApp, useInput, useStdin, Static } from "ink";
9
9
  import Spinner from "ink-spinner";
10
10
  import TextInput from "ink-text-input";
@@ -14,6 +14,7 @@ import * as path from "path";
14
14
  import {
15
15
  getApiKey,
16
16
  getBaseUrl,
17
+ getMleModel,
17
18
  saveConfig,
18
19
  clearApiKey,
19
20
  getHfToken,
@@ -21,7 +22,7 @@ import {
21
22
  clearHfToken,
22
23
  } from "./config.js";
23
24
  import * as api from "./api.js";
24
- import { ChatApp } from "./chat/ChatApp.js";
25
+ import { WebSocketClient, type HistoryMessage } from "./client/WebSocketClient.js";
25
26
  import {
26
27
  isEnabled as isTelemetryEnabled,
27
28
  hasChosenTelemetry,
@@ -150,9 +151,30 @@ function parseDatasetRef(datasetStr: string): api.DatasetRef | null {
150
151
  };
151
152
  }
152
153
 
153
- function parseArgs(argv: string[]): { command: string[]; flags: Record<string, string> } {
154
+ const BOOLEAN_FLAGS = new Set([
155
+ "help",
156
+ "h",
157
+ "version",
158
+ "v",
159
+ "private",
160
+ "save",
161
+ "multi-label",
162
+ "include-spans",
163
+ "echo",
164
+ "include-confidence",
165
+ "format-results",
166
+ "reasoning-trace",
167
+ "use-meta-felix",
168
+ ]);
169
+
170
+ function parseArgs(argv: string[]): {
171
+ command: string[];
172
+ flags: Record<string, string>;
173
+ parseErrors: string[];
174
+ } {
154
175
  const command: string[] = [];
155
176
  const flags: Record<string, string> = {};
177
+ const parseErrors: string[] = [];
156
178
 
157
179
  for (let i = 0; i < argv.length; i++) {
158
180
  const arg = argv[i];
@@ -163,7 +185,12 @@ function parseArgs(argv: string[]): { command: string[]; flags: Record<string, s
163
185
  flags[key] = next;
164
186
  i++;
165
187
  } else {
166
- flags[key] = "true";
188
+ if (BOOLEAN_FLAGS.has(key)) {
189
+ flags[key] = "true";
190
+ } else {
191
+ parseErrors.push(`--${key}`);
192
+ flags[key] = "";
193
+ }
167
194
  }
168
195
  } else if (arg.startsWith("-") && arg.length === 2) {
169
196
  // Handle short flags like -v, -h
@@ -173,14 +200,37 @@ function parseArgs(argv: string[]): { command: string[]; flags: Record<string, s
173
200
  flags[key] = next;
174
201
  i++;
175
202
  } else {
176
- flags[key] = "true";
203
+ if (BOOLEAN_FLAGS.has(key)) {
204
+ flags[key] = "true";
205
+ } else {
206
+ parseErrors.push(`-${key}`);
207
+ flags[key] = "";
208
+ }
177
209
  }
178
210
  } else {
179
211
  command.push(arg);
180
212
  }
181
213
  }
182
214
 
183
- return { command, flags };
215
+ return { command, flags, parseErrors };
216
+ }
217
+
218
+ type CreateProjectExample = Record<string, unknown> | undefined;
219
+
220
+ function parseProjectExample(exampleStr: string | undefined): { value?: CreateProjectExample; error?: string } {
221
+ if (!exampleStr) {
222
+ return { value: undefined };
223
+ }
224
+
225
+ try {
226
+ const parsed = JSON.parse(exampleStr) as unknown;
227
+ if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
228
+ return { error: "--example must be a JSON object" };
229
+ }
230
+ return { value: parsed as CreateProjectExample };
231
+ } catch {
232
+ return { error: "--example must be valid JSON" };
233
+ }
184
234
  }
185
235
 
186
236
  // ─────────────────────────────────────────────────────────────────────────────
@@ -663,177 +713,758 @@ function ApiCommand<T>({ action, successMessage }: ApiCommandProps<T>) {
663
713
  );
664
714
  }
665
715
 
666
- // ─────────────────────────────────────────────────────────────────────────────
667
- // Job Logs Command (prettified output)
668
- // ─────────────────────────────────────────────────────────────────────────────
716
+ interface AgentInteractiveCommandProps {
717
+ message: string;
718
+ conversationId?: string;
719
+ history?: api.AgentChatHistoryItem[];
720
+ }
669
721
 
670
- function JobLogsCommand({ jobId }: { jobId: string }) {
671
- const { exit } = useApp();
672
- const [state, setState] = useState<"loading" | "done" | "error">("loading");
673
- const [logs, setLogs] = useState<api.TrainingLog[]>([]);
722
+ function toHistoryMessages(
723
+ items: Array<api.AgentChatHistoryItem | api.ChatSessionMessage>
724
+ ): HistoryMessage[] {
725
+ return items
726
+ .filter((item) => Boolean(item && typeof item.content === "string" && typeof item.role === "string"))
727
+ .map((item) => {
728
+ const message: HistoryMessage = {
729
+ role: item.role as "user" | "assistant" | "tool" | string,
730
+ content: item.content,
731
+ };
732
+
733
+ const sessionMessage = item as api.ChatSessionMessage;
734
+ if (sessionMessage.tool_call_id) {
735
+ message.tool_call_id = sessionMessage.tool_call_id;
736
+ }
737
+ if (sessionMessage.tool_calls && Array.isArray(sessionMessage.tool_calls)) {
738
+ message.tool_calls = sessionMessage.tool_calls.filter((call) => Boolean(call));
739
+ }
740
+
741
+ return message;
742
+ });
743
+ }
744
+
745
+ function AgentInteractiveCommand({ message, conversationId, history }: AgentInteractiveCommandProps) {
746
+ const [state, setState] = useState<"connecting" | "running" | "done" | "error">("connecting");
747
+ const [stream, setStream] = useState("");
674
748
  const [error, setError] = useState("");
749
+ const [statusHint, setStatusHint] = useState("");
750
+ const [client] = useState(() => new WebSocketClient());
751
+ const doneHistory = useRef<HistoryMessage[] | undefined>(undefined);
675
752
 
676
753
  useEffect(() => {
677
- (async () => {
678
- const result = await api.getJobLogs(jobId);
679
- if (result.ok && result.data) {
680
- setLogs(result.data.logs || []);
754
+ let isActive = true;
755
+ const model = getMleModel();
756
+ let wsSessionId: string | undefined;
757
+ const finish = (code: number) => {
758
+ setTimeout(() => process.exit(code), 300);
759
+ };
760
+ const getLatestAssistantContent = (
761
+ messages: HistoryMessage[] | undefined
762
+ ): string => {
763
+ if (!messages?.length) {
764
+ return "";
765
+ }
766
+ const lastAssistant = [...messages]
767
+ .reverse()
768
+ .find(
769
+ (entry) =>
770
+ entry.role === "assistant" && typeof entry.content === "string" && entry.content.trim()
771
+ );
772
+ return lastAssistant?.content?.trim() ?? "";
773
+ };
774
+ let streamBuffer = "";
775
+ const appendStream = (content: string) => {
776
+ streamBuffer += content;
777
+ setStream(streamBuffer);
778
+ };
779
+ const runAgentFallback = async (
780
+ fallbackHistory: HistoryMessage[],
781
+ sessionIdHint?: string
782
+ ) => {
783
+ const plainHistory: api.AgentChatHistoryItem[] = fallbackHistory.map((entry) => ({
784
+ role: entry.role,
785
+ content: entry.content,
786
+ }));
787
+ const fallbackTimeout = (ms: number) =>
788
+ new Promise<never>((_, reject) => {
789
+ setTimeout(() => reject(new Error(`REST fallback timed out after ${ms}ms`)), ms);
790
+ });
791
+ setState("running");
792
+ setStatusHint("WebSocket unavailable, using REST fallback...");
793
+ try {
794
+ const runRequest = (sessionIdOverride?: string) => {
795
+ const requestConversationId = sessionIdOverride;
796
+ return Promise.race([
797
+ api.agentChat({
798
+ message,
799
+ ...(requestConversationId ? { conversation_id: requestConversationId } : {}),
800
+ ...(plainHistory.length ? { history: plainHistory } : {}),
801
+ }),
802
+ fallbackTimeout(8000),
803
+ ]);
804
+ };
805
+
806
+ const attempts = Array.from(
807
+ new Set([sessionIdHint, conversationId, undefined] as Array<string | undefined>)
808
+ );
809
+ let result: Awaited<ReturnType<typeof runRequest>> | undefined;
810
+ let lastError: string | undefined;
811
+
812
+ for (const attemptConversationId of attempts) {
813
+ const attemptResult = await runRequest(attemptConversationId);
814
+ if (attemptResult.ok && attemptResult.data?.answer) {
815
+ result = attemptResult;
816
+ break;
817
+ }
818
+ if (!attemptResult.ok && attemptResult.error) {
819
+ lastError = attemptResult.error;
820
+ }
821
+ }
822
+
823
+ if (!result) {
824
+ if (lastError) {
825
+ throw new Error(lastError || "Agent fallback request failed.");
826
+ }
827
+ result = {
828
+ ok: false,
829
+ status: 0,
830
+ error: "Agent fallback request failed.",
831
+ };
832
+ }
833
+
834
+ if (!isActive) {
835
+ return;
836
+ }
837
+ if (!result.ok) {
838
+ throw new Error(lastError || result.error || "Agent fallback request failed.");
839
+ }
840
+ setStream(result.data?.answer ?? "");
841
+ if (!result.data?.answer) {
842
+ setError("Agent returned no response content.");
843
+ setState("error");
844
+ setTimeout(() => process.exit(1), 300);
845
+ return;
846
+ }
847
+ setStatusHint("");
681
848
  setState("done");
682
- } else {
683
- setError(result.error ?? "Unknown error");
849
+ setTimeout(() => process.exit(0), 300);
850
+ } catch (fallbackError) {
851
+ if (!isActive) {
852
+ return;
853
+ }
854
+ const fallbackErrorMessage =
855
+ fallbackError instanceof Error
856
+ ? fallbackError.message
857
+ : String(fallbackError);
858
+ if (fallbackErrorMessage.includes("REST fallback timed out")) {
859
+ setError(
860
+ "Interactive mode timed out. Agent runtime is not reachable in this environment.\n" +
861
+ "Retry in standard mode (no mode flag needed), or set up a reachable WSS endpoint."
862
+ );
863
+ } else {
864
+ setError(fallbackErrorMessage);
865
+ }
684
866
  setState("error");
867
+ setTimeout(() => process.exit(1), 300);
685
868
  }
686
- setTimeout(() => exit(), 500);
687
- })();
688
- }, [jobId, exit]);
869
+ };
870
+ const start = async () => {
871
+ doneHistory.current = undefined;
872
+ const mergedHistory: HistoryMessage[] = [];
873
+ let commandCompleted = false;
874
+ let responseTimeoutId: ReturnType<typeof setTimeout> | undefined;
875
+ const fallbackOnTimeout = async () => {
876
+ if (!isActive || commandCompleted) {
877
+ return;
878
+ }
879
+ commandCompleted = true;
880
+ if (responseTimeoutId) {
881
+ clearTimeout(responseTimeoutId);
882
+ }
883
+ client.disconnect();
884
+ await runAgentFallback(mergedHistory, wsSessionId);
885
+ };
689
886
 
690
- if (state === "loading") {
691
- return <Loading />;
692
- }
887
+ const connectWithTimeout = async (ms: number): Promise<void> => {
888
+ return new Promise<void>((resolve, reject) => {
889
+ const timeoutId = setTimeout(() => {
890
+ reject(new Error(`WebSocket connect timed out after ${ms}ms`));
891
+ }, ms);
892
+
893
+ client.connect()
894
+ .then(() => {
895
+ clearTimeout(timeoutId);
896
+ resolve();
897
+ })
898
+ .catch((err) => {
899
+ clearTimeout(timeoutId);
900
+ reject(err);
901
+ });
902
+ });
903
+ };
904
+ const withTimeout = async <T,>(promise: Promise<T>, ms: number, label: string): Promise<T> => {
905
+ return new Promise<T>((resolve, reject) => {
906
+ const timeoutId = setTimeout(() => {
907
+ reject(new Error(`${label} timed out after ${ms}ms`));
908
+ }, ms);
909
+
910
+ promise.then(
911
+ (value) => {
912
+ clearTimeout(timeoutId);
913
+ resolve(value);
914
+ },
915
+ (error) => {
916
+ clearTimeout(timeoutId);
917
+ reject(error);
918
+ }
919
+ );
920
+ });
921
+ };
922
+
923
+ try {
924
+ if (conversationId) {
925
+ setStatusHint(`Loading conversation ${conversationId}...`);
926
+ const sessionResult = await api.getAgentSession(conversationId);
927
+ if (!sessionResult.ok) {
928
+ throw new Error(
929
+ sessionResult.error ??
930
+ `Could not load conversation ${conversationId}. Please check the conversation id and credentials.`
931
+ );
932
+ }
933
+
934
+ const session = sessionResult.data;
935
+ if (session?.messages?.length) {
936
+ mergedHistory.push(...toHistoryMessages(session.messages));
937
+ }
938
+ }
939
+
940
+ if (history?.length) {
941
+ mergedHistory.push(...toHistoryMessages(history));
942
+ }
943
+
944
+ const isWsHealthy = await withTimeout(client.health(), 3000, "WebSocket health check");
945
+ if (!isWsHealthy) {
946
+ throw new Error("WebSocket endpoint is not reachable.");
947
+ }
948
+
949
+ await connectWithTimeout(5000);
950
+ if (!isActive) {
951
+ return;
952
+ }
953
+
954
+ setStatusHint("");
955
+ setState("running");
956
+ responseTimeoutId = setTimeout(() => {
957
+ void fallbackOnTimeout();
958
+ }, 5000);
959
+
960
+ await client.chat(message, {
961
+ onStream: (content) => {
962
+ appendStream(content);
963
+ },
964
+ onAssistantMessage: (content) => {
965
+ appendStream(content);
966
+ },
967
+ onToolCall: async (call) => {
968
+ setStatusHint(`Tool requested: ${call.tool}`);
969
+ return "CLI does not execute tool calls yet.";
970
+ },
971
+ onError: (err) => {
972
+ if (!isActive) return;
973
+ setError(err.message || "WebSocket agent error");
974
+ setState("error");
975
+ finish(1);
976
+ },
977
+ onDone: (messages, sessionId) => {
978
+ if (!isActive) return;
979
+ commandCompleted = true;
980
+ if (responseTimeoutId) {
981
+ clearTimeout(responseTimeoutId);
982
+ }
983
+ doneHistory.current = messages;
984
+ wsSessionId = sessionId;
985
+ },
986
+ }, {
987
+ history: mergedHistory,
988
+ ...(model ? { config: { model } } : {}),
989
+ fileReferences: [],
990
+ });
991
+ if (!streamBuffer.trim()) {
992
+ const doneAssistantContent = getLatestAssistantContent(doneHistory.current);
993
+ if (doneAssistantContent) {
994
+ appendStream(doneAssistantContent);
995
+ setState("done");
996
+ finish(0);
997
+ return;
998
+ }
999
+
1000
+ setStatusHint("No assistant content in websocket response; retrying via REST fallback.");
1001
+ await runAgentFallback(mergedHistory, wsSessionId);
1002
+ return;
1003
+ }
1004
+
1005
+ setState("done");
1006
+ finish(0);
1007
+ } catch (err) {
1008
+ if (!isActive) return;
1009
+ commandCompleted = true;
1010
+ if (responseTimeoutId) {
1011
+ clearTimeout(responseTimeoutId);
1012
+ }
1013
+ const rawError = err instanceof Error ? err.message : String(err);
1014
+ const unavailable =
1015
+ rawError.includes("WebSocket") ||
1016
+ rawError.includes("websocket") ||
1017
+ rawError.includes("timed out") ||
1018
+ rawError.includes("not reachable");
1019
+
1020
+ if (unavailable) {
1021
+ const wsUrl = client.getWebSocketUrl();
1022
+ setStatusHint(`WebSocket timed out at ${wsUrl}. Falling back to REST.`);
1023
+ await runAgentFallback(mergedHistory);
1024
+ return;
1025
+ }
1026
+ setError(
1027
+ rawError || "Agent runtime unavailable. Retry by running `agent` in standard mode (omit --mode)."
1028
+ );
1029
+ setState("error");
1030
+ finish(1);
1031
+ }
1032
+ };
1033
+ start();
1034
+
1035
+ return () => {
1036
+ isActive = false;
1037
+ if (state !== "done" && state !== "error") {
1038
+ client.disconnect();
1039
+ }
1040
+ };
1041
+ }, [client, conversationId, history, message]);
1042
+
1043
+ useEffect(() => {
1044
+ if (state === "done") {
1045
+ setTimeout(() => process.exit(0), 300);
1046
+ } else if (state === "error") {
1047
+ setTimeout(() => process.exit(1), 300);
1048
+ }
1049
+ }, [state]);
693
1050
 
694
1051
  if (state === "error") {
695
- return <ErrorMessage error={error} />;
1052
+ return (
1053
+ <Box flexDirection="column">
1054
+ <ErrorMessage error={error || "WebSocket agent command failed."} />
1055
+ </Box>
1056
+ );
696
1057
  }
697
1058
 
698
- const formatLogEntry = (log: api.TrainingLog): string => {
699
- const ts = new Date(log.timestamp).toLocaleTimeString();
700
- return `[${ts}] [${log.level}] ${log.message}`;
701
- };
1059
+ if (state === "connecting") {
1060
+ return (
1061
+ <Box flexDirection="column">
1062
+ <Loading message={statusHint || "Connecting to agent runtime..."} />
1063
+ </Box>
1064
+ );
1065
+ }
702
1066
 
703
1067
  return (
704
1068
  <Box flexDirection="column">
705
- {logs.length === 0 ? (
706
- <Text dimColor>No logs available</Text>
1069
+ {state === "running" ? (
1070
+ <Loading message={stream ? "Streaming response..." : "Waiting for agent response..."} />
707
1071
  ) : (
708
- logs.map((log) => (
709
- <Text key={log.id} color={log.level === "ERROR" ? "red" : log.level === "WARNING" ? "yellow" : undefined}>
710
- {formatLogEntry(log)}
711
- </Text>
712
- ))
1072
+ <Success message="Agent response received" />
713
1073
  )}
1074
+ {stream ? <Text>{stream}</Text> : <Text dimColor>No response content yet.</Text>}
1075
+ {conversationId ? <Text dimColor>Conversation: {conversationId}</Text> : null}
714
1076
  </Box>
715
1077
  );
716
1078
  }
717
1079
 
718
- // ─────────────────────────────────────────────────────────────────────────────
719
- // Local Dataset Helpers
720
- // ─────────────────────────────────────────────────────────────────────────────
721
-
722
- const DATASETS_DIR = path.join(process.cwd(), "datasets");
1080
+ type AutoAgentTurnRole = "user" | "assistant";
723
1081
 
724
- function ensureDatasetsDir(): void {
725
- if (!fs.existsSync(DATASETS_DIR)) {
726
- fs.mkdirSync(DATASETS_DIR, { recursive: true });
727
- }
1082
+ interface AutoAgentTurn {
1083
+ role: AutoAgentTurnRole;
1084
+ content: string;
728
1085
  }
729
1086
 
730
- interface LocalDataset {
731
- id: string;
732
- name: string;
733
- path: string;
734
- type: string;
735
- size: number;
736
- sample_size: number;
737
- created_at: string;
738
- source: "local";
1087
+ function formatAutoAgentTurn(turn: AutoAgentTurn): string {
1088
+ const prefix = turn.role === "user" ? "You: " : "Agent: ";
1089
+ return `${prefix}${turn.content}`;
739
1090
  }
740
1091
 
741
- function listLocalDatasets(): LocalDataset[] {
742
- if (!fs.existsSync(DATASETS_DIR)) {
743
- return [];
744
- }
1092
+ function AutoAgentInteractiveSession({
1093
+ conversationId: initialConversationId,
1094
+ history,
1095
+ mode,
1096
+ firstMessage,
1097
+ }: {
1098
+ conversationId?: string;
1099
+ history?: api.AgentChatHistoryItem[];
1100
+ mode: "standard" | "research";
1101
+ firstMessage?: string;
1102
+ }) {
1103
+ const [input, setInput] = useState("");
1104
+ const [isLoading, setIsLoading] = useState(false);
1105
+ const [error, setError] = useState("");
1106
+ const [statusHint, setStatusHint] = useState("Start typing...");
1107
+ const [conversationId, setConversationId] = useState(initialConversationId);
1108
+ const [historyState, setHistoryState] = useState<api.AgentChatHistoryItem[]>(history ?? []);
1109
+ const [turns, setTurns] = useState<AutoAgentTurn[]>(
1110
+ (history ?? []).map((entry) =>
1111
+ entry.role === "assistant"
1112
+ ? { role: "assistant", content: entry.content }
1113
+ : { role: "user", content: entry.content }
1114
+ )
1115
+ );
1116
+ const didSeedFirstMessage = useRef(false);
745
1117
 
746
- const files = fs.readdirSync(DATASETS_DIR).filter((f) => f.endsWith(".json"));
747
- return files.map((filename) => {
748
- const filePath = path.join(DATASETS_DIR, filename);
749
- const stats = fs.statSync(filePath);
750
- let data: unknown[] = [];
751
- let type = "unknown";
1118
+ const runAutoTurn = async (rawMessage: string) => {
1119
+ const trimmed = rawMessage.trim();
1120
+ if (!trimmed || isLoading) {
1121
+ return;
1122
+ }
752
1123
 
753
- try {
754
- const content = JSON.parse(fs.readFileSync(filePath, "utf-8"));
755
- data = Array.isArray(content) ? content : content.data ?? [];
756
- const firstItem = data[0] as Record<string, unknown> | undefined;
757
- type = content.task_type ?? (firstItem?.spans ? "ner" : firstItem?.label ? "classification" : "custom");
758
- } catch {
759
- // Ignore parse errors
1124
+ const nextHistory: api.AgentChatHistoryItem[] = [
1125
+ ...historyState,
1126
+ { role: "user", content: trimmed },
1127
+ ];
1128
+ setHistoryState(nextHistory);
1129
+ setTurns((current) => [...current, { role: "user", content: trimmed }]);
1130
+ setInput("");
1131
+ setError("");
1132
+ setIsLoading(true);
1133
+ setStatusHint("Thinking...");
1134
+
1135
+ const result = await api.agentChat({
1136
+ message: trimmed,
1137
+ ...(conversationId ? { conversation_id: conversationId } : {}),
1138
+ ...(nextHistory.length ? { history: nextHistory } : {}),
1139
+ });
1140
+
1141
+ if (!result.ok) {
1142
+ if (
1143
+ result.status === 403 &&
1144
+ (result.error ?? "").toLowerCase().includes("deep research mode")
1145
+ ) {
1146
+ setError(
1147
+ "Research mode requires a Pro subscription.\n" +
1148
+ "To run this command, upgrade your account to Pro and retry.\n" +
1149
+ "Go to Manage subscription under Billing at https://agent.pioneer.ai/account."
1150
+ );
1151
+ } else {
1152
+ setError(result.error || "Agent request failed.");
1153
+ }
1154
+ setIsLoading(false);
1155
+ setStatusHint("Ready for next message.");
1156
+ return;
760
1157
  }
761
1158
 
762
- return {
763
- id: filename.replace(".json", ""),
764
- name: filename.replace(".json", ""),
765
- path: filePath,
766
- type,
767
- size: stats.size,
768
- sample_size: data.length,
769
- created_at: stats.birthtime.toISOString(),
770
- source: "local" as const,
771
- };
772
- });
773
- }
1159
+ const answer = result.data.answer?.trim() || "No response content yet.";
1160
+ const nextConversationId = result.data.conversation_id || conversationId;
774
1161
 
775
- function saveLocalDataset(name: string, data: unknown, type: string): string {
776
- ensureDatasetsDir();
777
- const filename = `${name.replace(/[^a-zA-Z0-9-_]/g, "_")}.json`;
778
- const filePath = path.join(DATASETS_DIR, filename);
779
- fs.writeFileSync(filePath, JSON.stringify({ task_type: type, data }, null, 2));
780
- return filePath;
781
- }
1162
+ setConversationId(nextConversationId);
1163
+ setTurns((current) => [...current, { role: "assistant", content: answer }]);
1164
+ setHistoryState((current) => [...current, { role: "assistant", content: answer }]);
1165
+ setStatusHint("Ready for next message.");
1166
+ setIsLoading(false);
1167
+ };
782
1168
 
783
- // ─────────────────────────────────────────────────────────────────────────────
784
- // Generate Command (saves to local file when --save is false)
785
- // ─────────────────────────────────────────────────────────────────────────────
1169
+ useEffect(() => {
1170
+ if (!firstMessage || didSeedFirstMessage.current) {
1171
+ return;
1172
+ }
786
1173
 
787
- interface GenerateCommandProps<T> {
788
- action: () => Promise<api.ApiResult<T>>;
789
- datasetName: string;
790
- datasetType: string;
791
- saveToRemote: boolean;
792
- }
1174
+ didSeedFirstMessage.current = true;
1175
+ void runAutoTurn(firstMessage);
1176
+ }, [firstMessage]);
793
1177
 
794
- interface GenerateResult {
795
- data?: unknown[];
796
- success?: boolean;
797
- dataset?: { id?: string; dataset_name?: string };
1178
+ return (
1179
+ <Box flexDirection="column">
1180
+ <Text bold>Agent mode selected.</Text>
1181
+ <Text>
1182
+ Mode: {mode === "research" ? "research" : "standard"}{" "}
1183
+ {conversationId ? `(conversation ${conversationId})` : "(new conversation)"}
1184
+ </Text>
1185
+ <Box flexDirection="column" marginTop={1}>
1186
+ {turns.map((entry, idx) => (
1187
+ <Text key={`agent-turn-${idx}`} dimColor={entry.role === "user"}>
1188
+ {formatAutoAgentTurn(entry)}
1189
+ </Text>
1190
+ ))}
1191
+ </Box>
1192
+ <Box marginTop={1}>
1193
+ {isLoading ? <Loading message={statusHint} /> : <Text color="cyan">&gt; </Text>}
1194
+ {isLoading ? null : (
1195
+ <TextInput
1196
+ value={input}
1197
+ onChange={setInput}
1198
+ onSubmit={runAutoTurn}
1199
+ />
1200
+ )}
1201
+ </Box>
1202
+ {error && (
1203
+ <Box marginTop={1}>
1204
+ <ErrorMessage error={error} />
1205
+ </Box>
1206
+ )}
1207
+ <Text dimColor>Type Ctrl+C to exit.</Text>
1208
+ </Box>
1209
+ );
798
1210
  }
799
1211
 
800
- function GenerateCommand<T extends GenerateResult>({
801
- action,
802
- datasetName,
803
- datasetType,
804
- saveToRemote,
805
- }: GenerateCommandProps<T>) {
806
- const { exit } = useApp();
807
- const [state, setState] = useState<"loading" | "done" | "error">("loading");
808
- const [savedPath, setSavedPath] = useState<string | null>(null);
809
- const [savedDatasetId, setSavedDatasetId] = useState<string | null>(null);
810
- const [remoteSaveFailed, setRemoteSaveFailed] = useState(false);
811
- const [count, setCount] = useState(0);
812
- const [error, setError] = useState("");
1212
+ function AgentInteractivePrompt({
1213
+ conversationId,
1214
+ history,
1215
+ mode,
1216
+ }: {
1217
+ conversationId?: string;
1218
+ history?: api.AgentChatHistoryItem[];
1219
+ mode?: "standard" | "research";
1220
+ }) {
1221
+ const [input, setInput] = useState("");
1222
+ const [isReady, setReady] = useState(false);
1223
+ const [message, setMessage] = useState("");
1224
+ const { isRawModeSupported } = useStdin();
1225
+ const shouldExitImmediately = !isRawModeSupported;
813
1226
 
814
1227
  useEffect(() => {
815
- (async () => {
816
- const result = await action();
817
- if (result.ok && result.data) {
818
- const data = result.data.data ?? [];
819
- setCount(Array.isArray(data) ? data.length : 0);
1228
+ if (!shouldExitImmediately) {
1229
+ return;
1230
+ }
820
1231
 
821
- // Check if dataset was saved remotely
822
- const savedDataset = result.data.dataset;
823
- if (savedDataset?.id) {
824
- setSavedDatasetId(savedDataset.id);
825
- } else if (!saveToRemote) {
826
- // Save to local file only if not saving remotely
827
- const name = datasetName || `${datasetType}-${Date.now()}`;
828
- const filePath = saveLocalDataset(name, data, datasetType);
829
- setSavedPath(filePath);
830
- } else {
831
- // Remote save was requested but no dataset returned - save locally as fallback
832
- setRemoteSaveFailed(true);
833
- const name = datasetName || `${datasetType}-${Date.now()}`;
834
- const filePath = saveLocalDataset(name, data, datasetType);
835
- setSavedPath(filePath);
1232
+ const timeout = setTimeout(() => {
1233
+ process.exit(0);
1234
+ }, 75);
1235
+
1236
+ return () => clearTimeout(timeout);
1237
+ }, [shouldExitImmediately]);
1238
+
1239
+ if (!isRawModeSupported) {
1240
+ return (
1241
+ <Box flexDirection="column">
1242
+ <ErrorMessage error="Interactive input is not supported in this terminal." />
1243
+ <Text>Use interactive mode for this environment:</Text>
1244
+ <Text dimColor>{` agent${mode === "research" ? " --mode research" : ""}`}</Text>
1245
+ <Text dimColor> agent --help</Text>
1246
+ </Box>
1247
+ );
1248
+ }
1249
+
1250
+ if (!isReady) {
1251
+ return (
1252
+ <Box flexDirection="column">
1253
+ <Text bold>Agent mode selected.</Text>
1254
+ <Text>Type your message and press enter to start:</Text>
1255
+ <Box>
1256
+ <Text color="cyan">&gt; </Text>
1257
+ <TextInput
1258
+ value={input}
1259
+ onChange={setInput}
1260
+ onSubmit={(value) => {
1261
+ const trimmed = value.trim();
1262
+ if (!trimmed) {
1263
+ return;
1264
+ }
1265
+ setMessage(trimmed);
1266
+ setReady(true);
1267
+ }}
1268
+ />
1269
+ </Box>
1270
+ <Text dimColor>Type Ctrl+C to cancel.</Text>
1271
+ </Box>
1272
+ );
1273
+ }
1274
+
1275
+ return (
1276
+ <AutoAgentInteractiveSession
1277
+ conversationId={conversationId}
1278
+ history={history}
1279
+ mode={mode === "research" ? "research" : "standard"}
1280
+ firstMessage={message}
1281
+ />
1282
+ );
1283
+ }
1284
+
1285
+ // ─────────────────────────────────────────────────────────────────────────────
1286
+ // Interactive Model Create Selector
1287
+ // ─────────────────────────────────────────────────────────────────────────────
1288
+
1289
+ function ModelCreateInteractive({
1290
+ name,
1291
+ icon,
1292
+ repo,
1293
+ description,
1294
+ example,
1295
+ }: {
1296
+ name?: string;
1297
+ icon?: string;
1298
+ repo?: string;
1299
+ description?: string;
1300
+ example?: CreateProjectExample;
1301
+ }) {
1302
+ const [query, setQuery] = useState("");
1303
+ const [models, setModels] = useState<api.BaseModelInfo[]>([]);
1304
+ const [loading, setLoading] = useState(true);
1305
+ const [error, setError] = useState("");
1306
+ const [selectedModelId, setSelectedModelId] = useState<string | null>(null);
1307
+ const [highlightIndex, setHighlightIndex] = useState(0);
1308
+
1309
+ useEffect(() => {
1310
+ (async () => {
1311
+ const result = await api.listBaseModels();
1312
+ if (!result.ok) {
1313
+ setError(result.error ?? "Unable to load base models.");
1314
+ setLoading(false);
1315
+ return;
1316
+ }
1317
+ const modelsData = result.data
1318
+ ? Array.isArray((result.data as api.BaseModelsResponse).models)
1319
+ ? (result.data as api.BaseModelsResponse).models
1320
+ : (result.data as api.BaseModelInfo[])
1321
+ : [];
1322
+ setModels(modelsData.sort((a, b) => a.id.localeCompare(b.id)));
1323
+ setLoading(false);
1324
+ })();
1325
+ }, []);
1326
+
1327
+ const normalizedQuery = query.trim().toLowerCase();
1328
+ const matchingModels = models.filter((model) => {
1329
+ const text = `${model.id} ${model.name ?? ""} ${model.label ?? ""} ${model.description ?? ""}`.toLowerCase();
1330
+ return normalizedQuery.length === 0 || text.includes(normalizedQuery);
1331
+ });
1332
+ const topMatches = matchingModels;
1333
+ useEffect(() => {
1334
+ setHighlightIndex(0);
1335
+ }, [query]);
1336
+
1337
+ const resolveModelId = (queryValue: string): string | undefined => {
1338
+ const trimmed = queryValue.trim();
1339
+ if (!trimmed) {
1340
+ return topMatches[highlightIndex]?.id;
1341
+ }
1342
+ const exact = models.find(
1343
+ (model) => model.id.toLowerCase() === trimmed.toLowerCase()
1344
+ );
1345
+ if (exact) return exact.id;
1346
+ return topMatches[highlightIndex]?.id;
1347
+ };
1348
+
1349
+ useEffect(() => {
1350
+ if (highlightIndex >= topMatches.length) {
1351
+ setHighlightIndex(topMatches.length > 0 ? topMatches.length - 1 : 0);
1352
+ }
1353
+ }, [highlightIndex, topMatches.length]);
1354
+
1355
+ useInput((_, key) => {
1356
+ if (!topMatches.length) return;
1357
+ if (key.upArrow) {
1358
+ setHighlightIndex((idx) => (idx === 0 ? topMatches.length - 1 : idx - 1));
1359
+ return;
1360
+ }
1361
+ if (key.downArrow) {
1362
+ setHighlightIndex((idx) => (idx === topMatches.length - 1 ? 0 : idx + 1));
1363
+ return;
1364
+ }
1365
+ if (key.return) {
1366
+ const resolvedModel = resolveModelId(query);
1367
+ if (!resolvedModel) {
1368
+ setError("No matching models found. Refine your search and try again.");
1369
+ return;
1370
+ }
1371
+ setSelectedModelId(resolvedModel);
1372
+ }
1373
+ });
1374
+
1375
+ if (error) {
1376
+ return (
1377
+ <Box flexDirection="column">
1378
+ <ErrorMessage error={error} />
1379
+ <Text> </Text>
1380
+ <Text dimColor>Try again with --model explicitly, or check your API connectivity.</Text>
1381
+ </Box>
1382
+ );
1383
+ }
1384
+
1385
+ if (selectedModelId) {
1386
+ return (
1387
+ <ApiCommand
1388
+ action={() =>
1389
+ api.createProject({
1390
+ name: name ?? selectedModelId,
1391
+ ...(icon ? { icon } : {}),
1392
+ ...(repo ? { repo } : {}),
1393
+ ...(description ? { description } : {}),
1394
+ active_model_id: selectedModelId,
1395
+ selected_model_id: selectedModelId,
1396
+ ...(example ? { example } : {}),
1397
+ })
836
1398
  }
1399
+ successMessage="Model entry created"
1400
+ />
1401
+ );
1402
+ }
1403
+
1404
+ if (loading) {
1405
+ return <Loading message="Loading supported models..." />;
1406
+ }
1407
+
1408
+ return (
1409
+ <Box flexDirection="column">
1410
+ <Text bold>Choose a base model for this entry</Text>
1411
+ <Text>Type to filter. Use ↑/↓ to navigate and Enter to select.</Text>
1412
+ <Text dimColor>Type any part of model id, name, label, or description.</Text>
1413
+ <Text> </Text>
1414
+ <TextInput
1415
+ value={query}
1416
+ onChange={(value) => setQuery(value)}
1417
+ onSubmit={(value) => {
1418
+ const resolvedModel = resolveModelId(value);
1419
+ if (!resolvedModel) {
1420
+ setError("No matching models found. Refine your search and try again.");
1421
+ return;
1422
+ }
1423
+ setSelectedModelId(resolvedModel);
1424
+ }}
1425
+ placeholder="e.g. qwen/qwen3-8b"
1426
+ />
1427
+ <Text> </Text>
1428
+ {topMatches.length === 0 ? (
1429
+ <Text dimColor>No matching models found.</Text>
1430
+ ) : (
1431
+ <Box flexDirection="column">
1432
+ {topMatches.map((model, index) => {
1433
+ const suffix = [model.label, model.task_type, model.type].filter(Boolean).join(" · ");
1434
+ const modelLine = `${model.id}${suffix ? ` (${suffix})` : ""}`;
1435
+ const isHighlighted = index === highlightIndex;
1436
+ return (
1437
+ <Text key={model.id} color={isHighlighted ? "cyan" : undefined} bold={isHighlighted}>
1438
+ {isHighlighted ? "▶ " : " "}
1439
+ {modelLine}
1440
+ </Text>
1441
+ );
1442
+ })}
1443
+ </Box>
1444
+ )}
1445
+ <Text> </Text>
1446
+ <Text dimColor>Press Enter to select the highlighted model.</Text>
1447
+ <Text dimColor> </Text>
1448
+ <Text dimColor>Tip: You can still run {"model endpoints create --model \"<base-model-id>\""} for exact model ids.</Text>
1449
+ </Box>
1450
+ );
1451
+ }
1452
+
1453
+ // ─────────────────────────────────────────────────────────────────────────────
1454
+ // Job Logs Command (prettified output)
1455
+ // ─────────────────────────────────────────────────────────────────────────────
1456
+
1457
+ function JobLogsCommand({ jobId }: { jobId: string }) {
1458
+ const { exit } = useApp();
1459
+ const [state, setState] = useState<"loading" | "done" | "error">("loading");
1460
+ const [logs, setLogs] = useState<api.TrainingLog[]>([]);
1461
+ const [error, setError] = useState("");
1462
+
1463
+ useEffect(() => {
1464
+ (async () => {
1465
+ const result = await api.getJobLogs(jobId);
1466
+ if (result.ok && result.data) {
1467
+ setLogs(result.data.logs || []);
837
1468
  setState("done");
838
1469
  } else {
839
1470
  setError(result.error ?? "Unknown error");
@@ -841,7 +1472,7 @@ function GenerateCommand<T extends GenerateResult>({
841
1472
  }
842
1473
  setTimeout(() => exit(), 500);
843
1474
  })();
844
- }, [action, exit, datasetName, datasetType, saveToRemote]);
1475
+ }, [jobId, exit]);
845
1476
 
846
1477
  if (state === "loading") {
847
1478
  return <Loading />;
@@ -851,287 +1482,178 @@ function GenerateCommand<T extends GenerateResult>({
851
1482
  return <ErrorMessage error={error} />;
852
1483
  }
853
1484
 
1485
+ const formatLogEntry = (log: api.TrainingLog): string => {
1486
+ const ts = new Date(log.timestamp).toLocaleTimeString();
1487
+ return `[${ts}] [${log.level}] ${log.message}`;
1488
+ };
1489
+
854
1490
  return (
855
1491
  <Box flexDirection="column">
856
- <Success message={`${datasetType.toUpperCase()} dataset generated (${count} examples)`} />
857
- {savedDatasetId && (
858
- <Text color="green">Saved to remote: {savedDatasetId}</Text>
859
- )}
860
- {remoteSaveFailed && (
861
- <Text color="yellow">⚠ Remote save failed (check server logs). Saved locally instead.</Text>
862
- )}
863
- {savedPath && (
864
- <Text color="cyan">Saved to: {savedPath}</Text>
1492
+ {logs.length === 0 ? (
1493
+ <Text dimColor>No logs available</Text>
1494
+ ) : (
1495
+ logs.map((log) => (
1496
+ <Text key={log.id} color={log.level === "ERROR" ? "red" : log.level === "WARNING" ? "yellow" : undefined}>
1497
+ {formatLogEntry(log)}
1498
+ </Text>
1499
+ ))
865
1500
  )}
866
1501
  </Box>
867
1502
  );
868
1503
  }
869
1504
 
870
1505
  // ─────────────────────────────────────────────────────────────────────────────
871
- // Notebook Run Command
1506
+ // Local Dataset Helpers
872
1507
  // ─────────────────────────────────────────────────────────────────────────────
873
1508
 
874
- interface NotebookCell {
875
- cell_type: "code" | "markdown" | "raw";
876
- source: string[];
877
- metadata?: Record<string, unknown>;
878
- }
879
-
880
- interface NotebookFile {
881
- cells: NotebookCell[];
882
- metadata?: Record<string, unknown>;
883
- nbformat: number;
884
- nbformat_minor: number;
885
- }
1509
+ const DATASETS_DIR = path.join(process.cwd(), "datasets");
886
1510
 
887
- function readNotebookFile(filePath: string): NotebookFile {
888
- const absPath = path.resolve(filePath);
889
- if (!fs.existsSync(absPath)) {
890
- throw new Error(`File not found: ${absPath}`);
1511
+ function ensureDatasetsDir(): void {
1512
+ if (!fs.existsSync(DATASETS_DIR)) {
1513
+ fs.mkdirSync(DATASETS_DIR, { recursive: true });
891
1514
  }
892
- const content = fs.readFileSync(absPath, "utf-8");
893
- return JSON.parse(content) as NotebookFile;
894
1515
  }
895
1516
 
896
- interface NotebookRunCommandProps {
897
- filePath: string;
898
- gpu: string;
899
- loadFelixHelpers: boolean;
1517
+ interface LocalDataset {
1518
+ id: string;
1519
+ name: string;
1520
+ path: string;
1521
+ type: string;
1522
+ size: number;
1523
+ sample_size: number;
1524
+ created_at: string;
1525
+ source: "local";
900
1526
  }
901
1527
 
902
- function NotebookRunCommand({ filePath, gpu, loadFelixHelpers }: NotebookRunCommandProps) {
903
- const { exit } = useApp();
904
- const [phase, setPhase] = useState<"reading" | "session" | "running" | "done" | "error">("reading");
905
- const [sessionId, setSessionId] = useState<string | null>(null);
906
- const [currentCell, setCurrentCell] = useState(0);
907
- const [totalCells, setTotalCells] = useState(0);
908
- const [outputs, setOutputs] = useState<Array<{ cell: number; source: string; result: api.ExecuteCodeResponse | null; error?: string }>>([]);
909
- const [error, setError] = useState("");
910
-
911
- useEffect(() => {
912
- (async () => {
913
- // 1. Read notebook
914
- let notebook: NotebookFile;
915
- try {
916
- notebook = readNotebookFile(filePath);
917
- } catch (e) {
918
- setError(e instanceof Error ? e.message : String(e));
919
- setPhase("error");
920
- setTimeout(() => exit(), 500);
921
- return;
922
- }
1528
+ function listLocalDatasets(): LocalDataset[] {
1529
+ if (!fs.existsSync(DATASETS_DIR)) {
1530
+ return [];
1531
+ }
923
1532
 
924
- const codeCells = notebook.cells.filter((c) => c.cell_type === "code");
925
- setTotalCells(codeCells.length);
1533
+ const files = fs.readdirSync(DATASETS_DIR).filter((f) => f.endsWith(".json"));
1534
+ return files.map((filename) => {
1535
+ const filePath = path.join(DATASETS_DIR, filename);
1536
+ const stats = fs.statSync(filePath);
1537
+ let data: unknown[] = [];
1538
+ let type = "unknown";
926
1539
 
927
- if (codeCells.length === 0) {
928
- setError("No code cells found in notebook");
929
- setPhase("error");
930
- setTimeout(() => exit(), 500);
931
- return;
932
- }
1540
+ try {
1541
+ const content = JSON.parse(fs.readFileSync(filePath, "utf-8"));
1542
+ data = Array.isArray(content) ? content : content.data ?? [];
1543
+ const firstItem = data[0] as Record<string, unknown> | undefined;
1544
+ type = content.task_type ?? (firstItem?.spans ? "ner" : firstItem?.label ? "classification" : "custom");
1545
+ } catch {
1546
+ // Ignore parse errors
1547
+ }
933
1548
 
934
- // 2. Create session
935
- setPhase("session");
936
- const sessionResult = await api.createNotebookSession({
937
- gpu,
938
- load_felix_helpers: loadFelixHelpers,
939
- });
1549
+ return {
1550
+ id: filename.replace(".json", ""),
1551
+ name: filename.replace(".json", ""),
1552
+ path: filePath,
1553
+ type,
1554
+ size: stats.size,
1555
+ sample_size: data.length,
1556
+ created_at: stats.birthtime.toISOString(),
1557
+ source: "local" as const,
1558
+ };
1559
+ });
1560
+ }
940
1561
 
941
- if (!sessionResult.ok || !sessionResult.data) {
942
- setError(sessionResult.error ?? "Failed to create notebook session");
943
- setPhase("error");
944
- setTimeout(() => exit(), 500);
945
- return;
946
- }
1562
+ function saveLocalDataset(name: string, data: unknown, type: string): string {
1563
+ ensureDatasetsDir();
1564
+ const filename = `${name.replace(/[^a-zA-Z0-9-_]/g, "_")}.json`;
1565
+ const filePath = path.join(DATASETS_DIR, filename);
1566
+ fs.writeFileSync(filePath, JSON.stringify({ task_type: type, data }, null, 2));
1567
+ return filePath;
1568
+ }
947
1569
 
948
- let sid = sessionResult.data.session_id;
949
- setSessionId(sid);
950
-
951
- // Poll until session is ready (if status is 'creating')
952
- if (sessionResult.data.status === "creating") {
953
- let ready = false;
954
- for (let attempt = 0; attempt < 60; attempt++) {
955
- await new Promise((r) => setTimeout(r, 2000));
956
- const status = await api.getNotebookSessionStatus(sid);
957
- if (status.ok && status.data?.status === "ready") {
958
- // Status response returns the real session_id when ready
959
- sid = status.data.session_id;
960
- setSessionId(sid);
961
- ready = true;
962
- break;
963
- }
964
- if (status.ok && status.data?.status === "failed") {
965
- setError(status.data.error ?? "Session creation failed");
966
- setPhase("error");
967
- setTimeout(() => exit(), 500);
968
- return;
969
- }
970
- }
971
- if (!ready) {
972
- setError("Session creation timed out after 2 minutes");
973
- setPhase("error");
974
- setTimeout(() => exit(), 500);
975
- return;
976
- }
977
- }
1570
+ // ─────────────────────────────────────────────────────────────────────────────
1571
+ // Generate Command (saves to local file when --save is false)
1572
+ // ─────────────────────────────────────────────────────────────────────────────
978
1573
 
979
- // 3. Execute cells sequentially
980
- setPhase("running");
981
- const cellOutputs: typeof outputs = [];
1574
+ interface GenerateCommandProps<T> {
1575
+ action: () => Promise<api.ApiResult<T>>;
1576
+ datasetName: string;
1577
+ datasetType: string;
1578
+ saveToRemote: boolean;
1579
+ }
982
1580
 
983
- for (let i = 0; i < codeCells.length; i++) {
984
- setCurrentCell(i + 1);
985
- const cell = codeCells[i];
986
- const code = Array.isArray(cell.source) ? cell.source.join("") : String(cell.source);
1581
+ interface GenerateResult {
1582
+ data?: unknown[];
1583
+ success?: boolean;
1584
+ dataset?: { id?: string; dataset_name?: string };
1585
+ }
987
1586
 
988
- if (!code.trim()) {
989
- cellOutputs.push({ cell: i + 1, source: code, result: null });
990
- continue;
991
- }
1587
+ function GenerateCommand<T extends GenerateResult>({
1588
+ action,
1589
+ datasetName,
1590
+ datasetType,
1591
+ saveToRemote,
1592
+ }: GenerateCommandProps<T>) {
1593
+ const { exit } = useApp();
1594
+ const [state, setState] = useState<"loading" | "done" | "error">("loading");
1595
+ const [savedPath, setSavedPath] = useState<string | null>(null);
1596
+ const [savedDatasetId, setSavedDatasetId] = useState<string | null>(null);
1597
+ const [remoteSaveFailed, setRemoteSaveFailed] = useState(false);
1598
+ const [count, setCount] = useState(0);
1599
+ const [error, setError] = useState("");
992
1600
 
993
- const execResult = await api.executeNotebookCode({
994
- code,
995
- session_id: sid,
996
- cell_id: `cell-${i}`,
997
- });
1601
+ useEffect(() => {
1602
+ (async () => {
1603
+ const result = await action();
1604
+ if (result.ok && result.data) {
1605
+ const data = result.data.data ?? [];
1606
+ setCount(Array.isArray(data) ? data.length : 0);
998
1607
 
999
- if (!execResult.ok) {
1000
- cellOutputs.push({ cell: i + 1, source: code, result: null, error: execResult.error ?? "Execution failed" });
1001
- // Continue running remaining cells despite errors
1608
+ // Check if dataset was saved remotely
1609
+ const savedDataset = result.data.dataset;
1610
+ if (savedDataset?.id) {
1611
+ setSavedDatasetId(savedDataset.id);
1612
+ } else if (!saveToRemote) {
1613
+ // Save to local file only if not saving remotely
1614
+ const name = datasetName || `${datasetType}-${Date.now()}`;
1615
+ const filePath = saveLocalDataset(name, data, datasetType);
1616
+ setSavedPath(filePath);
1002
1617
  } else {
1003
- cellOutputs.push({ cell: i + 1, source: code, result: execResult.data ?? null });
1618
+ // Remote save was requested but no dataset returned - save locally as fallback
1619
+ setRemoteSaveFailed(true);
1620
+ const name = datasetName || `${datasetType}-${Date.now()}`;
1621
+ const filePath = saveLocalDataset(name, data, datasetType);
1622
+ setSavedPath(filePath);
1004
1623
  }
1005
-
1006
- setOutputs([...cellOutputs]);
1624
+ setState("done");
1625
+ } else {
1626
+ setError(result.error ?? "Unknown error");
1627
+ setState("error");
1007
1628
  }
1008
-
1009
- setPhase("done");
1010
- setTimeout(() => exit(), 1000);
1629
+ setTimeout(() => exit(), 500);
1011
1630
  })();
1012
- }, [filePath, gpu, loadFelixHelpers, exit]);
1013
-
1014
- if (phase === "error") {
1015
- return <ErrorMessage error={error} />;
1016
- }
1631
+ }, [action, exit, datasetName, datasetType, saveToRemote]);
1017
1632
 
1018
- if (phase === "reading") {
1019
- return <Loading message="Reading notebook..." />;
1633
+ if (state === "loading") {
1634
+ return <Loading />;
1020
1635
  }
1021
1636
 
1022
- if (phase === "session") {
1023
- return <Loading message={`Creating ${gpu} session...`} />;
1637
+ if (state === "error") {
1638
+ return <ErrorMessage error={error} />;
1024
1639
  }
1025
1640
 
1026
- // Render outputs
1027
- const renderCellOutput = (entry: (typeof outputs)[0]) => {
1028
- const lines: string[] = [];
1029
-
1030
- if (entry.error) {
1031
- lines.push(` Error: ${entry.error}`);
1032
- return lines.join("\n");
1033
- }
1034
-
1035
- if (!entry.result) {
1036
- lines.push(" (empty cell)");
1037
- return lines.join("\n");
1038
- }
1039
-
1040
- for (const out of entry.result.outputs) {
1041
- if (out.type === "stream" && out.text) {
1042
- // Truncate long stream output
1043
- const text = out.text.length > 2000 ? out.text.slice(0, 2000) + "\n ... (truncated)" : out.text;
1044
- lines.push(text);
1045
- } else if (out.type === "execute_result" || out.type === "display_data") {
1046
- if (out.data?.["text/plain"]) {
1047
- lines.push(out.data["text/plain"]);
1048
- } else if (out.text) {
1049
- lines.push(out.text);
1050
- }
1051
- if (out.data?.["image/png"]) {
1052
- lines.push(" [image output]");
1053
- }
1054
- } else if (out.type === "error") {
1055
- lines.push(` ${out.ename}: ${out.evalue}`);
1056
- if (out.traceback) {
1057
- // Strip ANSI codes for cleaner terminal output
1058
- lines.push(out.traceback.map((l) => l.replace(/\x1b\[[0-9;]*m/g, "")).join("\n"));
1059
- }
1060
- }
1061
- }
1062
-
1063
- if (entry.result.execution_time_ms) {
1064
- lines.push(` (${(entry.result.execution_time_ms / 1000).toFixed(1)}s)`);
1065
- }
1066
-
1067
- return lines.join("\n") || " (no output)";
1068
- };
1069
-
1070
1641
  return (
1071
1642
  <Box flexDirection="column">
1072
- {phase === "running" && (
1073
- <Box>
1074
- <Text color="yellow"><Spinner type="dots" /></Text>
1075
- <Text> Running cell {currentCell}/{totalCells}...</Text>
1076
- </Box>
1643
+ <Success message={`${datasetType.toUpperCase()} dataset generated (${count} examples)`} />
1644
+ {savedDatasetId && (
1645
+ <Text color="green">Saved to remote: {savedDatasetId}</Text>
1077
1646
  )}
1078
-
1079
- <Static items={outputs}>
1080
- {(entry) => (
1081
- <Box key={entry.cell} flexDirection="column" marginBottom={1}>
1082
- <Text bold color="cyan">── Cell {entry.cell}/{totalCells} ──</Text>
1083
- <Text dimColor>{entry.source.split("\n").slice(0, 3).join("\n")}{entry.source.split("\n").length > 3 ? "\n ..." : ""}</Text>
1084
- <Text color={entry.error || (entry.result && !entry.result.success) ? "red" : undefined}>
1085
- {renderCellOutput(entry)}
1086
- </Text>
1087
- </Box>
1088
- )}
1089
- </Static>
1090
-
1091
- {phase === "done" && (
1092
- <Box flexDirection="column" marginTop={1}>
1093
- <Success message={`Notebook complete: ${outputs.length} cells executed`} />
1094
- {sessionId && <Text dimColor>Session: {sessionId}</Text>}
1095
- {outputs.some((o) => o.error || (o.result && !o.result.success)) && (
1096
- <Text color="yellow">Some cells had errors — check output above</Text>
1097
- )}
1098
- </Box>
1647
+ {remoteSaveFailed && (
1648
+ <Text color="yellow">⚠ Remote save failed (check server logs). Saved locally instead.</Text>
1649
+ )}
1650
+ {savedPath && (
1651
+ <Text color="cyan">Saved to: {savedPath}</Text>
1099
1652
  )}
1100
1653
  </Box>
1101
1654
  );
1102
1655
  }
1103
1656
 
1104
- function createBlankNotebook(name: string): string {
1105
- const notebook: NotebookFile = {
1106
- cells: [
1107
- {
1108
- cell_type: "markdown",
1109
- source: [`# ${name}\n`],
1110
- metadata: {},
1111
- },
1112
- {
1113
- cell_type: "code",
1114
- source: [],
1115
- metadata: {},
1116
- },
1117
- ],
1118
- metadata: {
1119
- kernelspec: { display_name: "Python 3", language: "python", name: "python3" },
1120
- language_info: { name: "python", version: "3.12.0" },
1121
- },
1122
- nbformat: 4,
1123
- nbformat_minor: 4,
1124
- };
1125
-
1126
- const filename = name.endsWith(".ipynb") ? name : `${name}.ipynb`;
1127
- const filePath = path.resolve(filename);
1128
- if (fs.existsSync(filePath)) {
1129
- throw new Error(`File already exists: ${filePath}`);
1130
- }
1131
- fs.writeFileSync(filePath, JSON.stringify(notebook, null, 1));
1132
- return filePath;
1133
- }
1134
-
1135
1657
  // ─────────────────────────────────────────────────────────────────────────────
1136
1658
  // Helper: Infer format from file extension
1137
1659
  // ─────────────────────────────────────────────────────────────────────────────
@@ -1202,177 +1724,19 @@ function DatasetListCommand() {
1202
1724
  </Text>
1203
1725
  <Text dimColor> {ds.id}</Text>
1204
1726
  </Box>
1205
- ))
1206
- )}
1207
- <Text> </Text>
1208
- <Text bold color="cyan">Local Datasets ({localDatasets.length})</Text>
1209
- {localDatasets.length === 0 ? (
1210
- <Text dimColor> No local datasets in ./datasets/</Text>
1211
- ) : (
1212
- localDatasets.map((ds) => (
1213
- <Text key={ds.id}>
1214
- {" "}<Text color="green">{ds.name}</Text> <Text dimColor>({ds.type}, {ds.sample_size} examples)</Text>
1215
- </Text>
1216
- ))
1217
- )}
1218
- </Box>
1219
- );
1220
- }
1221
-
1222
- // ─────────────────────────────────────────────────────────────────────────────
1223
- // Competition List Command
1224
- // ─────────────────────────────────────────────────────────────────────────────
1225
-
1226
- function CompetitionListCommand() {
1227
- const { exit } = useApp();
1228
- const [state, setState] = useState<"loading" | "done" | "error">("loading");
1229
- const [competitions, setCompetitions] = useState<api.CompetitionInfo[]>([]);
1230
- const [error, setError] = useState("");
1231
-
1232
- useEffect(() => {
1233
- (async () => {
1234
- const result = await api.listCompetitions();
1235
- if (result.ok && result.data) {
1236
- setCompetitions(result.data.competitions || []);
1237
- setState("done");
1238
- } else {
1239
- setError(result.error ?? "Unknown error");
1240
- setState("error");
1241
- }
1242
- setTimeout(() => exit(), 500);
1243
- })();
1244
- }, [exit]);
1245
-
1246
- if (state === "loading") {
1247
- return <Loading message="Loading competitions..." />;
1248
- }
1249
-
1250
- if (state === "error") {
1251
- return <ErrorMessage error={error} />;
1252
- }
1253
-
1254
- if (competitions.length === 0) {
1255
- return <Text dimColor>No active competitions found.</Text>;
1256
- }
1257
-
1258
- return (
1259
- <Box flexDirection="column">
1260
- <Text bold color="cyan">Active Competitions ({competitions.length})</Text>
1261
- <Text> </Text>
1262
- {competitions.map((comp, idx) => (
1263
- <Box key={comp.dataset_id} flexDirection="column" marginBottom={1}>
1264
- <Text bold color="yellow">
1265
- {idx + 1}. {comp.dataset_name}
1266
- </Text>
1267
- <Text>
1268
- {" "}Type: <Text color="magenta">{comp.dataset_type}</Text>
1269
- {" "}Samples: <Text color="blue">{comp.sample_count ?? "N/A"}</Text>
1270
- {" "}Entries: <Text color="green">{comp.total_entries}</Text>
1271
- </Text>
1272
- {comp.description && (
1273
- <Text dimColor>{" "}{comp.description}</Text>
1274
- )}
1275
- {comp.labels && comp.labels.length > 0 && (
1276
- <Text>{" "}Labels: <Text color="cyan">{comp.labels.join(", ")}</Text></Text>
1277
- )}
1278
- {comp.winner && (
1279
- <Text>
1280
- {" "}Winner: <Text color="green" bold>{comp.winner.display_name}</Text>
1281
- {" "}(F1: <Text color="yellow">{comp.winner.f1_score.toFixed(4)}</Text>)
1282
- </Text>
1283
- )}
1284
- <Text dimColor>{" "}ID: {comp.dataset_id}</Text>
1285
- </Box>
1286
- ))}
1287
- </Box>
1288
- );
1289
- }
1290
-
1291
- // ─────────────────────────────────────────────────────────────────────────────
1292
- // Leaderboard Command
1293
- // ─────────────────────────────────────────────────────────────────────────────
1294
-
1295
- function LeaderboardCommand({ datasetId, limit }: { datasetId: string; limit?: number }) {
1296
- const { exit } = useApp();
1297
- const [state, setState] = useState<"loading" | "done" | "error">("loading");
1298
- const [data, setData] = useState<api.LeaderboardEntriesResponse | null>(null);
1299
- const [error, setError] = useState("");
1300
-
1301
- useEffect(() => {
1302
- (async () => {
1303
- const result = await api.getLeaderboardEntries(datasetId, limit);
1304
- if (result.ok && result.data) {
1305
- setData(result.data);
1306
- setState("done");
1307
- } else {
1308
- setError(result.error ?? "Unknown error");
1309
- setState("error");
1310
- }
1311
- setTimeout(() => exit(), 500);
1312
- })();
1313
- }, [datasetId, limit, exit]);
1314
-
1315
- if (state === "loading") {
1316
- return <Loading message="Loading leaderboard..." />;
1317
- }
1318
-
1319
- if (state === "error") {
1320
- return <ErrorMessage error={error} />;
1321
- }
1322
-
1323
- if (!data || data.entries.length === 0) {
1324
- return (
1325
- <Box flexDirection="column">
1326
- <Text bold color="cyan">Leaderboard: {data?.dataset_name ?? datasetId}</Text>
1327
- <Text dimColor> No entries yet. Be the first to submit!</Text>
1328
- </Box>
1329
- );
1330
- }
1331
-
1332
- return (
1333
- <Box flexDirection="column">
1334
- <Text bold color="cyan">Leaderboard: {data.dataset_name} ({data.total_entries} entries)</Text>
1335
- <Text> </Text>
1336
- {/* Header */}
1337
- <Box>
1338
- <Box width={6}><Text bold dimColor>Rank</Text></Box>
1339
- <Box width={22}><Text bold dimColor>Name</Text></Box>
1340
- <Box width={22}><Text bold dimColor>Model</Text></Box>
1341
- <Box width={10}><Text bold dimColor>F1</Text></Box>
1342
- <Box width={10}><Text bold dimColor>Precision</Text></Box>
1343
- <Box width={10}><Text bold dimColor>Recall</Text></Box>
1344
- </Box>
1345
- {/* Entries */}
1346
- {data.entries.map((entry, idx) => {
1347
- const rank = entry.rank ?? idx + 1;
1348
- const isFirst = rank === 1;
1349
- return (
1350
- <Box key={entry.id}>
1351
- <Box width={6}>
1352
- <Text color={isFirst ? "yellow" : undefined} bold={isFirst}>
1353
- {isFirst ? `#${rank}` : `#${rank}`}
1354
- </Text>
1355
- </Box>
1356
- <Box width={22}>
1357
- <Text color={isFirst ? "yellow" : undefined} bold={isFirst}>
1358
- {entry.display_name.substring(0, 20)}
1359
- </Text>
1360
- </Box>
1361
- <Box width={22}>
1362
- <Text color="cyan">{entry.model_name.substring(0, 20)}</Text>
1363
- </Box>
1364
- <Box width={10}>
1365
- <Text color="green" bold>{entry.f1_score.toFixed(4)}</Text>
1366
- </Box>
1367
- <Box width={10}>
1368
- <Text>{entry.precision_score?.toFixed(4) ?? "N/A"}</Text>
1369
- </Box>
1370
- <Box width={10}>
1371
- <Text>{entry.recall_score?.toFixed(4) ?? "N/A"}</Text>
1372
- </Box>
1373
- </Box>
1374
- );
1375
- })}
1727
+ ))
1728
+ )}
1729
+ <Text> </Text>
1730
+ <Text bold color="cyan">Local Datasets ({localDatasets.length})</Text>
1731
+ {localDatasets.length === 0 ? (
1732
+ <Text dimColor> No local datasets in ./datasets/</Text>
1733
+ ) : (
1734
+ localDatasets.map((ds) => (
1735
+ <Text key={ds.id}>
1736
+ {" "}<Text color="green">{ds.name}</Text> <Text dimColor>({ds.type}, {ds.sample_size} examples)</Text>
1737
+ </Text>
1738
+ ))
1739
+ )}
1376
1740
  </Box>
1377
1741
  );
1378
1742
  }
@@ -2004,11 +2368,54 @@ function DeployedModelCard({ model, index }: DeployedModelCardProps) {
2004
2368
  );
2005
2369
  }
2006
2370
 
2371
+ // ─────────────────────────────────────────────────────────────────────────────
2372
+ // Registered Model (Project) Visualization Component
2373
+ // ─────────────────────────────────────────────────────────────────────────────
2374
+
2375
+ interface ProjectModelCardProps {
2376
+ model: api.ProjectResponse;
2377
+ index: number;
2378
+ }
2379
+
2380
+ function ProjectModelCard({ model, index }: ProjectModelCardProps) {
2381
+ const formatDateShort = (dateStr: string | null | undefined) => {
2382
+ if (!dateStr) return "N/A";
2383
+ const date = new Date(dateStr);
2384
+ return date.toLocaleDateString() + " " + date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
2385
+ };
2386
+
2387
+ return (
2388
+ <Box flexDirection="column" marginTop={index > 0 ? 0 : 0}>
2389
+ <Box>
2390
+ <Text bold color="green">●</Text>
2391
+ <Text> </Text>
2392
+ <Box width={28}>
2393
+ <Text bold color="cyan">{model.name?.substring(0, 26) || "Unnamed"}</Text>
2394
+ </Box>
2395
+ <Box width={38}>
2396
+ <Text dimColor>{model.id}</Text>
2397
+ </Box>
2398
+ <Box width={24}>
2399
+ <Text color="magenta">{model.selected_model_id || "N/A"}</Text>
2400
+ </Box>
2401
+ <Box width={18}>
2402
+ <Text dimColor>{formatDateShort(model.created_at)}</Text>
2403
+ </Box>
2404
+ </Box>
2405
+ {model.description && (
2406
+ <Box marginLeft={2}>
2407
+ <Text dimColor>{model.description}</Text>
2408
+ </Box>
2409
+ )}
2410
+ </Box>
2411
+ );
2412
+ }
2413
+
2007
2414
  // ─────────────────────────────────────────────────────────────────────────────
2008
2415
  // Model List Command
2009
2416
  // ─────────────────────────────────────────────────────────────────────────────
2010
2417
 
2011
- type ModelListFilter = "all" | "trained" | "deployed";
2418
+ type ModelListFilter = "registered" | "trained" | "deployed" | "artifacts";
2012
2419
 
2013
2420
  interface ModelListCommandProps {
2014
2421
  filter: ModelListFilter;
@@ -2042,11 +2449,42 @@ function ModelListCommand({ filter }: ModelListCommandProps) {
2042
2449
  return <ErrorMessage error={error} />;
2043
2450
  }
2044
2451
 
2045
- const showDeployed = filter === "all" || filter === "deployed";
2046
- const showTrained = filter === "all" || filter === "trained";
2452
+ const showProjects = filter === "registered";
2453
+ const showDeployed = filter === "deployed" || filter === "artifacts";
2454
+ const showTrained = filter === "trained" || filter === "artifacts";
2047
2455
 
2048
2456
  return (
2049
2457
  <Box flexDirection="column">
2458
+ {showProjects && (
2459
+ <>
2460
+ <Text bold color="cyan">Model Entries ({data?.projects.length ?? 0})</Text>
2461
+ {data?.projects.length === 0 ? (
2462
+ <Text dimColor> No model entries</Text>
2463
+ ) : (
2464
+ <Box flexDirection="column" marginTop={1}>
2465
+ <Box marginBottom={0}>
2466
+ <Text bold dimColor> </Text>
2467
+ <Box width={28}>
2468
+ <Text bold dimColor>Name</Text>
2469
+ </Box>
2470
+ <Box width={38}>
2471
+ <Text bold dimColor>Model ID</Text>
2472
+ </Box>
2473
+ <Box width={24}>
2474
+ <Text bold dimColor>Base Model</Text>
2475
+ </Box>
2476
+ <Box width={18}>
2477
+ <Text bold dimColor>Created</Text>
2478
+ </Box>
2479
+ </Box>
2480
+ {data?.projects.map((model, index) => (
2481
+ <ProjectModelCard key={model.id || index} model={model} index={index} />
2482
+ ))}
2483
+ </Box>
2484
+ )}
2485
+ </>
2486
+ )}
2487
+ {showProjects && (showDeployed || showTrained) && <Text> </Text>}
2050
2488
  {showDeployed && (
2051
2489
  <>
2052
2490
  <Text bold color="cyan">Deployed Models ({data?.deployed.length ?? 0})</Text>
@@ -2135,6 +2573,9 @@ interface ModelGenerateCommandProps {
2135
2573
  systemMsg?: string;
2136
2574
  maxTokens: number;
2137
2575
  temperature: number;
2576
+ topP?: number;
2577
+ includeReasoningTrace?: boolean;
2578
+ projectId?: string;
2138
2579
  }
2139
2580
 
2140
2581
  function normalizeBaseModels(
@@ -2157,18 +2598,74 @@ function formatDecoderSuggestions(decoderIds: string[]): string {
2157
2598
  return `\nTry one of these decoder models: ${decoderIds.slice(0, 4).join(", ")}`;
2158
2599
  }
2159
2600
 
2601
+ function normalizeModelId(modelId: string): string {
2602
+ return modelId.trim();
2603
+ }
2604
+
2605
+ function getDecoderTaskType(model: api.BaseModelInfo | null | undefined): string {
2606
+ if (!model) return "";
2607
+ return `${model.task_type ?? model.type ?? ""}`.trim().toLowerCase();
2608
+ }
2609
+
2610
+ function modelSupportsDecoderInference(model: api.BaseModelInfo | null | undefined): boolean {
2611
+ if (!model) return false;
2612
+ if (model.supports_inference !== undefined) {
2613
+ return model.supports_inference;
2614
+ }
2615
+ if (model.supports_on_demand_inference !== undefined) {
2616
+ return model.supports_on_demand_inference;
2617
+ }
2618
+ return true;
2619
+ }
2620
+
2621
+ function isDecoderModel(model: api.BaseModelInfo | null | undefined): boolean {
2622
+ const taskType = getDecoderTaskType(model);
2623
+ return (
2624
+ taskType === "decoder" ||
2625
+ taskType === "llm" ||
2626
+ taskType === "generative"
2627
+ );
2628
+ }
2629
+
2630
+ function shouldFallbackToTextCompletions(errorMessage: string): boolean {
2631
+ const normalized = errorMessage.toLowerCase();
2632
+ return (
2633
+ normalized.includes("/v1/completions") ||
2634
+ normalized.includes("without a chat template") ||
2635
+ normalized.includes("text completion")
2636
+ );
2637
+ }
2638
+
2639
+ function buildTextCompletionPrompt(prompt: string, systemMsg?: string): string {
2640
+ if (!systemMsg) return prompt;
2641
+ return `System instruction:\n${systemMsg}\n\nUser prompt:\n${prompt}\n\nAssistant response:`;
2642
+ }
2643
+
2644
+ function parseCommaSeparated(value?: string): string[] {
2645
+ if (!value) return [];
2646
+ return value
2647
+ .split(",")
2648
+ .map((item) => item.trim())
2649
+ .filter(Boolean);
2650
+ }
2651
+
2160
2652
  function ModelGenerateCommand({
2161
2653
  modelId,
2162
2654
  prompt,
2163
2655
  systemMsg,
2164
2656
  maxTokens,
2165
2657
  temperature,
2658
+ topP,
2659
+ includeReasoningTrace,
2660
+ projectId,
2166
2661
  }: ModelGenerateCommandProps) {
2167
2662
  const { exit } = useApp();
2168
2663
  const [state, setState] = useState<"checking" | "running" | "done" | "error">(
2169
2664
  "checking"
2170
2665
  );
2171
- const [data, setData] = useState<api.InferenceResponse | null>(null);
2666
+ const [data, setData] = useState<api.InferenceResponse | api.TextCompletionResponse | null>(
2667
+ null
2668
+ );
2172
2669
  const [error, setError] = useState("");
2173
2670
 
2174
2671
  useEffect(() => {
@@ -2181,8 +2678,9 @@ function ModelGenerateCommand({
2181
2678
  ? normalizeBaseModels(baseModelsResult.data)
2182
2679
  : [];
2183
2680
  const decoderModelIds = baseModels
2184
- .filter((model) => model.type === "decoder")
2681
+ .filter((model) => isDecoderModel(model) && modelSupportsDecoderInference(model))
2185
2682
  .map((model) => model.id);
2683
+ const normalizedModelId = normalizeModelId(modelId);
2186
2684
 
2187
2685
  const fail = (message: string) => {
2188
2686
  if (!active) return;
@@ -2191,38 +2689,35 @@ function ModelGenerateCommand({
2191
2689
  setTimeout(() => exit(), 500);
2192
2690
  };
2193
2691
 
2194
- if (modelId === "base") {
2195
- fail(
2196
- "Model 'base' is encoder-only and cannot run decoder generation. Use 'model predict' for encoder tasks." +
2197
- formatDecoderSuggestions(decoderModelIds)
2198
- );
2199
- return;
2200
- }
2201
-
2202
- const matchedBaseModel = baseModels.find((model) => model.id === modelId);
2203
- if (matchedBaseModel && matchedBaseModel.type !== "decoder") {
2692
+ const matchedBaseModel = baseModels.find((model) => model.id === normalizedModelId);
2693
+ if (matchedBaseModel && (!isDecoderModel(matchedBaseModel) || !modelSupportsDecoderInference(matchedBaseModel))) {
2204
2694
  const modelName = matchedBaseModel.label || matchedBaseModel.name || matchedBaseModel.id;
2695
+ const taskType = getDecoderTaskType(matchedBaseModel) || "unknown";
2696
+ const supportsInference = modelSupportsDecoderInference(matchedBaseModel);
2205
2697
  fail(
2206
- `Model '${modelName}' is type '${matchedBaseModel.type}' and is not decoder-compatible.` +
2698
+ `Model '${modelName}' is type '${taskType}' and is not decoder-compatible for inference` +
2699
+ `${supportsInference ? "" : " (supports_inference = false)"} ` +
2700
+ `(supports_inference: ${String(matchedBaseModel.supports_inference ?? "unknown")}, task_type: ${taskType}).` +
2207
2701
  formatDecoderSuggestions(decoderModelIds)
2208
2702
  );
2209
2703
  return;
2210
2704
  }
2211
2705
 
2212
- if (modelId.startsWith("base:") && baseModels.length > 0 && !matchedBaseModel) {
2706
+ if (baseModels.length > 0 && !isUuid(normalizedModelId) && !matchedBaseModel) {
2213
2707
  fail(
2214
- `Unknown base model '${modelId}'.` + formatDecoderSuggestions(decoderModelIds)
2708
+ `Model '${modelId}' is not a recognized decoder base model and is not a valid job UUID.` +
2709
+ ` Use 'model list' to copy a full training job UUID or provide a known decoder catalog ID.`
2215
2710
  );
2216
2711
  return;
2217
2712
  }
2218
2713
 
2219
- if (isUuid(modelId)) {
2220
- const jobResult = await api.getJob(modelId);
2714
+ if (isUuid(normalizedModelId)) {
2715
+ const jobResult = await api.getJob(normalizedModelId);
2221
2716
  if (jobResult.ok && jobResult.data) {
2222
2717
  const taskType = jobResult.data.task_type?.toLowerCase();
2223
- if (taskType && taskType !== "decoder") {
2718
+ if (taskType && !isDecoderModel({ task_type: taskType } as api.BaseModelInfo)) {
2224
2719
  fail(
2225
- `Training job '${modelId}' has task_type '${taskType}', so it is not decoder-compatible.` +
2720
+ `Training job '${normalizedModelId}' has task_type '${taskType}', so it is not decoder-compatible.` +
2226
2721
  formatDecoderSuggestions(decoderModelIds)
2227
2722
  );
2228
2723
  return;
@@ -2233,18 +2728,21 @@ function ModelGenerateCommand({
2233
2728
  if (!active) return;
2234
2729
  setState("running");
2235
2730
 
2236
- const messages: Array<{ role: "system" | "user" | "assistant"; content: string }> = [];
2731
+ const messages: api.InferenceMessage[] = [];
2237
2732
  if (systemMsg) {
2238
2733
  messages.push({ role: "system", content: systemMsg });
2239
2734
  }
2240
2735
  messages.push({ role: "user", content: prompt });
2241
2736
 
2242
2737
  const result = await api.runInference({
2243
- model_id: modelId,
2738
+ model_id: normalizedModelId,
2244
2739
  task: "generate",
2245
2740
  messages,
2246
2741
  max_tokens: maxTokens,
2247
2742
  temperature,
2743
+ ...(topP !== undefined ? { top_p: topP } : {}),
2744
+ ...(includeReasoningTrace ? { include_reasoning_trace: true } : {}),
2745
+ ...(projectId ? { project_id: projectId } : {}),
2248
2746
  });
2249
2747
 
2250
2748
  if (!active) return;
@@ -2253,9 +2751,32 @@ function ModelGenerateCommand({
2253
2751
  setData(result.data);
2254
2752
  setState("done");
2255
2753
  } else {
2256
- let message = result.error ?? "Unknown error";
2257
- const normalizedError = formatApiError(message);
2258
- if (normalizedError.toLowerCase().includes("inference failed")) {
2754
+ const primaryError = result.error ?? "Unknown error";
2755
+ const normalizedError = formatApiError(primaryError);
2756
+ let message = normalizedError;
2757
+
2758
+ if (shouldFallbackToTextCompletions(normalizedError)) {
2759
+ const completionResult = await api.runTextCompletion({
2760
+ model: normalizedModelId,
2761
+ prompt: buildTextCompletionPrompt(prompt, systemMsg),
2762
+ max_tokens: maxTokens,
2763
+ temperature,
2764
+ ...(topP !== undefined ? { extra_body: { top_p: topP } } : {}),
2765
+ });
2766
+
2767
+ if (!active) return;
2768
+
2769
+ if (completionResult.ok && completionResult.data) {
2770
+ setData(completionResult.data);
2771
+ setState("done");
2772
+ setTimeout(() => exit(), 500);
2773
+ return;
2774
+ }
2775
+
2776
+ message =
2777
+ "Decoder chat inference failed, and raw text fallback via /v1/completions also failed.\n" +
2778
+ formatApiError(completionResult.error ?? "Unknown error");
2779
+ } else if (normalizedError.toLowerCase().includes("inference failed")) {
2259
2780
  message =
2260
2781
  `${normalizedError}\nDecoder inference request reached the backend but failed to execute.` +
2261
2782
  formatDecoderSuggestions(decoderModelIds);
@@ -2270,7 +2791,17 @@ function ModelGenerateCommand({
2270
2791
  return () => {
2271
2792
  active = false;
2272
2793
  };
2273
- }, [modelId, prompt, systemMsg, maxTokens, temperature, exit]);
2794
+ }, [
2795
+ modelId,
2796
+ prompt,
2797
+ systemMsg,
2798
+ maxTokens,
2799
+ temperature,
2800
+ topP,
2801
+ includeReasoningTrace,
2802
+ projectId,
2803
+ exit,
2804
+ ]);
2274
2805
 
2275
2806
  if (state === "checking") {
2276
2807
  return <Loading message="Checking decoder model compatibility..." />;
@@ -2301,16 +2832,12 @@ type HelpContext =
2301
2832
  | "dataset"
2302
2833
  | "dataset-analyze"
2303
2834
  | "dataset-edit"
2304
- | "project"
2305
2835
  | "job"
2306
2836
  | "model"
2307
- | "chat"
2308
2837
  | "eval"
2309
2838
  | "benchmark"
2310
- | "competition"
2311
- | "notebook"
2312
2839
  | "inference"
2313
- | "adaptive-finetuning";
2840
+ | "agent";
2314
2841
 
2315
2842
  interface HelpProps {
2316
2843
  context?: HelpContext;
@@ -2356,6 +2883,7 @@ const Help: React.FC<HelpProps> = ({ context = "root" }) => {
2356
2883
  <Text> dataset get {"<name[:version]>"} Get dataset details</Text>
2357
2884
  <Text> dataset delete {"<name[:version]>"} Delete a dataset</Text>
2358
2885
  <Text> dataset analyze {"<name[:version]>"} Analyze a dataset</Text>
2886
+ <Text> dataset analyze-llm {"<name[:version]>"} LLM-only dataset analysis</Text>
2359
2887
  <Text> </Text>
2360
2888
  <Text bold> Generate:</Text>
2361
2889
  <Text> dataset generate ner</Text>
@@ -2386,12 +2914,53 @@ const Help: React.FC<HelpProps> = ({ context = "root" }) => {
2386
2914
  <Text> --num {"<n>"} Number of examples (default: 10)</Text>
2387
2915
  <Text> --save true Save to database</Text>
2388
2916
  <Text> --name {"<name>"} Dataset name (required if --save)</Text>
2917
+ <Text> Advanced generation flags:</Text>
2918
+ <Text> --quality {"<light|medium|heavy>"} Generation quality profile</Text>
2919
+ <Text> --generation-profile {"<auto|fast|balanced|quality>"} Runtime profile</Text>
2920
+ <Text> --reasoning-trace {"true|false"} Include reasoning traces (decoder only)</Text>
2921
+ <Text> --reasoning-effort {"<low|medium|high>"} Reasoning effort</Text>
2922
+ <Text> --multiplicator {"<json>"} Multiplicator settings</Text>
2923
+ <Text> --use-meta-felix {"true|false"} Use MetaFelix metadata</Text>
2924
+ <Text> --min-criteria {"<n>"} Minimum diversity criteria</Text>
2925
+ <Text> --target-choices {"<n>"} Diversity target choices</Text>
2926
+ <Text> --project-id {"<id>"} Project ID</Text>
2927
+ <Text> --type {"training|evaluation|split"} Dataset type</Text>
2928
+ <Text> --visibility {"private|public"} Dataset visibility</Text>
2929
+ <Text> --split-ratio {"<train:eval>|{json>}"} Split dataset ratio</Text>
2930
+ <Text> --negative-ratio {"<n>"} Percent negative samples</Text>
2931
+ <Text> --classified-examples {"<json>"} Classified examples with feedback</Text>
2389
2932
  <Text> </Text>
2390
2933
  <Text bold> Infer Labels:</Text>
2391
2934
  <Text> dataset infer ner Infer NER labels from description</Text>
2392
2935
  <Text> dataset infer classification Infer classification labels</Text>
2393
2936
  <Text> dataset infer fields Infer input/output fields</Text>
2394
2937
  <Text> --domain {"<desc>"} Domain description (required)</Text>
2938
+ <Text> dataset infer infer-advanced Infer constraints and multiplicator from a prompt</Text>
2939
+ <Text> --prompt {"<prompt>"} Prompt for inference</Text>
2940
+ <Text> --labels {"<l1,l2,...>"} Optional labels to guide suggestions</Text>
2941
+ <Text> --data-type {"<type>"} entity_extraction|classification|json_extraction</Text>
2942
+ <Text> dataset infer improve-prompt Improve a generation prompt</Text>
2943
+ <Text> --prompt {"<prompt>"} Prompt to improve</Text>
2944
+ <Text> --data-type {"<type>"} Optional prompt domain hint</Text>
2945
+ <Text> dataset label-existing ner Label existing NER texts</Text>
2946
+ <Text> --labels {"<l1,l2,...>"} Labels for entities</Text>
2947
+ <Text> --inputs {"[{\"text\":\"...\"},...]"} Input texts JSON array (required)</Text>
2948
+ <Text> --name {"<name>"} Output dataset name (optional if --save false)</Text>
2949
+ <Text> --project-id {"<project_id>"} Assign output dataset to project</Text>
2950
+ <Text> --save {"<true|false>"} Save dataset (default: false)</Text>
2951
+ <Text> dataset label-existing classification Label existing classification texts</Text>
2952
+ <Text> --labels {"<l1,l2,...>"} Labels for classes</Text>
2953
+ <Text> --inputs {"[{\"text\":\"...\"},...]"} Input texts JSON array (required)</Text>
2954
+ <Text> --name {"<name>"} Output dataset name (optional if --save false)</Text>
2955
+ <Text> --project-id {"<project_id>"} Assign output dataset to project</Text>
2956
+ <Text> --save {"<true|false>"} Save dataset (default: false)</Text>
2957
+ <Text> dataset label-existing fields Label existing structured records</Text>
2958
+ <Text> --input-fields {"[{\"name\":\"...\"},...]"} Input schema fields (required)</Text>
2959
+ <Text> --output-fields {"[{\"name\":\"...\"},...]"} Output schema fields (required)</Text>
2960
+ <Text> --inputs {"[{\"f1\":\"v\"},...]"} Input records JSON array (required)</Text>
2961
+ <Text> --name {"<name>"} Output dataset name (optional if --save false)</Text>
2962
+ <Text> --project-id {"<project_id>"} Assign output dataset to project</Text>
2963
+ <Text> --save {"<true|false>"} Save dataset (default: false)</Text>
2395
2964
  <Text> </Text>
2396
2965
  <Text bold> Upload/Download:</Text>
2397
2966
  <Text> dataset upload {"<file>"} Upload local file to Pioneer</Text>
@@ -2413,6 +2982,8 @@ const Help: React.FC<HelpProps> = ({ context = "root" }) => {
2413
2982
  <Text bold> Data Editing:</Text>
2414
2983
  <Text> dataset edit --help Show data editing commands</Text>
2415
2984
  <Text> dataset edit scan-pii {"<name[:version]>"} Scan for PII</Text>
2985
+ <Text> dataset edit dismiss-outlier {"<name[:version]>"} Dismiss an outlier fingerprint</Text>
2986
+ <Text> --fingerprint {"<hash>"} Outlier fingerprint from dataset analysis</Text>
2416
2987
  <Text> dataset edit subsample {"<name[:version]>"} Create a subsample</Text>
2417
2988
  </Box>
2418
2989
  );
@@ -2431,6 +3002,8 @@ const Help: React.FC<HelpProps> = ({ context = "root" }) => {
2431
3002
  <Text> dataset edit scan-phd {"<name[:version]>"} Scan for prompt injection</Text>
2432
3003
  <Text> --columns {"<col1,col2>"} Columns to scan (optional, scans all if omitted)</Text>
2433
3004
  <Text> --threshold {"<n>"} Detection threshold (default: 0.5)</Text>
3005
+ <Text> dataset edit dismiss-outlier {"<name[:version]>"} Dismiss an outlier fingerprint</Text>
3006
+ <Text> --fingerprint {"<hash>"} Outlier fingerprint from dataset analysis</Text>
2434
3007
  <Text> dataset edit subsample {"<name[:version]>"} Create a subsample</Text>
2435
3008
  <Text> --n {"<count>"} Target sample count (required)</Text>
2436
3009
  <Text> --method {"<type>"} Method: random, balanced, stratified</Text>
@@ -2458,32 +3031,13 @@ const Help: React.FC<HelpProps> = ({ context = "root" }) => {
2458
3031
  <Text> Options: ner, classification, generative</Text>
2459
3032
  <Text> --analyses {"<a1,a2,...>"} Analyses to run (required, comma-separated)</Text>
2460
3033
  <Text> Options: distribution, duplicates, outliers, splits, diversity</Text>
3034
+ <Text> </Text>
3035
+ <Text> LLM-only analysis:</Text>
3036
+ <Text> dataset analyze-llm {"<id>"} --task-type {"<type>"} --description {"<text>"} --labels {"<l1,l2>"} </Text>
2461
3037
  <Text> </Text>
2462
3038
  <Text bold> Example:</Text>
2463
3039
  <Text> dataset analyze abc123 --task-type ner --analyses distribution,duplicates</Text>
2464
- </Box>
2465
- );
2466
- }
2467
-
2468
- // Project help
2469
- if (context === "project") {
2470
- return (
2471
- <Box flexDirection="column">
2472
- <Text bold>Project Commands:</Text>
2473
- <Text> project list List all projects</Text>
2474
- <Text> project get {"<project-id>"} Get project details</Text>
2475
- <Text> project create Create a project</Text>
2476
- <Text> --name {"<name>"} Project name (required)</Text>
2477
- <Text> --icon {"<icon>"} Icon name (optional, default: folder)</Text>
2478
- <Text> --repo {"<repo-url>"} Repository URL/reference (optional)</Text>
2479
- <Text> --description {"<text>"} Description (optional)</Text>
2480
- <Text> --model-id {"<id>"} Selected model ID (optional)</Text>
2481
- <Text> --example {"<json>"} JSON example payload (optional)</Text>
2482
- <Text> project update {"<project-id>"} Update project fields</Text>
2483
- <Text> --name {"<name>"} --icon {"<icon>"} --repo {"<repo-url>"} --description {"<text>"} --model-id {"<id>"}</Text>
2484
- <Text> project delete {"<project-id>"} Delete a project</Text>
2485
- <Text> project dataset-count {"<project-id>"} Show attached dataset count</Text>
2486
- <Text> project quality-metrics {"<project-id>"} Show LLMAJ pass/fail metrics</Text>
3040
+ <Text> dataset analyze-llm abc123 --task-type ner --description "NER quality analysis"</Text>
2487
3041
  </Box>
2488
3042
  );
2489
3043
  }
@@ -2509,156 +3063,139 @@ const Help: React.FC<HelpProps> = ({ context = "root" }) => {
2509
3063
  );
2510
3064
  }
2511
3065
 
2512
- // Model help
2513
- if (context === "model") {
3066
+ // Model endpoints help
3067
+ if (context === "model-endpoints") {
2514
3068
  return (
2515
3069
  <Box flexDirection="column">
2516
- <Text bold>Model Commands:</Text>
2517
- <Text> model list List all models (trained + deployed)</Text>
2518
- <Text> model list trained List trained models only</Text>
2519
- <Text> model list deployed List deployed models only</Text>
2520
- <Text> model delete {"<job-id>"} Undeploy a model by training job ID</Text>
2521
- <Text> model download {"<job-id>"} Get model download URL by job ID</Text>
2522
- <Text> </Text>
2523
- <Text bold> Inference:</Text>
2524
- <Text> model predict {"<job-id>"} --text {"<text>"} --labels {"<labels>"} Run NER inference</Text>
2525
- <Text> --task {"<task>"} Task: extract_entities, classify_text, extract_json (default: extract_entities)</Text>
2526
- <Text> --threshold {"<n>"} Confidence threshold 0-1 (default: 0.5)</Text>
2527
- <Text> model generate {"<model-id>"} --prompt {"<text>"} Run decoder generation</Text>
2528
- <Text> --system {"<text>"} System message (optional)</Text>
2529
- <Text> --max-tokens {"<n>"} Max tokens to generate (default: 256)</Text>
2530
- <Text> --temperature {"<n>"} Sampling temperature (default: 0.7)</Text>
2531
- <Text dimColor> model-id can be a decoder training job UUID or base:{"<provider/model>"}</Text>
2532
- <Text> </Text>
2533
- <Text bold> Upload:</Text>
2534
- <Text> model upload {"<job-id>"} --to hf Upload trained model to Hugging Face</Text>
2535
- <Text> --repo {"<repo>"} HF repo (required, e.g., username/model)</Text>
2536
- <Text> --private Make repo private</Text>
3070
+ <Text bold>Model Endpoint Commands:</Text>
3071
+ <Text> Aliases: model_endpoints ...</Text>
3072
+ <Text> model endpoints list</Text>
3073
+ <Text> model endpoints create</Text>
3074
+ <Text> --name {"<name>"} Optional (defaults to model id)</Text>
3075
+ <Text> --icon {"<icon>"} Optional</Text>
3076
+ <Text> --repo {"<repo-url>"} Optional</Text>
3077
+ <Text> --description {"<text>"} Optional</Text>
3078
+ <Text> --model {"<base-model-id>"} Optional (starts interactive picker when omitted)</Text>
3079
+ <Text> --example {"<json>"} Optional</Text>
3080
+ <Text> model endpoints get {"<model-id>"} Get endpoint/model entry details</Text>
3081
+ <Text> model endpoints update {"<model-id>"} Update endpoint metadata</Text>
3082
+ <Text> --name {"<name>"} --icon {"<icon>"} --repo {"<repo-url>"} --description {"<text>"} --model-id {"<id>"}</Text>
3083
+ <Text> model endpoints delete {"<model-id>"} Delete an endpoint/model entry</Text>
3084
+ <Text> model endpoints dataset-count {"<model-id>"} Get attached dataset count</Text>
3085
+ <Text> model endpoints quality-metrics {"<model-id>"} Show LLMAJ pass/fail metrics</Text>
3086
+ <Text> model endpoints deploy {"<model-id>"} --job {"<training-job-id>"} [--reason {"<text>"}] Deploy a trained job to the endpoint</Text>
3087
+ <Text> model endpoints rollback {"<model-id>"} {"<deployment-id>"} Rollback endpoint to previous deployment</Text>
3088
+ </Box>
3089
+ );
3090
+ }
3091
+
3092
+ // Model artifacts help
3093
+ if (context === "model-artifacts") {
3094
+ return (
3095
+ <Box flexDirection="column">
3096
+ <Text bold>Model Artifact Commands:</Text>
3097
+ <Text> Aliases: model_artifacts ...</Text>
3098
+ <Text> model artifacts list Show both trained and deployed artifacts</Text>
3099
+ <Text> model artifacts trained List trained artifacts</Text>
3100
+ <Text> model artifacts deployed List deployed artifacts</Text>
3101
+ <Text> model artifacts download {"<job-id>"} Download model artifact</Text>
3102
+ <Text> model artifacts delete {"<job-id>"} Delete deployed artifact record</Text>
3103
+ <Text> model artifacts upload {"<job-id>"} --to hf Upload trained model artifact to Hugging Face</Text>
3104
+ <Text> --repo {"<repo>"} HF repo (required, e.g., username/model)</Text>
3105
+ <Text> --private Make repo private</Text>
2537
3106
  <Text dimColor> Note: Set HF token with 'pioneer auth hf' first</Text>
2538
3107
  <Text dimColor> Note: Use full job ID (not partial ID shown in list)</Text>
2539
3108
  </Box>
2540
3109
  );
2541
3110
  }
2542
3111
 
2543
- // Inference help
2544
- if (context === "inference") {
3112
+ // Model help
3113
+ if (context === "model") {
2545
3114
  return (
2546
3115
  <Box flexDirection="column">
2547
- <Text bold>Inference Commands:</Text>
2548
- <Text> model predict {"<model-id>"} --text {"<text>"} --labels {"<labels>"}</Text>
2549
- <Text> Run encoder inference (NER, classification, JSON extraction)</Text>
2550
- <Text> Use "base" as model-id for the base GLiNER2 model</Text>
3116
+ <Text bold>Model Commands:</Text>
3117
+ <Text> model endpoints ... (alias: model_endpoints) Manage model catalog entries (from /projects)</Text>
3118
+ <Text> model artifacts ... (alias: model_artifacts) Manage trained/deployed artifacts (from /felix)</Text>
2551
3119
  <Text> </Text>
2552
- <Text> model generate {"<model-id>"} --prompt {"<text>"}</Text>
2553
- <Text> Run decoder text generation on a decoder model</Text>
2554
- <Text> Example base IDs: base:Qwen/Qwen3-8B, base:meta-llama/Llama-3.1-8B-Instruct</Text>
3120
+ <Text> model endpoints list</Text>
3121
+ <Text> model endpoints create</Text>
3122
+ <Text> model endpoints get {"<model-id>"}</Text>
3123
+ <Text> model endpoints deploy {"<model-id>"} --job {"<training-job-id>"} [--reason {"<text>"}]</Text>
3124
+ <Text> model endpoints rollback {"<model-id>"} {"<deployment-id>"}</Text>
3125
+ <Text> model artifacts list</Text>
3126
+ <Text> model artifacts trained</Text>
3127
+ <Text> model artifacts deployed</Text>
3128
+ <Text> model artifacts download {"<job-id>"}</Text>
2555
3129
  <Text> </Text>
2556
- <Text bold> Options:</Text>
2557
- <Text> --task {"<task>"} extract_entities | classify_text | extract_json | schema</Text>
2558
- <Text> --labels {"<labels>"} Comma-separated labels for extraction/classification</Text>
2559
- <Text> --threshold {"<n>"} Confidence threshold (0-1, default: 0.5)</Text>
2560
- <Text> --system {"<text>"} System message for decoder generation</Text>
2561
- <Text> --max-tokens {"<n>"} Max tokens for generation (default: 256)</Text>
2562
- <Text> --temperature {"<n>"} Sampling temperature (default: 0.7)</Text>
2563
3130
  </Box>
2564
3131
  );
2565
3132
  }
2566
3133
 
2567
- // Chat help
2568
- if (context === "chat") {
3134
+ // Inference help
3135
+ if (context === "inference") {
2569
3136
  return (
2570
3137
  <Box flexDirection="column">
2571
- <Text bold>Chat Commands:</Text>
2572
- <Text> chat Start interactive chat agent</Text>
2573
- <Text> --message {"<msg>"} Initial message to process</Text>
3138
+ <Text bold>Inference Commands:</Text>
3139
+ <Text> inference base-models</Text>
3140
+ <Text> List base models from /base-models</Text>
2574
3141
  <Text> </Text>
2575
- <Text dimColor> Note: Model selection available in chat via /model command</Text>
3142
+ <Text> inference encoder {"<model-id>"} --text {"<text>"} --labels {"<labels>"}</Text>
3143
+ <Text> Run encoder inference via /inference</Text>
3144
+ <Text> --task {"<task>"} extract_entities | classify_text | extract_json | schema</Text>
3145
+ <Text> --labels {"<labels>"} Comma-separated labels (or use --schema JSON)</Text>
3146
+ <Text> --schema {"<json>"} JSON schema object for advanced tasks</Text>
3147
+ <Text> --threshold {"<n>"} Confidence threshold (0-1, default: 0.5)</Text>
3148
+ <Text> --project-id {"<id>"} Associate inference with a project</Text>
3149
+ <Text> </Text>
3150
+ <Text> inference decoder {"<model-id>"} --prompt {"<text>"}</Text>
3151
+ <Text> Run decoder generation via /inference</Text>
3152
+ <Text> --system {"<text>"} System message (optional)</Text>
3153
+ <Text> --max-tokens {"<n>"} Max tokens (default: 256)</Text>
3154
+ <Text> --temperature {"<n>"} Sampling temperature (default: 0.7)</Text>
3155
+ <Text> --top-p {"<n>"} Top-p sampling (0-1)</Text>
3156
+ <Text> --reasoning-trace Include reasoning trace when supported</Text>
3157
+ <Text> --project-id {"<id>"} Associate inference with a project</Text>
3158
+ <Text> Example model IDs: Qwen/Qwen3-8B, meta-llama/Llama-3.1-8B-Instruct</Text>
3159
+ <Text> </Text>
3160
+ <Text> inference completions {"<model-id>"} --prompt {"<text>"}</Text>
3161
+ <Text> Run raw text completion via /v1/completions</Text>
3162
+ <Text> --max-tokens {"<n>"} Max tokens (default: 256)</Text>
3163
+ <Text> --temperature {"<n>"} Sampling temperature (default: 0.7)</Text>
3164
+ <Text> --top-p {"<n>"} Top-p (sent via extra_body)</Text>
3165
+ <Text> --stop {"<a,b,c>"} Stop sequences (comma-separated)</Text>
3166
+ <Text> --echo true Echo prompt in output</Text>
3167
+ <Text> --provider {"<name>"} Provider override (optional)</Text>
2576
3168
  </Box>
2577
3169
  );
2578
3170
  }
2579
3171
 
2580
3172
  // Eval help
2581
3173
  if (context === "eval") {
2582
- return (
2583
- <Box flexDirection="column">
2584
- <Text bold>Evaluation Commands:</Text>
2585
- <Text dimColor> Dataset format: name[:version] (version defaults to "latest")</Text>
2586
- <Text> </Text>
2587
- <Text> eval list {"<name[:version]>"} List evaluations for a dataset</Text>
2588
- <Text> eval get {"<id>"} Get evaluation details</Text>
2589
- <Text> eval create Create a new evaluation</Text>
2590
- <Text> --model-id {"<id>"} Model to evaluate (required)</Text>
2591
- <Text> --dataset {"<name[:version]>"} Dataset to evaluate on (required)</Text>
2592
- <Text> --task-type {"<type>"} Task type: ner, classification</Text>
2593
- <Text> --text-column {"<col>"} Text column name</Text>
2594
- <Text> --label-column {"<col>"} Label column name</Text>
2595
- </Box>
2596
- );
3174
+ return <ErrorMessage error="The 'eval' command group is temporarily hidden for this version." />;
2597
3175
  }
2598
3176
 
2599
3177
  // Benchmark help
2600
3178
  if (context === "benchmark") {
2601
- return (
2602
- <Box flexDirection="column">
2603
- <Text bold>Benchmark Commands:</Text>
2604
- <Text> benchmark list List available benchmarks</Text>
2605
- <Text> benchmark run Start a benchmark evaluation</Text>
2606
- <Text> --model-id {"<id>"} Model to evaluate (required)</Text>
2607
- <Text> --task {"<type>"} Task: ner, text_classification (required)</Text>
2608
- <Text> --benchmark {"<name>"} Benchmark name (required)</Text>
2609
- <Text> --max-samples {"<n>"} Max samples (default: 100)</Text>
2610
- <Text> --split {"<name>"} Dataset split (default: test)</Text>
2611
- <Text> benchmark get {"<id>"} Get evaluation status/results</Text>
2612
- <Text> benchmark cancel {"<id>"} Cancel running evaluation</Text>
2613
- </Box>
2614
- );
3179
+ return <ErrorMessage error="The 'benchmark' command group is temporarily hidden for this version." />;
2615
3180
  }
2616
3181
 
2617
- // Adaptive fine-tuning help
2618
- if (context === "adaptive-finetuning") {
3182
+ // Agent help
3183
+ if (context === "agent") {
2619
3184
  return (
2620
3185
  <Box flexDirection="column">
2621
- <Text bold>Adaptive Fine-tuning Commands:</Text>
2622
- <Text> adaptive-finetuning chat Send a request to the adaptive FT agent</Text>
2623
- <Text> --message {"<text>"} Instruction (required)</Text>
3186
+ <Text bold>Agent Commands:</Text>
3187
+ <Text> agent Start interactive agent chat</Text>
3188
+ <Text> --mode {"<research>"} --mode research uses Pro workflow</Text>
3189
+ <Text> </Text>
3190
+ <Text> Omit --mode to use the default standard interactive mode.</Text>
2624
3191
  <Text> --conversation-id {"<id>"} Continue an existing conversation</Text>
2625
- <Text> --filters {"<json>"} Optional query filters JSON</Text>
3192
+ <Text> --filters {"<json>"} Reserved for future query filters</Text>
2626
3193
  <Text> --history {"<json>"} Optional message history JSON</Text>
2627
3194
  <Text> </Text>
2628
- <Text dimColor> Alias: aft</Text>
2629
- <Text dimColor> Example: pioneer adaptive-finetuning chat --message "Analyze failures from last 24h and propose retraining plan"</Text>
2630
- </Box>
2631
- );
2632
- }
2633
-
2634
- // Competition help
2635
- if (context === "competition") {
2636
- return (
2637
- <Box flexDirection="column">
2638
- <Text bold>Competition Commands:</Text>
2639
- <Text> competition list List active competitions</Text>
2640
- <Text> competition show {"<dataset-id>"} View sample data for a competition</Text>
2641
- <Text> competition leaderboard {"<dataset-id>"} View leaderboard rankings</Text>
2642
- <Text> --limit {"<n>"} Number of entries (default: 10)</Text>
2643
- <Text> competition submit {"<dataset-id>"} Submit evaluation to leaderboard</Text>
2644
- <Text> --eval-id {"<id>"} Evaluation ID (required)</Text>
2645
- <Text> --name {"<name>"} Display name (required)</Text>
2646
- </Box>
2647
- );
2648
- }
2649
-
2650
- // Notebook help
2651
- if (context === "notebook") {
2652
- return (
2653
- <Box flexDirection="column">
2654
- <Text bold>Notebook Commands:</Text>
2655
- <Text> </Text>
2656
- <Text> notebook run {"<file>"} Run all cells in a .ipynb notebook</Text>
2657
- <Text> --gpu {"<type>"} GPU type: cpu, t4, a10g, a100, h100 (default: cpu)</Text>
2658
- <Text> --no-felix Don't inject Felix helper functions</Text>
2659
- <Text> notebook create {"<name>"} Create a blank notebook</Text>
2660
- <Text> notebook sessions List active notebook sessions</Text>
2661
- <Text> notebook stop {"<session-id>"} Terminate a notebook session</Text>
3195
+ <Text dimColor>Example: pioneer agent</Text>
3196
+ <Text dimColor>Example: pioneer agent --mode research</Text>
3197
+ <Text dimColor>Then type: Analyze failures and propose retraining plan</Text>
3198
+ <Text dimColor>Then type: Draft a short status summary</Text>
2662
3199
  </Box>
2663
3200
  );
2664
3201
  }
@@ -2672,17 +3209,10 @@ const Help: React.FC<HelpProps> = ({ context = "root" }) => {
2672
3209
  <Text> pioneer {"<command>"} {"[options]"}</Text>
2673
3210
  <Text> </Text>
2674
3211
  <Text bold>Commands:</Text>
2675
- <Text> chat Start interactive chat agent</Text>
2676
3212
  <Text> auth Authentication (login, logout, status)</Text>
2677
- <Text> dataset Manage datasets (list, generate, edit, analyze)</Text>
2678
- <Text> project Manage projects</Text>
3213
+ <Text> model Manage model endpoints and artifacts</Text>
2679
3214
  <Text> job Manage training jobs</Text>
2680
- <Text> model Manage models</Text>
2681
- <Text> eval Model evaluations on datasets</Text>
2682
- <Text> benchmark Run benchmark evaluations</Text>
2683
- <Text> notebook Run and manage Jupyter notebooks</Text>
2684
- <Text> competition Competitions and leaderboards</Text>
2685
- <Text> adaptive-finetuning Adaptive fine-tuning agent</Text>
3215
+ <Text> agent Run agent chat (research is the only explicit alternate mode)</Text>
2686
3216
  <Text> telemetry Manage anonymous usage analytics</Text>
2687
3217
  <Text> </Text>
2688
3218
  <Text dimColor>Run 'pioneer {"<command>"} --help' for details on a specific command.</Text>
@@ -2690,24 +3220,12 @@ const Help: React.FC<HelpProps> = ({ context = "root" }) => {
2690
3220
  <Text dimColor>Get started:</Text>
2691
3221
  <Text dimColor> 1. Sign up at https://app.pioneer.ai</Text>
2692
3222
  <Text dimColor> 2. Run: pioneer auth login</Text>
2693
- <Text dimColor> 3. Start building with: pioneer chat</Text>
3223
+ <Text dimColor> 3. Start building with: pioneer agent</Text>
2694
3224
  </Box>
2695
3225
  );
2696
3226
  };
2697
3227
 
2698
3228
  // ─────────────────────────────────────────────────────────────────────────────
2699
- // Chat Wrapper Component
2700
- // ─────────────────────────────────────────────────────────────────────────────
2701
-
2702
- interface ChatWrapperProps {
2703
- flags: Record<string, string>;
2704
- }
2705
-
2706
- const ChatWrapper: React.FC<ChatWrapperProps> = ({ flags }) => {
2707
- const initialMessage = flags.message;
2708
- return <ChatApp initialMessage={initialMessage} />;
2709
- };
2710
-
2711
3229
  // ─────────────────────────────────────────────────────────────────────────────
2712
3230
  // Main Router
2713
3231
  // ─────────────────────────────────────────────────────────────────────────────
@@ -2715,13 +3233,75 @@ const ChatWrapper: React.FC<ChatWrapperProps> = ({ flags }) => {
2715
3233
  interface AppProps {
2716
3234
  command: string[];
2717
3235
  flags: Record<string, string>;
3236
+ parseErrors: string[];
2718
3237
  }
2719
3238
 
2720
- const App: React.FC<AppProps> = ({ command, flags }) => {
3239
+ const App: React.FC<AppProps> = ({ command, flags, parseErrors }) => {
2721
3240
  // Check if raw mode is supported for interactive prompts
2722
3241
  const { isRawModeSupported } = useStdin();
2723
3242
  const [showTelemetryPrompt, setShowTelemetryPrompt] = useState(!hasChosenTelemetry());
2724
3243
  const [group, action, ...rest] = command;
3244
+ const normalizedAction =
3245
+ action === "model_endpoints"
3246
+ ? "endpoints"
3247
+ : action === "model_artifacts"
3248
+ ? "artifacts"
3249
+ : action;
3250
+ const hasParseErrors = parseErrors.length > 0;
3251
+ const isModelCreateMissingModel =
3252
+ group === "model" &&
3253
+ normalizedAction === "endpoints" &&
3254
+ rest[0] === "create" &&
3255
+ parseErrors.length === 1 &&
3256
+ parseErrors[0] === "--model";
3257
+ const isModelEndpointsDeployMissingJob =
3258
+ group === "model" &&
3259
+ normalizedAction === "endpoints" &&
3260
+ rest[0] === "deploy" &&
3261
+ !flags["job"];
3262
+
3263
+ if (group === "dataset" || group === "inference" || group === "eval" || group === "benchmark") {
3264
+ return (
3265
+ <ErrorMessage
3266
+ error={`The '${group}' command group is temporarily hidden for this version. Use 'pioneer --help' to see available commands.`}
3267
+ />
3268
+ );
3269
+ }
3270
+
3271
+ if (hasParseErrors && !isModelCreateMissingModel) {
3272
+ if (isModelEndpointsDeployMissingJob) {
3273
+ const errorMessage = rest[1]
3274
+ ? `Training job ID required: model endpoints deploy ${rest[1]} --job <training-job-id>`
3275
+ : "Training job ID required: model endpoints deploy <model-id> --job <training-job-id>";
3276
+
3277
+ return (
3278
+ <ApiCommand
3279
+ action={() =>
3280
+ Promise.resolve<api.ApiResult<{ message: string }>>({
3281
+ ok: false,
3282
+ status: 400,
3283
+ error: errorMessage,
3284
+ data: {
3285
+ message: errorMessage,
3286
+ },
3287
+ })
3288
+ }
3289
+ successMessage="Validation failed"
3290
+ />
3291
+ );
3292
+ }
3293
+
3294
+ return (
3295
+ <Box flexDirection="column">
3296
+ <ErrorMessage error="One or more flags are missing values. Please provide values for: " />
3297
+ {parseErrors.map((flag) => (
3298
+ <Text dimColor key={flag}>
3299
+ - {flag} {"<value>"}
3300
+ </Text>
3301
+ ))}
3302
+ </Box>
3303
+ );
3304
+ }
2725
3305
 
2726
3306
  // In non-interactive mode, skip telemetry prompt and default to disabled
2727
3307
  useEffect(() => {
@@ -2734,9 +3314,11 @@ const App: React.FC<AppProps> = ({ command, flags }) => {
2734
3314
  // Track command usage (must be before any conditional returns)
2735
3315
  useEffect(() => {
2736
3316
  if (group && !showTelemetryPrompt) {
2737
- trackCommand(group, action);
3317
+ const actionForTracking =
3318
+ group === "model" && normalizedAction !== action ? action || normalizedAction : normalizedAction;
3319
+ trackCommand(group, actionForTracking);
2738
3320
  }
2739
- }, [group, action, showTelemetryPrompt]);
3321
+ }, [group, action, normalizedAction, showTelemetryPrompt]);
2740
3322
 
2741
3323
  // Show telemetry consent prompt on first run (but not for --help or --version, and only if interactive)
2742
3324
  if (showTelemetryPrompt && isRawModeSupported && !flags.help && flags.version !== "true" && flags.v !== "true") {
@@ -2748,14 +3330,6 @@ const App: React.FC<AppProps> = ({ command, flags }) => {
2748
3330
  return <Help />;
2749
3331
  }
2750
3332
 
2751
- // Chat command - Interactive agent
2752
- if (group === "chat") {
2753
- if (flags.help === "true" || action === "help") {
2754
- return <Help context="chat" />;
2755
- }
2756
- return <ChatWrapper flags={flags} />;
2757
- }
2758
-
2759
3333
  // Auth commands
2760
3334
  if (group === "auth") {
2761
3335
  if (flags.help === "true" || !action || action === "help") {
@@ -2895,6 +3469,31 @@ const App: React.FC<AppProps> = ({ command, flags }) => {
2895
3469
  );
2896
3470
  }
2897
3471
 
3472
+ if (subAction === "dismiss-outlier" && rest[1]) {
3473
+ const dataset = parseDatasetRef(rest[1]);
3474
+ const fingerprint = flags["fingerprint"];
3475
+ if (!dataset) {
3476
+ return <ErrorMessage error={`Invalid dataset format: ${rest[1]}. Use name[:version] format (e.g., my-dataset or my-dataset:v1).`} />;
3477
+ }
3478
+ if (!fingerprint) {
3479
+ return (
3480
+ <ErrorMessage error="--fingerprint is required (from dataset analysis output)" />
3481
+ );
3482
+ }
3483
+
3484
+ return (
3485
+ <ApiCommand
3486
+ action={() =>
3487
+ api.dismissOutlier({
3488
+ dataset,
3489
+ fingerprint,
3490
+ })
3491
+ }
3492
+ successMessage={`Outlier dismissed for dataset ${rest[1]}`}
3493
+ />
3494
+ );
3495
+ }
3496
+
2898
3497
  return <Help context="dataset-edit" />;
2899
3498
  }
2900
3499
 
@@ -3076,6 +3675,41 @@ const App: React.FC<AppProps> = ({ command, flags }) => {
3076
3675
  />
3077
3676
  );
3078
3677
  }
3678
+ if (action === "analyze-llm") {
3679
+ const datasetStr = rest[0];
3680
+ const taskType = flags["task-type"];
3681
+ const taskDescription = flags["description"] || flags["task-description"];
3682
+ const labels = parseCommaSeparated(flags["labels"]);
3683
+
3684
+ if (!datasetStr || !taskType) {
3685
+ return <Help context="dataset-analyze" />;
3686
+ }
3687
+
3688
+ if (!["ner", "classification", "generative"].includes(taskType)) {
3689
+ return (
3690
+ <ErrorMessage error="--task-type must be one of: ner, classification, generative" />
3691
+ );
3692
+ }
3693
+
3694
+ const dataset = parseDatasetRef(datasetStr);
3695
+ if (!dataset) {
3696
+ return <ErrorMessage error={`Invalid dataset format: ${datasetStr}. Use name:version format.`} />;
3697
+ }
3698
+
3699
+ return (
3700
+ <ApiCommand
3701
+ action={() =>
3702
+ api.analyzeDatasetLLM({
3703
+ task_type: taskType as "ner" | "classification" | "generative",
3704
+ dataset_name: dataset.name,
3705
+ dataset_version: dataset.version,
3706
+ ...(taskDescription ? { task_description: taskDescription } : {}),
3707
+ ...(labels.length ? { labels } : {}),
3708
+ })
3709
+ }
3710
+ />
3711
+ );
3712
+ }
3079
3713
  if (action === "analyze") {
3080
3714
  const datasetStr = rest[0];
3081
3715
  const taskType = flags["task-type"];
@@ -3098,13 +3732,138 @@ const App: React.FC<AppProps> = ({ command, flags }) => {
3098
3732
  );
3099
3733
  }
3100
3734
 
3101
- // Generate commands
3102
- if (action === "generate") {
3103
- const subAction = rest[0];
3104
- const numExamples = flags["num"] ? parseInt(flags["num"], 10) : undefined;
3105
- const domainDescription = flags["domain"];
3106
- const saveDataset = flags["save"]?.toLowerCase() === "true";
3107
- const datasetName = flags["name"];
3735
+ // Generate commands
3736
+ if (action === "generate") {
3737
+ const subAction = rest[0];
3738
+ const numExamples = flags["num"] ? parseInt(flags["num"], 10) : undefined;
3739
+ const domainDescription = flags["domain"];
3740
+ const saveDataset = flags["save"]?.toLowerCase() === "true";
3741
+ const datasetName = flags["name"];
3742
+ const quality = flags["quality"] as "light" | "medium" | "heavy" | undefined;
3743
+ const generationProfile = flags["generation-profile"] as
3744
+ | "auto"
3745
+ | "fast"
3746
+ | "balanced"
3747
+ | "quality"
3748
+ | undefined;
3749
+ const includeReasoningTrace =
3750
+ flags["reasoning-trace"] === undefined
3751
+ ? undefined
3752
+ : flags["reasoning-trace"].toLowerCase() !== "false";
3753
+ const reasoningEffort = flags["reasoning-effort"] as
3754
+ | "low"
3755
+ | "medium"
3756
+ | "high"
3757
+ | undefined;
3758
+ const validGenerationProfiles = ["auto", "fast", "balanced", "quality"];
3759
+ if (generationProfile && !validGenerationProfiles.includes(generationProfile)) {
3760
+ return <ErrorMessage error="--generation-profile must be one of: auto, fast, balanced, quality" />;
3761
+ }
3762
+ const validReasoningEfforts = ["low", "medium", "high"];
3763
+ if (reasoningEffort && !validReasoningEfforts.includes(reasoningEffort)) {
3764
+ return <ErrorMessage error="--reasoning-effort must be one of: low, medium, high" />;
3765
+ }
3766
+ const multiplicatorArg = flags["multiplicator"];
3767
+ const useMetaFelix = flags["use-meta-felix"]
3768
+ ? flags["use-meta-felix"].toLowerCase() !== "false"
3769
+ : undefined;
3770
+ const minCriteria = flags["min-criteria"] ? parseInt(flags["min-criteria"], 10) : undefined;
3771
+ if (flags["min-criteria"] && Number.isNaN(minCriteria)) {
3772
+ return <ErrorMessage error="--min-criteria must be a number" />;
3773
+ }
3774
+ const targetChoices = flags["target-choices"]
3775
+ ? parseInt(flags["target-choices"], 10)
3776
+ : undefined;
3777
+ if (flags["target-choices"] && Number.isNaN(targetChoices)) {
3778
+ return <ErrorMessage error="--target-choices must be a number" />;
3779
+ }
3780
+ const projectId = flags["project-id"];
3781
+ const generationType = flags["type"] as "training" | "evaluation" | "split" | undefined;
3782
+ if (generationType && !["training", "evaluation", "split"].includes(generationType)) {
3783
+ return <ErrorMessage error="--type must be one of: training, evaluation, split" />;
3784
+ }
3785
+ const visibility = flags["visibility"] as "private" | "public" | undefined;
3786
+ if (visibility && !["private", "public"].includes(visibility)) {
3787
+ return <ErrorMessage error="--visibility must be private or public" />;
3788
+ }
3789
+ const splitRatioArg = flags["split-ratio"];
3790
+ const splitRatio =
3791
+ splitRatioArg && splitRatioArg.includes(":")
3792
+ ? (() => {
3793
+ const [training, evaluation] = splitRatioArg.split(":").map((v) => parseFloat(v));
3794
+ if (Number.isNaN(training) || Number.isNaN(evaluation)) {
3795
+ return undefined;
3796
+ }
3797
+ return { training, evaluation };
3798
+ })()
3799
+ : splitRatioArg
3800
+ ? (() => {
3801
+ try {
3802
+ const parsed = JSON.parse(splitRatioArg);
3803
+ return parsed;
3804
+ } catch {
3805
+ return undefined;
3806
+ }
3807
+ })()
3808
+ : undefined;
3809
+ const negativeRatio = flags["negative-ratio"]
3810
+ ? parseFloat(flags["negative-ratio"])
3811
+ : undefined;
3812
+ if (flags["negative-ratio"] && Number.isNaN(negativeRatio)) {
3813
+ return <ErrorMessage error="--negative-ratio must be a number" />;
3814
+ }
3815
+ const classifiedExamplesArg = flags["classified-examples"];
3816
+ const qualityArg = flags["quality"];
3817
+ if (qualityArg && !["light", "medium", "heavy"].includes(qualityArg)) {
3818
+ return <ErrorMessage error="--quality must be light, medium, or heavy" />;
3819
+ }
3820
+ if (splitRatioArg && splitRatio === undefined) {
3821
+ return <ErrorMessage error="--split-ratio must be training:evaluation or a JSON object" />;
3822
+ }
3823
+ let parsedMultiplicator: Record<string, unknown> | undefined;
3824
+ if (multiplicatorArg) {
3825
+ try {
3826
+ const parsed = JSON.parse(multiplicatorArg);
3827
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3828
+ return <ErrorMessage error="--multiplicator must be a valid JSON object" />;
3829
+ }
3830
+ parsedMultiplicator = parsed;
3831
+ } catch {
3832
+ return <ErrorMessage error="--multiplicator must be valid JSON" />;
3833
+ }
3834
+ }
3835
+ let classifiedExamples: Record<string, unknown>[] | undefined;
3836
+ if (classifiedExamplesArg) {
3837
+ try {
3838
+ const parsed = JSON.parse(classifiedExamplesArg);
3839
+ if (!Array.isArray(parsed)) {
3840
+ return <ErrorMessage error="--classified-examples must be a JSON array" />;
3841
+ }
3842
+ classifiedExamples = parsed as Record<string, unknown>[];
3843
+ } catch {
3844
+ return <ErrorMessage error="--classified-examples must be valid JSON" />;
3845
+ }
3846
+ }
3847
+ if (multiplicatorArg && parsedMultiplicator === undefined) {
3848
+ return <ErrorMessage error="--multiplicator must be a valid JSON object" />;
3849
+ }
3850
+
3851
+ const commonGenerationOptions = {
3852
+ quality,
3853
+ generation_profile: generationProfile,
3854
+ ...(includeReasoningTrace !== undefined ? { include_reasoning_trace: includeReasoningTrace } : {}),
3855
+ ...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),
3856
+ ...(multiplicatorArg ? { multiplicator: parsedMultiplicator } : {}),
3857
+ ...(useMetaFelix !== undefined ? { use_meta_felix: useMetaFelix } : {}),
3858
+ ...(minCriteria !== undefined ? { min_criteria: minCriteria } : {}),
3859
+ ...(targetChoices !== undefined ? { target_choices: targetChoices } : {}),
3860
+ ...(projectId ? { project_id: projectId } : {}),
3861
+ ...(generationType ? { type: generationType } : {}),
3862
+ ...(visibility ? { visibility } : {}),
3863
+ ...(splitRatio ? { split_ratio: splitRatio } : {}),
3864
+ ...(negativeRatio !== undefined ? { negative_ratio: negativeRatio } : {}),
3865
+ ...(classifiedExamplesArg ? { classified_examples: classifiedExamples } : {}),
3866
+ };
3108
3867
 
3109
3868
  if (subAction === "ner") {
3110
3869
  const labels = flags["labels"]?.split(",");
@@ -3120,6 +3879,7 @@ const App: React.FC<AppProps> = ({ command, flags }) => {
3120
3879
  domain_description: domainDescription,
3121
3880
  save_dataset: saveDataset,
3122
3881
  dataset_name: datasetName,
3882
+ ...commonGenerationOptions,
3123
3883
  })
3124
3884
  }
3125
3885
  datasetName={datasetName || "ner-dataset"}
@@ -3145,6 +3905,7 @@ const App: React.FC<AppProps> = ({ command, flags }) => {
3145
3905
  multi_label: multiLabel,
3146
3906
  save_dataset: saveDataset,
3147
3907
  dataset_name: datasetName,
3908
+ ...commonGenerationOptions,
3148
3909
  })
3149
3910
  }
3150
3911
  datasetName={datasetName || "classification-dataset"}
@@ -3175,6 +3936,7 @@ const App: React.FC<AppProps> = ({ command, flags }) => {
3175
3936
  num_examples: numExamples,
3176
3937
  save_dataset: saveDataset,
3177
3938
  dataset_name: datasetName,
3939
+ ...commonGenerationOptions,
3178
3940
  })
3179
3941
  }
3180
3942
  datasetName={datasetName || "custom-dataset"}
@@ -3198,6 +3960,7 @@ const App: React.FC<AppProps> = ({ command, flags }) => {
3198
3960
  num_examples: numExamples,
3199
3961
  save_dataset: saveDataset,
3200
3962
  dataset_name: datasetName,
3963
+ ...commonGenerationOptions,
3201
3964
  })
3202
3965
  }
3203
3966
  datasetName={datasetName || "decoder-dataset"}
@@ -3213,6 +3976,44 @@ const App: React.FC<AppProps> = ({ command, flags }) => {
3213
3976
  // Infer labels commands
3214
3977
  if (action === "infer") {
3215
3978
  const subAction = rest[0];
3979
+ if (subAction === "improve-prompt") {
3980
+ const prompt = flags["prompt"] || flags["domain"];
3981
+ if (!prompt) {
3982
+ return <ErrorMessage error="--prompt is required for infer improve-prompt" />;
3983
+ }
3984
+ const dataType = flags["data-type"];
3985
+ return (
3986
+ <ApiCommand
3987
+ action={() =>
3988
+ api.improvePrompt({
3989
+ prompt,
3990
+ ...(dataType ? { data_type: dataType } : {}),
3991
+ })
3992
+ }
3993
+ />
3994
+ );
3995
+ }
3996
+
3997
+ if (subAction === "infer-advanced" || subAction === "advanced") {
3998
+ const prompt = flags["prompt"] || flags["domain"];
3999
+ const dataType = flags["data-type"];
4000
+ const labels = parseCommaSeparated(flags["labels"]);
4001
+ if (!prompt) {
4002
+ return <ErrorMessage error="--prompt is required for dataset infer infer-advanced" />;
4003
+ }
4004
+ return (
4005
+ <ApiCommand
4006
+ action={() =>
4007
+ api.inferAdvanced({
4008
+ prompt,
4009
+ ...(dataType ? { data_type: dataType } : {}),
4010
+ ...(labels.length > 0 ? { labels } : {}),
4011
+ })
4012
+ }
4013
+ />
4014
+ );
4015
+ }
4016
+
3216
4017
  const domainDescription = flags["domain"];
3217
4018
 
3218
4019
  if (!domainDescription) {
@@ -3246,137 +4047,129 @@ const App: React.FC<AppProps> = ({ command, flags }) => {
3246
4047
  return <Help context="dataset" />;
3247
4048
  }
3248
4049
 
3249
- return <Help context="dataset" />;
3250
- }
3251
-
3252
- // Project commands
3253
- if (group === "project") {
3254
- if (flags.help === "true" || !action || action === "help") {
3255
- return <Help context="project" />;
3256
- }
3257
-
3258
- if (action === "list") {
3259
- return <ApiCommand action={api.listProjects} />;
3260
- }
3261
-
3262
- if (action === "get") {
3263
- const projectId = rest[0];
3264
- if (!projectId) {
3265
- return <ErrorMessage error="Project ID required: project get <project-id>" />;
3266
- }
3267
- return <ApiCommand action={() => api.getProject(projectId)} />;
3268
- }
3269
-
3270
- if (action === "create") {
3271
- const name = flags["name"];
3272
- const icon = flags["icon"];
3273
- const repo = flags["repo"];
3274
- const description = flags["description"];
3275
- const modelId = flags["model-id"];
3276
- const exampleStr = flags["example"];
3277
-
3278
- if (!name) {
3279
- return <ErrorMessage error="--name is required for project creation" />;
4050
+ if (action === "label-existing") {
4051
+ const subAction = rest[0];
4052
+ const labels = parseCommaSeparated(flags["labels"]);
4053
+ const inputsArg = flags["inputs"];
4054
+ const datasetName = flags["name"];
4055
+ const saveDataset = flags["save"]?.toLowerCase() === "true";
4056
+ const domainDescription = flags["domain"];
4057
+ const projectId = flags["project-id"];
4058
+ if (!inputsArg) {
4059
+ return <ErrorMessage error="--inputs is required for label-existing commands" />;
3280
4060
  }
3281
-
3282
- let example: Record<string, unknown> | undefined;
3283
- if (exampleStr) {
3284
- try {
3285
- const parsed = JSON.parse(exampleStr) as unknown;
3286
- if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
3287
- return <ErrorMessage error="--example must be a JSON object" />;
3288
- }
3289
- example = parsed as Record<string, unknown>;
3290
- } catch {
3291
- return <ErrorMessage error="--example must be valid JSON" />;
3292
- }
4061
+ if (saveDataset && !datasetName) {
4062
+ return <ErrorMessage error="--name is required when --save=true for label-existing" />;
3293
4063
  }
3294
4064
 
3295
- return (
3296
- <ApiCommand
3297
- action={() =>
3298
- api.createProject({
3299
- name,
3300
- ...(icon ? { icon } : {}),
3301
- ...(repo ? { repo } : {}),
3302
- ...(description ? { description } : {}),
3303
- ...(modelId ? { selected_model_id: modelId } : {}),
3304
- ...(example ? { example } : {}),
3305
- })
3306
- }
3307
- successMessage="Project created"
3308
- />
3309
- );
3310
- }
3311
-
3312
- if (action === "update") {
3313
- const projectId = rest[0];
3314
- if (!projectId) {
3315
- return <ErrorMessage error="Project ID required: project update <project-id>" />;
4065
+ let inputs: unknown;
4066
+ try {
4067
+ inputs = JSON.parse(inputsArg);
4068
+ } catch {
4069
+ return <ErrorMessage error="--inputs must be valid JSON" />;
3316
4070
  }
3317
4071
 
3318
- const name = flags["name"];
3319
- const icon = flags["icon"];
3320
- const repo = flags["repo"];
3321
- const description = flags["description"];
3322
- const modelId = flags["model-id"];
3323
-
3324
- if (!name && !icon && !repo && !description && !modelId) {
4072
+ if (subAction === "ner" || subAction === "classification") {
4073
+ if (!labels || labels.length === 0) {
4074
+ return <ErrorMessage error="--labels is required for label-existing ner|classification" />;
4075
+ }
4076
+ if (
4077
+ !Array.isArray(inputs) ||
4078
+ inputs.length === 0 ||
4079
+ !inputs.every((item) => typeof item === "string")
4080
+ ) {
4081
+ return <ErrorMessage error="--inputs must be a JSON array of strings for ner/classification" />;
4082
+ }
4083
+ const common = { labels, inputs: inputs as string[], dataset_name: datasetName };
4084
+ if (subAction === "ner") {
4085
+ return (
4086
+ <ApiCommand
4087
+ action={() =>
4088
+ api.labelExistingNER({
4089
+ ...common,
4090
+ domain_description: domainDescription,
4091
+ save_dataset: saveDataset,
4092
+ project_id: projectId,
4093
+ })
4094
+ }
4095
+ />
4096
+ );
4097
+ }
3325
4098
  return (
3326
- <ErrorMessage error="Provide at least one field to update: --name, --icon, --repo, --description, or --model-id" />
4099
+ <ApiCommand
4100
+ action={() =>
4101
+ api.labelExistingClassification({
4102
+ ...common,
4103
+ domain_description: domainDescription,
4104
+ save_dataset: saveDataset,
4105
+ project_id: projectId,
4106
+ })
4107
+ }
4108
+ />
3327
4109
  );
3328
4110
  }
3329
4111
 
3330
- return (
3331
- <ApiCommand
3332
- action={() =>
3333
- api.updateProject(projectId, {
3334
- ...(name ? { name } : {}),
3335
- ...(icon ? { icon } : {}),
3336
- ...(repo ? { repo } : {}),
3337
- ...(description ? { description } : {}),
3338
- ...(modelId ? { selected_model_id: modelId } : {}),
3339
- })
3340
- }
3341
- successMessage="Project updated"
3342
- />
3343
- );
3344
- }
3345
-
3346
- if (action === "delete") {
3347
- const projectId = rest[0];
3348
- if (!projectId) {
3349
- return <ErrorMessage error="Project ID required: project delete <project-id>" />;
3350
- }
3351
- return (
3352
- <ApiCommand
3353
- action={() => api.deleteProject(projectId)}
3354
- successMessage={`Project ${projectId} deleted`}
3355
- />
3356
- );
3357
- }
3358
-
3359
- if (action === "dataset-count" || action === "count") {
3360
- const projectId = rest[0];
3361
- if (!projectId) {
3362
- return (
3363
- <ErrorMessage error="Project ID required: project dataset-count <project-id>" />
4112
+ if (subAction === "fields") {
4113
+ const inputFieldsArg = flags["input-fields"];
4114
+ const outputFieldsArg = flags["output-fields"];
4115
+ if (!inputFieldsArg || !outputFieldsArg) {
4116
+ return <ErrorMessage error="--input-fields and --output-fields are required for fields labeling" />;
4117
+ }
4118
+ let inputFields: unknown;
4119
+ let outputFields: unknown;
4120
+ try {
4121
+ inputFields = JSON.parse(inputFieldsArg);
4122
+ outputFields = JSON.parse(outputFieldsArg);
4123
+ } catch {
4124
+ return <ErrorMessage error="--input-fields and --output-fields must be valid JSON" />;
4125
+ }
4126
+ if (
4127
+ !Array.isArray(inputFields) ||
4128
+ !Array.isArray(outputFields) ||
4129
+ inputFields.length === 0 ||
4130
+ outputFields.length === 0
4131
+ ) {
4132
+ return (
4133
+ <ErrorMessage error="--input-fields and --output-fields must be non-empty arrays" />
4134
+ );
4135
+ }
4136
+ const validInputFields = inputFields.every(
4137
+ (field) => field && typeof field === "object" && !Array.isArray(field)
3364
4138
  );
3365
- }
3366
- return <ApiCommand action={() => api.getProjectDatasetCount(projectId)} />;
3367
- }
3368
-
3369
- if (action === "quality-metrics" || action === "quality") {
3370
- const projectId = rest[0];
3371
- if (!projectId) {
4139
+ const validOutputFields = outputFields.every(
4140
+ (field) => field && typeof field === "object" && !Array.isArray(field)
4141
+ );
4142
+ if (!validInputFields || !validOutputFields) {
4143
+ return <ErrorMessage error="--input-fields and --output-fields must be arrays of objects" />;
4144
+ }
4145
+ if (
4146
+ !Array.isArray(inputs) ||
4147
+ inputs.length === 0 ||
4148
+ !inputs.every((item) => item && typeof item === "object" && !Array.isArray(item))
4149
+ ) {
4150
+ return <ErrorMessage error="--inputs must be a JSON array of objects for fields" />;
4151
+ }
3372
4152
  return (
3373
- <ErrorMessage error="Project ID required: project quality-metrics <project-id>" />
4153
+ <ApiCommand
4154
+ action={() =>
4155
+ api.labelExistingFields({
4156
+ input_fields: inputFields as api.RecordField[],
4157
+ output_fields: outputFields as api.RecordField[],
4158
+ inputs: inputs as Record<string, unknown>[],
4159
+ dataset_name: datasetName,
4160
+ save_dataset: saveDataset,
4161
+ domain_description: domainDescription,
4162
+ project_id: projectId,
4163
+ })
4164
+ }
4165
+ />
3374
4166
  );
3375
4167
  }
3376
- return <ApiCommand action={() => api.getProjectQualityMetrics(projectId)} />;
4168
+
4169
+ return <Help context="dataset" />;
3377
4170
  }
3378
4171
 
3379
- return <Help context="project" />;
4172
+ return <Help context="dataset" />;
3380
4173
  }
3381
4174
 
3382
4175
  // Job commands
@@ -3433,181 +4226,558 @@ const App: React.FC<AppProps> = ({ command, flags }) => {
3433
4226
  return <Help context="job" />;
3434
4227
  }
3435
4228
 
3436
- // Model commands
3437
- if (group === "model") {
4229
+ // Inference commands
4230
+ if (group === "inference") {
3438
4231
  if (flags.help === "true" || !action || action === "help") {
3439
- return <Help context="model" />;
4232
+ return <Help context="inference" />;
3440
4233
  }
3441
- if (action === "list") {
3442
- const subAction = rest[0];
3443
- if (subAction === "help") {
3444
- return <Help context="model" />;
4234
+
4235
+ if (action === "base-models" || action === "models" || action === "list") {
4236
+ return <ApiCommand action={api.listBaseModels} />;
4237
+ }
4238
+
4239
+ if (action === "encoder") {
4240
+ const rawModelId = rest[0];
4241
+ if (!rawModelId) {
4242
+ return (
4243
+ <ErrorMessage error="Model ID required. Usage: inference encoder <model-id> --text <text> --labels <labels>" />
4244
+ );
3445
4245
  }
3446
- if (subAction === "trained") {
3447
- return <ModelListCommand filter="trained" />;
4246
+ const modelId = normalizeModelId(rawModelId);
4247
+ const text = flags["text"];
4248
+ if (!text) {
4249
+ return <ErrorMessage error="--text is required for encoder inference" />;
3448
4250
  }
3449
- if (subAction === "deployed") {
3450
- return <ModelListCommand filter="deployed" />;
4251
+
4252
+ const task = (flags["task"] || "extract_entities") as
4253
+ | "extract_entities"
4254
+ | "classify_text"
4255
+ | "extract_json"
4256
+ | "schema";
4257
+ if (!["extract_entities", "classify_text", "extract_json", "schema"].includes(task)) {
4258
+ return (
4259
+ <ErrorMessage error="--task must be one of: extract_entities, classify_text, extract_json, schema" />
4260
+ );
4261
+ }
4262
+
4263
+ const labels = parseCommaSeparated(flags["labels"]);
4264
+ const schemaStr = flags["schema"];
4265
+ let schema: string[] | Record<string, unknown> | null = null;
4266
+
4267
+ if (schemaStr) {
4268
+ try {
4269
+ const parsed = JSON.parse(schemaStr) as unknown;
4270
+ if (Array.isArray(parsed)) {
4271
+ if (!parsed.every((item) => typeof item === "string")) {
4272
+ return (
4273
+ <ErrorMessage error="--schema JSON array must contain only strings" />
4274
+ );
4275
+ }
4276
+ schema = parsed;
4277
+ } else if (parsed && typeof parsed === "object") {
4278
+ schema = parsed as Record<string, unknown>;
4279
+ } else {
4280
+ return (
4281
+ <ErrorMessage error="--schema must be a JSON object or array of strings" />
4282
+ );
4283
+ }
4284
+ } catch {
4285
+ return <ErrorMessage error="--schema must be valid JSON" />;
4286
+ }
4287
+ } else if (labels.length > 0) {
4288
+ schema = labels;
4289
+ }
4290
+
4291
+ if (!schema) {
4292
+ return <ErrorMessage error="Provide --labels or --schema for encoder inference" />;
4293
+ }
4294
+
4295
+ const threshold = flags["threshold"] ? parseFloat(flags["threshold"]) : 0.5;
4296
+ if (Number.isNaN(threshold) || threshold < 0 || threshold > 1) {
4297
+ return <ErrorMessage error="--threshold must be a number between 0 and 1" />;
4298
+ }
4299
+
4300
+ const includeConfidence =
4301
+ flags["include-confidence"] === undefined ||
4302
+ flags["include-confidence"].toLowerCase() !== "false";
4303
+ const includeSpans = flags["include-spans"]?.toLowerCase() === "true";
4304
+ const formatResults =
4305
+ flags["format-results"] === undefined ||
4306
+ flags["format-results"].toLowerCase() !== "false";
4307
+ const projectId = flags["project-id"];
4308
+
4309
+ return (
4310
+ <ApiCommand
4311
+ action={() =>
4312
+ api.runInference({
4313
+ model_id: modelId,
4314
+ task,
4315
+ text,
4316
+ schema,
4317
+ threshold,
4318
+ include_confidence: includeConfidence,
4319
+ include_spans: includeSpans,
4320
+ format_results: formatResults,
4321
+ ...(projectId ? { project_id: projectId } : {}),
4322
+ })
4323
+ }
4324
+ />
4325
+ );
4326
+ }
4327
+
4328
+ if (action === "decoder") {
4329
+ const modelId = rest[0];
4330
+ if (!modelId) {
4331
+ return (
4332
+ <ErrorMessage error="Model ID required. Usage: inference decoder <model-id> --prompt <text>" />
4333
+ );
4334
+ }
4335
+ const normalizedModelId = normalizeModelId(modelId);
4336
+ const prompt = flags["prompt"];
4337
+ if (!prompt) {
4338
+ return <ErrorMessage error="--prompt is required for decoder inference" />;
4339
+ }
4340
+ const systemMsg = flags["system"];
4341
+ const maxTokens = flags["max-tokens"] ? parseInt(flags["max-tokens"], 10) : 256;
4342
+ if (Number.isNaN(maxTokens) || maxTokens < 1) {
4343
+ return <ErrorMessage error="--max-tokens must be a positive integer" />;
4344
+ }
4345
+ const temperature = flags["temperature"] ? parseFloat(flags["temperature"]) : 0.7;
4346
+ if (Number.isNaN(temperature) || temperature < 0 || temperature > 2) {
4347
+ return <ErrorMessage error="--temperature must be a number between 0 and 2" />;
4348
+ }
4349
+ const topP = flags["top-p"] ? parseFloat(flags["top-p"]) : undefined;
4350
+ if (topP !== undefined && (Number.isNaN(topP) || topP < 0 || topP > 1)) {
4351
+ return <ErrorMessage error="--top-p must be a number between 0 and 1" />;
4352
+ }
4353
+ const includeReasoningTrace =
4354
+ flags["reasoning-trace"] !== undefined &&
4355
+ flags["reasoning-trace"].toLowerCase() !== "false";
4356
+ const projectId = flags["project-id"];
4357
+
4358
+ return (
4359
+ <ModelGenerateCommand
4360
+ modelId={normalizedModelId}
4361
+ prompt={prompt}
4362
+ systemMsg={systemMsg}
4363
+ maxTokens={maxTokens}
4364
+ temperature={temperature}
4365
+ topP={topP}
4366
+ includeReasoningTrace={includeReasoningTrace}
4367
+ projectId={projectId}
4368
+ />
4369
+ );
4370
+ }
4371
+
4372
+ if (action === "completions") {
4373
+ const rawModelId = rest[0];
4374
+ if (!rawModelId) {
4375
+ return (
4376
+ <ErrorMessage error="Model ID required. Usage: inference completions <model-id> --prompt <text>" />
4377
+ );
4378
+ }
4379
+ const modelId = normalizeModelId(rawModelId);
4380
+ const prompt = flags["prompt"];
4381
+ if (!prompt) {
4382
+ return <ErrorMessage error="--prompt is required for text completions" />;
4383
+ }
4384
+
4385
+ const systemMsg = flags["system"];
4386
+ const maxTokens = flags["max-tokens"] ? parseInt(flags["max-tokens"], 10) : 256;
4387
+ if (Number.isNaN(maxTokens) || maxTokens < 1) {
4388
+ return <ErrorMessage error="--max-tokens must be a positive integer" />;
3451
4389
  }
3452
- // Default: show all models
3453
- return <ModelListCommand filter="all" />;
4390
+ const temperature = flags["temperature"] ? parseFloat(flags["temperature"]) : 0.7;
4391
+ if (Number.isNaN(temperature) || temperature < 0 || temperature > 2) {
4392
+ return <ErrorMessage error="--temperature must be a number between 0 and 2" />;
4393
+ }
4394
+ const topP = flags["top-p"] ? parseFloat(flags["top-p"]) : undefined;
4395
+ if (topP !== undefined && (Number.isNaN(topP) || topP < 0 || topP > 1)) {
4396
+ return <ErrorMessage error="--top-p must be a number between 0 and 1" />;
4397
+ }
4398
+ const stopValues = parseCommaSeparated(flags["stop"]);
4399
+ const stop =
4400
+ stopValues.length === 0 ? undefined : stopValues.length === 1 ? stopValues[0] : stopValues;
4401
+ const echo = flags["echo"]?.toLowerCase() === "true";
4402
+ const provider = flags["provider"];
4403
+
4404
+ const extraBody: Record<string, unknown> = {};
4405
+ if (topP !== undefined) extraBody.top_p = topP;
4406
+ if (provider) extraBody.provider = provider;
4407
+
4408
+ return (
4409
+ <ApiCommand
4410
+ action={() =>
4411
+ api.runTextCompletion({
4412
+ model: modelId,
4413
+ prompt: buildTextCompletionPrompt(prompt, systemMsg),
4414
+ max_tokens: maxTokens,
4415
+ temperature,
4416
+ ...(stop !== undefined ? { stop } : {}),
4417
+ ...(echo ? { echo: true } : {}),
4418
+ ...(Object.keys(extraBody).length > 0 ? { extra_body: extraBody } : {}),
4419
+ })
4420
+ }
4421
+ />
4422
+ );
4423
+ }
4424
+
4425
+ return <Help context="inference" />;
4426
+ }
4427
+
4428
+ // Model commands
4429
+ if (group === "model") {
4430
+ if (flags.help === "true" || !action || action === "help") {
4431
+ return <Help context="model" />;
3454
4432
  }
3455
- if (action === "delete") {
3456
- if (!rest[0]) {
3457
- return <ErrorMessage error="Job ID required. Usage: model delete <job-id>" />;
4433
+
4434
+ if (normalizedAction === "endpoints") {
4435
+ const endpointAction = rest[0];
4436
+ const endpointArgs = rest.slice(1);
4437
+
4438
+ if (flags.help === "true" || !endpointAction || endpointAction === "help") {
4439
+ return <Help context="model-endpoints" />;
3458
4440
  }
3459
- const jobId = rest[0];
3460
- if (jobId.length !== 36) {
4441
+
4442
+ if (endpointAction === "create") {
4443
+ const name = flags["name"];
4444
+ const icon = flags["icon"];
4445
+ const repo = flags["repo"];
4446
+ const description = flags["description"];
4447
+ const modelId = flags["model"] || "";
4448
+ const exampleStr = flags["example"];
4449
+
4450
+ if (flags["model-id"]) {
4451
+ return (
4452
+ <ErrorMessage error="Use --model to specify the base model reference. --model-id is deprecated." />
4453
+ );
4454
+ }
4455
+
4456
+ if (flags["base-model"] || flags["active-model-id"]) {
4457
+ return (
4458
+ <ErrorMessage error="Use --model to specify the model reference. --base-model and --active-model-id are no longer supported." />
4459
+ );
4460
+ }
4461
+
4462
+ if (!modelId) {
4463
+ const parsedExample = parseProjectExample(exampleStr);
4464
+ if (parsedExample.error) {
4465
+ return <ErrorMessage error={parsedExample.error} />;
4466
+ }
4467
+ return (
4468
+ <ModelCreateInteractive
4469
+ name={name}
4470
+ icon={icon}
4471
+ repo={repo}
4472
+ description={description}
4473
+ example={parsedExample.value}
4474
+ />
4475
+ );
4476
+ }
4477
+
4478
+ const parsedExample = parseProjectExample(exampleStr);
4479
+ if (parsedExample.error) {
4480
+ return <ErrorMessage error={parsedExample.error} />;
4481
+ }
4482
+
3461
4483
  return (
3462
- <Box flexDirection="column">
3463
- <ErrorMessage error="Invalid job ID: must be full UUID (36 characters)" />
3464
- <Text dimColor> Provided: {jobId} ({jobId.length} characters)</Text>
3465
- <Text dimColor> Tip: Use 'pioneer model list' to see full job IDs</Text>
3466
- </Box>
4484
+ <ApiCommand
4485
+ action={() =>
4486
+ api.createProject({
4487
+ name: name ?? modelId,
4488
+ ...(icon ? { icon } : {}),
4489
+ ...(repo ? { repo } : {}),
4490
+ ...(description ? { description } : {}),
4491
+ ...(modelId ? { active_model_id: modelId } : {}),
4492
+ ...(modelId ? { selected_model_id: modelId } : {}),
4493
+ ...(parsedExample.value ? { example: parsedExample.value } : {}),
4494
+ })
4495
+ }
4496
+ successMessage="Model entry created"
4497
+ />
3467
4498
  );
3468
4499
  }
3469
- return (
3470
- <ApiCommand
3471
- action={() => api.deleteModel(jobId)}
3472
- successMessage={`Model ${jobId} deleted`}
3473
- />
3474
- );
3475
- }
3476
- if (action === "download") {
3477
- if (!rest[0]) {
3478
- return <ErrorMessage error="Job ID required. Usage: model download <job-id>" />;
4500
+
4501
+ if (endpointAction === "list") {
4502
+ return <ModelListCommand filter="registered" />;
4503
+ }
4504
+
4505
+ if (endpointAction === "get") {
4506
+ const modelId = endpointArgs[0];
4507
+ if (!modelId) {
4508
+ return <ErrorMessage error="Model ID required: model endpoints get <model-id>" />;
4509
+ }
4510
+ return <ApiCommand action={() => api.getProject(modelId)} />;
3479
4511
  }
3480
- const jobId = rest[0];
3481
- if (jobId.length !== 36) {
4512
+
4513
+ if (endpointAction === "update") {
4514
+ const modelId = endpointArgs[0];
4515
+ if (!modelId) {
4516
+ return <ErrorMessage error="Model ID required: model endpoints update <model-id>" />;
4517
+ }
4518
+
4519
+ const name = flags["name"];
4520
+ const icon = flags["icon"];
4521
+ const repo = flags["repo"];
4522
+ const description = flags["description"];
4523
+ const selectedModelId = flags["model-id"];
4524
+
4525
+ if (!name && !icon && !repo && !description && !selectedModelId) {
4526
+ return (
4527
+ <ErrorMessage error="Provide at least one field to update: --name, --icon, --repo, --description, or --model-id" />
4528
+ );
4529
+ }
4530
+
3482
4531
  return (
3483
- <Box flexDirection="column">
3484
- <ErrorMessage error="Invalid job ID: must be full UUID (36 characters)" />
3485
- <Text dimColor> Provided: {jobId} ({jobId.length} characters)</Text>
3486
- <Text dimColor> Tip: Use 'pioneer model list' to see full job IDs</Text>
3487
- </Box>
4532
+ <ApiCommand
4533
+ action={() =>
4534
+ api.updateProject(modelId, {
4535
+ ...(name ? { name } : {}),
4536
+ ...(icon ? { icon } : {}),
4537
+ ...(repo ? { repo } : {}),
4538
+ ...(description ? { description } : {}),
4539
+ ...(selectedModelId ? { selected_model_id: selectedModelId } : {}),
4540
+ })
4541
+ }
4542
+ successMessage="Model updated"
4543
+ />
3488
4544
  );
3489
4545
  }
3490
- return <ApiCommand action={() => api.downloadModel(jobId)} />;
3491
- }
3492
- // Model upload command
3493
- if (action === "upload") {
3494
- const destination = flags["to"];
3495
4546
 
3496
- // Show help if no arguments provided
3497
- if (!rest[0] && !destination) {
4547
+ if (endpointAction === "delete") {
4548
+ const modelId = endpointArgs[0];
4549
+ if (!modelId) {
4550
+ return <ErrorMessage error="Model ID required: model endpoints delete <model-id>" />;
4551
+ }
3498
4552
  return (
3499
- <Box flexDirection="column">
3500
- <Text bold>Model Upload:</Text>
3501
- <Text> </Text>
3502
- <Text> Upload to Hugging Face:</Text>
3503
- <Text> model upload {"<job-id>"} --to hf --repo {"<repo>"} [--hf-token {"<token>"}] [--private]</Text>
3504
- <Text> </Text>
3505
- <Text dimColor> Supported destinations: hf (more coming soon)</Text>
3506
- </Box>
4553
+ <ApiCommand
4554
+ action={() => api.deleteProject(modelId)}
4555
+ successMessage={`Model ${modelId} deleted`}
4556
+ />
3507
4557
  );
3508
4558
  }
3509
4559
 
3510
- // Upload to Hugging Face
3511
- if (destination === "hf") {
3512
- if (!rest[0]) {
3513
- return <ErrorMessage error="Job ID required: model upload <job-id> --to hf --repo <repo>" />;
4560
+ if (endpointAction === "dataset-count" || endpointAction === "count") {
4561
+ const modelId = endpointArgs[0];
4562
+ if (!modelId) {
4563
+ return <ErrorMessage error="Model ID required: model endpoints dataset-count <model-id>" />;
3514
4564
  }
3515
- const jobId = rest[0];
3516
- const repo = flags["repo"];
3517
- const hfTokenFlag = flags["hf-token"];
3518
- const isPrivate = flags["private"]?.toLowerCase() === "true";
4565
+ return <ApiCommand action={() => api.getProjectDatasetCount(modelId)} />;
4566
+ }
3519
4567
 
3520
- if (!repo) {
3521
- return <ErrorMessage error="--repo is required (e.g., username/model-name)" />;
4568
+ if (endpointAction === "quality-metrics" || endpointAction === "quality") {
4569
+ const modelId = endpointArgs[0];
4570
+ if (!modelId) {
4571
+ return <ErrorMessage error="Model ID required: model endpoints quality-metrics <model-id>" />;
3522
4572
  }
4573
+ return <ApiCommand action={() => api.getProjectQualityMetrics(modelId)} />;
4574
+ }
3523
4575
 
3524
- const hfToken = getHfToken(hfTokenFlag);
3525
- if (!hfToken) {
4576
+ if (endpointAction === "deploy") {
4577
+ const modelId = endpointArgs[0];
4578
+ const jobId = flags["job"];
4579
+
4580
+ if (!modelId) {
4581
+ return <ErrorMessage error="Model ID required: model endpoints deploy <model-id> --job <training-job-id>" />;
4582
+ }
4583
+ if (!jobId) {
4584
+ return <ErrorMessage error="Training job ID required: model endpoints deploy <model-id> --job <training-job-id>" />;
4585
+ }
4586
+ if (jobId.length !== 36) {
3526
4587
  return (
3527
4588
  <Box flexDirection="column">
3528
- <ErrorMessage error="Hugging Face token required." />
3529
- <Text> </Text>
3530
- <Text>Set your token with:</Text>
3531
- <Text color="cyan"> pioneer auth hf</Text>
3532
- <Text> </Text>
3533
- <Text dimColor>Get a token at: https://huggingface.co/settings/tokens</Text>
4589
+ <ErrorMessage error="Invalid job ID: must be full UUID (36 characters)" />
4590
+ <Text dimColor> Provided: {jobId} ({jobId.length} characters)</Text>
4591
+ <Text dimColor> Tip: Use 'pioneer model artifacts list' and 'pioneer model artifacts trained' to see full job IDs</Text>
3534
4592
  </Box>
3535
4593
  );
3536
4594
  }
3537
4595
 
4596
+ const reason = flags["reason"];
4597
+
3538
4598
  return (
3539
4599
  <ApiCommand
3540
4600
  action={() =>
3541
- api.pushModelToHub(jobId, {
3542
- hf_token: hfToken,
3543
- repo_id: repo,
3544
- private: isPrivate,
4601
+ api.deployTrainingJobToProject(modelId, {
4602
+ training_job_id: jobId,
4603
+ ...(reason ? { reason } : {}),
3545
4604
  })
3546
4605
  }
3547
- successMessage={`Model uploaded to Hugging Face: ${repo}`}
4606
+ successMessage={`Deployment initiated for project ${modelId} from job ${jobId}`}
4607
+ />
4608
+ );
4609
+ }
4610
+
4611
+ if (endpointAction === "rollback") {
4612
+ const modelId = endpointArgs[0];
4613
+ const deploymentId = endpointArgs[1];
4614
+
4615
+ if (!modelId) {
4616
+ return <ErrorMessage error="Model ID required: model endpoints rollback <model-id> <deployment-id>" />;
4617
+ }
4618
+ if (!deploymentId) {
4619
+ return (
4620
+ <ErrorMessage error="Deployment ID required: model endpoints rollback <model-id> <deployment-id>" />
4621
+ );
4622
+ }
4623
+ if (deploymentId.length !== 36) {
4624
+ return (
4625
+ <Box flexDirection="column">
4626
+ <ErrorMessage error="Invalid deployment ID: must be full UUID (36 characters)" />
4627
+ <Text dimColor> Provided: {deploymentId} ({deploymentId.length} characters)</Text>
4628
+ </Box>
4629
+ );
4630
+ }
4631
+
4632
+ return (
4633
+ <ApiCommand
4634
+ action={() => api.rollbackProjectDeployment(modelId, deploymentId)}
4635
+ successMessage={`Rollback initiated for endpoint ${modelId} using deployment ${deploymentId}`}
3548
4636
  />
3549
4637
  );
3550
4638
  }
3551
4639
 
3552
- return <ErrorMessage error="--to is required. Supported destinations: hf" />;
4640
+ return <Help context="model-endpoints" />;
3553
4641
  }
3554
- // Model predict command (encoder inference)
3555
- if (action === "predict") {
3556
- const modelId = rest[0];
3557
- if (!modelId) {
3558
- return <ErrorMessage error="Model ID required. Usage: model predict <model-id> --text <text> --labels <labels>" />;
4642
+
4643
+ if (normalizedAction === "artifacts") {
4644
+ const artifactsAction = rest[0];
4645
+ const artifactArgs = rest.slice(1);
4646
+
4647
+ if (flags.help === "true" || !artifactsAction || artifactsAction === "help") {
4648
+ return <Help context="model-artifacts" />;
3559
4649
  }
3560
- const text = flags["text"];
3561
- const labelsStr = flags["labels"];
3562
- if (!text) {
3563
- return <ErrorMessage error="--text is required for inference" />;
4650
+
4651
+ if (artifactsAction === "list") {
4652
+ return <ModelListCommand filter="artifacts" />;
3564
4653
  }
3565
- if (!labelsStr) {
3566
- return <ErrorMessage error="--labels is required (comma-separated, e.g., PERSON,ORG,LOCATION)" />;
4654
+
4655
+ if (artifactsAction === "trained") {
4656
+ return <ModelListCommand filter="trained" />;
3567
4657
  }
3568
- const labels = labelsStr.split(",").map((l: string) => l.trim());
3569
- const task = (flags["task"] || "extract_entities") as "extract_entities" | "classify_text" | "extract_json" | "schema";
3570
- const threshold = flags["threshold"] ? parseFloat(flags["threshold"]) : 0.5;
3571
4658
 
3572
- return (
3573
- <ApiCommand
3574
- action={() =>
3575
- api.runInference({
3576
- model_id: modelId,
3577
- task,
3578
- text,
3579
- schema: labels,
3580
- threshold,
3581
- include_confidence: true,
3582
- })
3583
- }
3584
- />
3585
- );
3586
- }
3587
- // Model generate command (decoder inference)
3588
- if (action === "generate") {
3589
- const modelId = rest[0];
3590
- if (!modelId) {
3591
- return <ErrorMessage error="Model ID required. Usage: model generate <model-id> --prompt <text>" />;
4659
+ if (artifactsAction === "deployed") {
4660
+ return <ModelListCommand filter="deployed" />;
3592
4661
  }
3593
- const prompt = flags["prompt"];
3594
- if (!prompt) {
3595
- return <ErrorMessage error="--prompt is required for text generation" />;
4662
+
4663
+ if (artifactsAction === "download") {
4664
+ if (!artifactArgs[0]) {
4665
+ return <ErrorMessage error="Job ID required: model artifacts download <job-id>" />;
4666
+ }
4667
+ const jobId = artifactArgs[0];
4668
+ if (jobId.length !== 36) {
4669
+ return (
4670
+ <Box flexDirection="column">
4671
+ <ErrorMessage error="Invalid job ID: must be full UUID (36 characters)" />
4672
+ <Text dimColor> Provided: {jobId} ({jobId.length} characters)</Text>
4673
+ <Text dimColor> Tip: Use 'pioneer model artifacts trained' or 'pioneer model artifacts deployed' to see full job IDs</Text>
4674
+ </Box>
4675
+ );
4676
+ }
4677
+ return <ApiCommand action={() => api.downloadModel(jobId)} />;
3596
4678
  }
3597
- const systemMsg = flags["system"];
3598
- const maxTokens = flags["max-tokens"] ? parseInt(flags["max-tokens"]) : 256;
3599
- const temperature = flags["temperature"] ? parseFloat(flags["temperature"]) : 0.7;
4679
+
4680
+ if (artifactsAction === "delete") {
4681
+ if (!artifactArgs[0]) {
4682
+ return <ErrorMessage error="Model ID required: model artifacts delete <job-id>" />;
4683
+ }
4684
+ const jobId = artifactArgs[0];
4685
+ if (jobId.length !== 36) {
4686
+ return (
4687
+ <Box flexDirection="column">
4688
+ <ErrorMessage error="Invalid job ID: must be full UUID (36 characters)" />
4689
+ <Text dimColor> Provided: {jobId} ({jobId.length} characters)</Text>
4690
+ <Text dimColor> Tip: Use 'pioneer model artifacts list' to see full job IDs</Text>
4691
+ </Box>
4692
+ );
4693
+ }
4694
+ return (
4695
+ <ApiCommand
4696
+ action={() => api.deleteModel(jobId)}
4697
+ successMessage={`Model ${jobId} deleted`}
4698
+ />
4699
+ );
4700
+ }
4701
+
4702
+ // Model upload command
4703
+ if (artifactsAction === "upload") {
4704
+ const destination = flags["to"];
4705
+
4706
+ if (!artifactArgs[0] && !destination) {
4707
+ return (
4708
+ <Box flexDirection="column">
4709
+ <Text bold>Model Upload:</Text>
4710
+ <Text> </Text>
4711
+ <Text> Upload to Hugging Face:</Text>
4712
+ <Text> model artifacts upload {"<job-id>"} --to hf --repo {"<repo>"} [--hf-token {"<token>"}] [--private]</Text>
4713
+ <Text> </Text>
4714
+ <Text dimColor> Supported destinations: hf (more coming soon)</Text>
4715
+ </Box>
4716
+ );
4717
+ }
4718
+
4719
+ if (destination === "hf") {
4720
+ if (!artifactArgs[0]) {
4721
+ return <ErrorMessage error="Job ID required: model artifacts upload <job-id> --to hf --repo <repo>" />;
4722
+ }
4723
+ const jobId = artifactArgs[0];
4724
+ const repo = flags["repo"];
4725
+ const hfTokenFlag = flags["hf-token"];
4726
+ const isPrivate = flags["private"]?.toLowerCase() === "true";
4727
+
4728
+ if (!repo) {
4729
+ return <ErrorMessage error="--repo is required (e.g., username/model-name)" />;
4730
+ }
4731
+
4732
+ const hfToken = getHfToken(hfTokenFlag);
4733
+ if (!hfToken) {
4734
+ return (
4735
+ <Box flexDirection="column">
4736
+ <ErrorMessage error="Hugging Face token required." />
4737
+ <Text> </Text>
4738
+ <Text>Set your token with:</Text>
4739
+ <Text color="cyan"> pioneer auth hf</Text>
4740
+ <Text> </Text>
4741
+ <Text dimColor>Get a token at: https://huggingface.co/settings/tokens</Text>
4742
+ </Box>
4743
+ );
4744
+ }
4745
+
4746
+ return (
4747
+ <ApiCommand
4748
+ action={() =>
4749
+ api.pushModelToHub(jobId, {
4750
+ hf_token: hfToken,
4751
+ repo_id: repo,
4752
+ private: isPrivate,
4753
+ })
4754
+ }
4755
+ successMessage={`Model uploaded to Hugging Face: ${repo}`}
4756
+ />
4757
+ );
4758
+ }
4759
+
4760
+ return <ErrorMessage error="--to is required. Supported destinations: hf" />;
4761
+ }
4762
+
4763
+ return <Help context="model-artifacts" />;
4764
+ }
4765
+
4766
+ if (action === "predict" || action === "generate") {
3600
4767
  return (
3601
- <ModelGenerateCommand
3602
- modelId={modelId}
3603
- prompt={prompt}
3604
- systemMsg={systemMsg}
3605
- maxTokens={maxTokens}
3606
- temperature={temperature}
4768
+ <ErrorMessage
4769
+ error={
4770
+ "model predict and model generate are no longer supported. Use model endpoints/... and model artifacts/... instead."
4771
+ }
3607
4772
  />
3608
4773
  );
3609
4774
  }
3610
- return <Help context="model" />;
4775
+
4776
+ return (
4777
+ <ErrorMessage
4778
+ error={`Unknown model command: model ${action}. Use 'pioneer model_endpoints ...', 'pioneer model endpoints ...', 'pioneer model_artifacts ...', or 'pioneer model artifacts ...'.`}
4779
+ />
4780
+ );
3611
4781
  }
3612
4782
 
3613
4783
  // Eval commands
@@ -3625,9 +4795,60 @@ const App: React.FC<AppProps> = ({ command, flags }) => {
3625
4795
  if (action === "list" && !rest[0]) {
3626
4796
  return <ErrorMessage error="Dataset required: eval list <name[:version]>" />;
3627
4797
  }
4798
+ if (action === "baseline-models") {
4799
+ return <ApiCommand action={api.listBaselineModels} />;
4800
+ }
3628
4801
  if (action === "get" && rest[0]) {
3629
4802
  return <ApiCommand action={() => api.getEvaluation(rest[0])} />;
3630
4803
  }
4804
+ if (action === "delete" && rest[0]) {
4805
+ const evaluationId = rest[0];
4806
+ return (
4807
+ <ApiCommand
4808
+ action={async () => {
4809
+ const result = await api.deleteEvaluation(evaluationId);
4810
+ if (!result.ok && (result.status === 401 || result.status === 403) && result.error) {
4811
+ const lower = result.error.toLowerCase();
4812
+ const indicatesJwt = lower.includes("jwt") ||
4813
+ lower.includes("api key access") ||
4814
+ lower.includes("table") ||
4815
+ lower.includes("requires authentication");
4816
+ if (indicatesJwt) {
4817
+ return {
4818
+ ok: false,
4819
+ status: result.status,
4820
+ error:
4821
+ "Evaluation deletion requires a JWT-authenticated session. Run 'pioneer auth login' to sign in with your account credentials.",
4822
+ };
4823
+ }
4824
+ }
4825
+ return result;
4826
+ }}
4827
+ />
4828
+ );
4829
+ }
4830
+ if (action === "update") {
4831
+ const mode = rest[0];
4832
+ const evaluationId = mode === "project" ? rest[1] : rest[0];
4833
+ if (!evaluationId) {
4834
+ return <Help context="eval" />;
4835
+ }
4836
+ const projectId = flags["project-id"];
4837
+ if (!projectId) {
4838
+ return <ErrorMessage error="--project-id is required" />;
4839
+ }
4840
+ return (
4841
+ <ApiCommand
4842
+ action={() =>
4843
+ api.updateEvaluationProject({
4844
+ evaluation_id: evaluationId,
4845
+ project_id: projectId,
4846
+ })
4847
+ }
4848
+ successMessage={`Evaluation ${evaluationId} reassigned`}
4849
+ />
4850
+ );
4851
+ }
3631
4852
  if (action === "create") {
3632
4853
  const modelId = flags["model-id"];
3633
4854
  const datasetStr = flags["dataset"];
@@ -3671,21 +4892,35 @@ const App: React.FC<AppProps> = ({ command, flags }) => {
3671
4892
  return <ApiCommand action={api.listBenchmarks} />;
3672
4893
  }
3673
4894
  if (action === "run") {
3674
- const modelId = flags["model-id"];
4895
+ const rawModelId = flags["model-id"];
3675
4896
  const task = flags["task"] as "ner" | "text_classification";
3676
4897
  const benchmark = flags["benchmark"];
3677
4898
  const maxSamples = flags["max-samples"] ? parseInt(flags["max-samples"], 10) : undefined;
3678
4899
  const split = flags["split"];
3679
4900
 
3680
- if (!modelId || !task || !benchmark) {
4901
+ if (!rawModelId || !task || !benchmark) {
3681
4902
  return <ErrorMessage error="--model-id, --task, and --benchmark are required" />;
3682
4903
  }
4904
+ const normalizedModelId = normalizeModelId(rawModelId);
4905
+ if (!isUuid(normalizedModelId)) {
4906
+ return (
4907
+ <ErrorMessage
4908
+ error="Benchmark model-id must be a training job UUID (example: 72c1ac92-3a89-439d-afe3-687d8a935c06)."
4909
+ />
4910
+ );
4911
+ }
4912
+ if (task !== "ner" && task !== "text_classification") {
4913
+ return <ErrorMessage error="--task must be either ner or text_classification" />;
4914
+ }
4915
+ if (maxSamples !== undefined && (Number.isNaN(maxSamples) || maxSamples < 1)) {
4916
+ return <ErrorMessage error="--max-samples must be a positive integer" />;
4917
+ }
3683
4918
 
3684
4919
  return (
3685
4920
  <ApiCommand
3686
4921
  action={() =>
3687
4922
  api.startBenchmarkEvaluation({
3688
- model_id: modelId,
4923
+ model_id: normalizedModelId,
3689
4924
  task,
3690
4925
  benchmark,
3691
4926
  max_samples: maxSamples,
@@ -3710,152 +4945,64 @@ const App: React.FC<AppProps> = ({ command, flags }) => {
3710
4945
  return <Help context="benchmark" />;
3711
4946
  }
3712
4947
 
3713
- // Notebook commands
3714
- if (group === "notebook") {
3715
- if (flags.help === "true" || !action || action === "help") {
3716
- return <Help context="notebook" />;
3717
- }
3718
-
3719
- if (action === "run") {
3720
- const file = rest[0];
3721
- if (!file) {
3722
- return <ErrorMessage error="File path required: notebook run <file.ipynb>" />;
3723
- }
3724
- const gpu = flags.gpu || "cpu";
3725
- const loadFelixHelpers = flags["no-felix"] !== "true";
3726
- return <NotebookRunCommand filePath={file} gpu={gpu} loadFelixHelpers={loadFelixHelpers} />;
3727
- }
3728
-
3729
- if (action === "create") {
3730
- const name = rest[0] || flags.name;
3731
- if (!name) {
3732
- return <ErrorMessage error="Name required: notebook create <name>" />;
3733
- }
3734
- try {
3735
- const filePath = createBlankNotebook(name);
3736
- return <Success message={`Created notebook: ${filePath}`} />;
3737
- } catch (e) {
3738
- return <ErrorMessage error={e instanceof Error ? e.message : String(e)} />;
3739
- }
3740
- }
3741
-
3742
- if (action === "sessions") {
3743
- return <ApiCommand action={api.listNotebookSessions} />;
4948
+ // Adaptive agent commands (new short command)
4949
+ if (group === "agent") {
4950
+ if (flags.help === "true" || action === "help") {
4951
+ return <Help context="agent" />;
3744
4952
  }
3745
4953
 
3746
- if (action === "stop") {
3747
- const sessionId = rest[0];
3748
- if (!sessionId) {
3749
- return <ErrorMessage error="Session ID required: notebook stop <session-id>" />;
3750
- }
4954
+ if (action && !action.startsWith("-")) {
3751
4955
  return (
3752
- <ApiCommand
3753
- action={() => api.terminateNotebookSession(sessionId)}
3754
- successMessage={`Session ${sessionId} terminated`}
4956
+ <ErrorMessage
4957
+ error={
4958
+ 'Invalid agent command syntax. Use one of:\n' +
4959
+ "pioneer agent\n" +
4960
+ "pioneer agent --mode research"
4961
+ }
3755
4962
  />
3756
4963
  );
3757
4964
  }
3758
4965
 
3759
- return <Help context="notebook" />;
3760
- }
3761
-
3762
- // Adaptive fine-tuning commands
3763
- if (group === "adaptive-finetuning" || group === "aft") {
3764
- if (flags.help === "true" || action === "help") {
3765
- return <Help context="adaptive-finetuning" />;
3766
- }
3767
-
3768
- let message = flags.message;
3769
- if (!message) {
3770
- const rawParts = action === "chat" ? rest : [action, ...rest];
3771
- message = rawParts.filter(Boolean).join(" ").trim();
4966
+ if (flags.message) {
4967
+ return <ErrorMessage error="The --message flag has been removed for agent. Run `pioneer agent` or `pioneer agent --mode research` and provide input interactively." />;
3772
4968
  }
3773
4969
 
3774
- if (!message) {
3775
- return (
3776
- <ErrorMessage
3777
- error={'Message required. Usage: adaptive-finetuning chat --message "Analyze failures and propose retraining plan"'}
3778
- />
3779
- );
4970
+ const rawMode = flags.mode;
4971
+ const validModes = ["research"];
4972
+ const normalizedMode = rawMode ? rawMode.toLowerCase() : undefined;
4973
+ if (normalizedMode && !validModes.includes(normalizedMode)) {
4974
+ return <ErrorMessage error="--mode must be one of: research" />;
3780
4975
  }
4976
+ const mode = (normalizedMode === "research" ? "research" : "standard") as "standard" | "research";
3781
4977
 
3782
- let history: api.AdaptiveFinetuningHistoryItem[] | undefined;
4978
+ let history: api.AgentChatHistoryItem[] | undefined;
3783
4979
  if (flags.history) {
3784
4980
  try {
3785
4981
  const parsed = JSON.parse(flags.history) as unknown;
3786
4982
  if (!Array.isArray(parsed)) {
3787
4983
  return <ErrorMessage error="--history must be a JSON array" />;
3788
4984
  }
3789
- history = parsed as api.AdaptiveFinetuningHistoryItem[];
4985
+ history = parsed as api.AgentChatHistoryItem[];
3790
4986
  } catch {
3791
4987
  return <ErrorMessage error="--history must be valid JSON" />;
3792
4988
  }
3793
4989
  }
3794
4990
 
3795
- let filters: api.AdaptiveFinetuningQueryFilters | undefined;
3796
4991
  if (flags.filters) {
3797
- try {
3798
- const parsed = JSON.parse(flags.filters) as unknown;
3799
- if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
3800
- return <ErrorMessage error="--filters must be a JSON object" />;
3801
- }
3802
- filters = parsed as api.AdaptiveFinetuningQueryFilters;
3803
- } catch {
3804
- return <ErrorMessage error="--filters must be valid JSON" />;
3805
- }
4992
+ return <ErrorMessage error='--filters is not supported for /auto-agent/clarify. Omit this flag for now.' />;
3806
4993
  }
3807
4994
 
3808
4995
  return (
3809
- <ApiCommand
3810
- action={() =>
3811
- api.adaptiveFinetuningChat({
3812
- message,
3813
- ...(flags["conversation-id"] ? { conversation_id: flags["conversation-id"] } : {}),
3814
- ...(history ? { history } : {}),
3815
- ...(filters ? { filters } : {}),
3816
- })
3817
- }
3818
- successMessage="Adaptive fine-tuning response received"
4996
+ <AgentInteractivePrompt
4997
+ conversationId={flags["conversation-id"]}
4998
+ history={history}
4999
+ mode={mode}
3819
5000
  />
3820
5001
  );
3821
5002
  }
3822
5003
 
3823
- // Competition commands
3824
- if (group === "competition") {
3825
- if (flags.help === "true" || !action || action === "help") {
3826
- return <Help context="competition" />;
3827
- }
3828
- if (action === "list") {
3829
- return <CompetitionListCommand />;
3830
- }
3831
- if (action === "show" && rest[0]) {
3832
- return <ApiCommand action={() => api.getCompetitionSamples(rest[0])} />;
3833
- }
3834
- if (action === "leaderboard" && rest[0]) {
3835
- const limit = flags["limit"] ? parseInt(flags["limit"], 10) : undefined;
3836
- return <LeaderboardCommand datasetId={rest[0]} limit={limit} />;
3837
- }
3838
- if (action === "submit" && rest[0]) {
3839
- const evalId = flags["eval-id"];
3840
- const displayName = flags["name"];
3841
-
3842
- if (!evalId || !displayName) {
3843
- return <ErrorMessage error="--eval-id and --name are required" />;
3844
- }
3845
-
3846
- return (
3847
- <ApiCommand
3848
- action={() =>
3849
- api.submitToLeaderboard(rest[0], {
3850
- evaluation_id: evalId,
3851
- display_name: displayName,
3852
- })
3853
- }
3854
- successMessage="Submitted to leaderboard"
3855
- />
3856
- );
3857
- }
3858
- return <Help context="competition" />;
5004
+ if (group === "notebook") {
5005
+ return <ErrorMessage error="The notebook command is deprecated and has been removed from this CLI." />;
3859
5006
  }
3860
5007
 
3861
5008
  return <Help />;
@@ -3869,7 +5016,7 @@ import packageJson from "../package.json";
3869
5016
 
3870
5017
  async function main() {
3871
5018
  const argv = process.argv.slice(2);
3872
- const { command, flags } = parseArgs(argv);
5019
+ const { command, flags, parseErrors } = parseArgs(argv);
3873
5020
 
3874
5021
  // Handle version flag early (before React render)
3875
5022
  if (flags.version === "true" || flags.v === "true") {
@@ -3877,7 +5024,7 @@ async function main() {
3877
5024
  process.exit(0);
3878
5025
  }
3879
5026
 
3880
- await render(<App command={command} flags={flags} />).waitUntilExit();
5027
+ await render(<App command={command} flags={flags} parseErrors={parseErrors} />).waitUntilExit();
3881
5028
  }
3882
5029
 
3883
5030
  main();