@toloka-platform/sdk 0.2.0-preview.2 → 0.2.0-preview.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +10 -4
  2. package/dist/cli/toloka.js +159 -151
  3. package/dist/cli/toloka.js.jsc +0 -0
  4. package/dist/cli/toloka.js.jsc.sha256 +1 -1
  5. package/dist/cli/toloka.js.sha256 +1 -1
  6. package/docs/authoring.md +18 -1
  7. package/docs/cli-v2.md +4 -3
  8. package/docs/cli.md +29 -10
  9. package/docs/getting-started.md +3 -3
  10. package/docs/limitations.md +1 -1
  11. package/docs/testing.md +1 -1
  12. package/docs/troubleshooting.md +1 -1
  13. package/examples/face-image-collection/README.md +2 -2
  14. package/openapi/gen/api.ts +78 -0
  15. package/openapi/gen/models/checkReport.ts +2 -1
  16. package/openapi/gen/models/checkReportAnyOfTwoseven.ts +5 -0
  17. package/openapi/gen/models/checkReportAnyOfTwosevenResultsItem.ts +6 -0
  18. package/openapi/gen/models/checkReportAnyOfTwosevenResultsItemVerdict.ts +2 -0
  19. package/openapi/gen/models/currentUser.ts +1 -0
  20. package/openapi/gen/models/deliveryOrder.ts +9 -0
  21. package/openapi/gen/models/deliveryOrderClientName.ts +2 -0
  22. package/openapi/gen/models/getTrainingRunMetricHistoryParams.ts +4 -0
  23. package/openapi/gen/models/index.ts +19 -0
  24. package/openapi/gen/models/listDeliveryOrders200.ts +5 -0
  25. package/openapi/gen/models/trainingRunMetricHistory.ts +9 -0
  26. package/openapi/gen/models/trainingRunMetricHistoryMetric.ts +2 -0
  27. package/openapi/gen/models/trainingRunMetricHistoryPointsItem.ts +9 -0
  28. package/openapi/gen/models/trainingRunMetricHistoryPointsItemNonFinite.ts +2 -0
  29. package/openapi/gen/models/trainingRunMetricHistoryPointsItemValue.ts +2 -0
  30. package/openapi/gen/models/trainingRunMetrics.ts +9 -0
  31. package/openapi/gen/models/trainingRunMetricsMetricsItem.ts +8 -0
  32. package/openapi/gen/models/trainingRunMetricsMetricsItemLatest.ts +9 -0
  33. package/openapi/gen/models/trainingRunMetricsMetricsItemLatestNonFinite.ts +2 -0
  34. package/openapi/gen/models/trainingRunMetricsMetricsItemLatestValue.ts +2 -0
  35. package/openapi/gen/models/trainingRunMetricsMetricsItemMetric.ts +2 -0
  36. package/openapi/gen/models/trainingRunMetricsUnavailableReason.ts +2 -0
  37. package/package.json +2 -2
  38. package/skills/catalog.json +55 -7
  39. package/skills/toloka-build/SKILL.md +10 -7
  40. package/skills/toloka-build/references/node-semantics.md +6 -4
  41. package/skills/toloka-build/references/workflow-and-cli.md +21 -13
  42. package/skills/toloka-fine-tuning/SKILL.md +72 -0
  43. package/skills/toloka-get-started/SKILL.md +55 -0
  44. package/skills/toloka-gisting/SKILL.md +76 -0
  45. package/skills/toloka-report-issue/references/report-template.md +1 -1
  46. package/src/authoring/index.ts +14 -0
  47. package/src/checks/task-ui-typecheck.ts +12 -10
  48. package/src/cli/analytics.ts +52 -6
  49. package/src/cli/auth.ts +69 -16
  50. package/src/cli/auto-update.ts +24 -33
  51. package/src/cli/check.ts +12 -10
  52. package/src/cli/commands/auth.ts +89 -0
  53. package/src/cli/commands/config.ts +28 -0
  54. package/src/cli/commands/dataset.ts +75 -0
  55. package/src/cli/commands/delivery-order.ts +18 -0
  56. package/src/cli/commands/models.ts +122 -0
  57. package/src/cli/commands/pipeline.ts +132 -0
  58. package/src/cli/commands/project.ts +175 -0
  59. package/src/cli/commands/report.ts +39 -0
  60. package/src/cli/commands/secret.ts +39 -0
  61. package/src/cli/commands/skill.ts +36 -0
  62. package/src/cli/commands/source.ts +204 -0
  63. package/src/cli/commands/status.ts +18 -0
  64. package/src/cli/commands/training.ts +228 -0
  65. package/src/cli/commands/version.ts +14 -0
  66. package/src/cli/config.ts +5 -6
  67. package/src/cli/context.ts +213 -0
  68. package/src/cli/delivery-orders.ts +30 -0
  69. package/src/cli/environment.ts +26 -0
  70. package/src/cli/init.ts +1 -4
  71. package/src/cli/program.ts +168 -1344
  72. package/src/cli/project-context.ts +9 -11
  73. package/src/cli/sdk-version.ts +38 -0
  74. package/src/cli/skill.ts +6 -2
  75. package/src/cli/terminal-io.ts +103 -0
  76. package/src/cli/trainings.ts +118 -27
  77. package/src/cli/version.ts +1 -1
  78. package/src/client.ts +18 -0
  79. package/src/importer.ts +32 -2
@@ -1,309 +1,92 @@
1
- import { Command, CommanderError, InvalidArgumentError } from "commander";
2
- import { assert } from "es-toolkit";
3
- import { writeFile } from "node:fs/promises";
4
- import { resolve } from "node:path";
5
- import { createInterface } from "node:readline/promises";
6
- import { createClient } from "../client";
7
- import type { PotatoClient } from "../client";
8
- import { readTolokaState } from "../state";
9
- import type { TolokaState } from "../state";
10
- import {
11
- CHECK_RESULT_FILE_ENV,
12
- checkProject,
13
- watchProjectChecks,
14
- runFreshCheck,
15
- type CheckProjectOptions,
16
- type CheckProjectResult,
17
- } from "./check";
18
- import { authStatus, login, logout } from "./auth";
19
- import type { AuthPrompt } from "./auth";
1
+ import { Command, CommanderError } from "commander";
2
+ import { createClient, type PotatoClient } from "../client";
20
3
  import { commandNameFromArgv, createCliAnalytics } from "./analytics";
21
4
  import type { CliAnalytics, CliAnalyticsCommandOutcome } from "./analytics";
22
5
  import type { AutoUpdateHooks } from "./auto-update";
23
6
  import { maybeAutoUpdate } from "./auto-update";
24
- import { PRODUCTION_BASE_URL, resolveInternalBaseUrl, resolveTolokaConfig } from "./config";
25
- import type { TolokaEnvironment } from "./config";
26
- import {
27
- findCommandCredentials,
28
- readStoredCredentials,
29
- requireStoredCredentials,
30
- } from "./credentials";
31
- import type { StoredCredentials } from "./credentials";
32
- import { getCliSetting, readCliSettings, setCliSetting } from "./settings";
33
- import {
34
- generateDatasetFieldDeclarations,
35
- listProjectDatasets,
36
- loadDataset,
37
- setProjectSecret,
38
- uploadDataset,
39
- } from "./data";
40
- import { SDK_VERSION, initProject, readAgentSkillStalenessWarning } from "./init";
41
- import {
42
- applyProject,
43
- importProject,
44
- planProject,
45
- projectStatus,
46
- validateProject,
47
- } from "./project";
7
+ import { CHECK_RESULT_FILE_ENV, type checkProject } from "./check";
8
+ import { registerAuthCommands } from "./commands/auth";
9
+ import { registerConfigCommands } from "./commands/config";
10
+ import { registerDatasetCommands } from "./commands/dataset";
11
+ import { registerDeliveryOrderCommands } from "./commands/delivery-order";
12
+ import { registerModelCommands } from "./commands/models";
13
+ import { registerPipelineCommands } from "./commands/pipeline";
14
+ import { registerProjectCommands } from "./commands/project";
15
+ import { registerReportCommand } from "./commands/report";
16
+ import { registerSecretCommands } from "./commands/secret";
17
+ import { registerSkillCommands } from "./commands/skill";
18
+ import { registerSourceCommands } from "./commands/source";
19
+ import { registerStatusCommand } from "./commands/status";
20
+ import { registerTrainingCommands } from "./commands/training";
21
+ import { registerVersionCommand } from "./commands/version";
22
+ import { resolveTolokaConfig, type TolokaEnvironment } from "./config";
23
+ import { findCommandCredentials } from "./credentials";
48
24
  import {
49
- archiveModel,
50
- createModel,
51
- downloadModelVersion,
52
- getBaseModel,
53
- getModel,
54
- listBaseModels,
55
- listModels,
56
- listModelVersions,
57
- parseBaseModelId,
58
- } from "./models";
59
- import {
60
- MISSING_PROJECT_ID_MESSAGE,
61
- addProject,
62
- createProject,
63
- currentProject,
64
- listRemoteProjects,
65
- removeProject,
66
- resolveProjectContext,
67
- switchProject,
68
- untrackProjectResource,
69
- } from "./project-context";
70
- import { estimatePipelineCost } from "./cost";
71
- import { submitIssueReport } from "./report";
72
- import type { IssueReportAuthor } from "./report";
25
+ CREDENTIAL_PRECEDENCE,
26
+ resolveInternalUser,
27
+ resolveSourceRoot,
28
+ type CliIo,
29
+ type CreatedProgram,
30
+ type GlobalOptions,
31
+ type ProgramContext,
32
+ } from "./context";
33
+ import { normalizeEnvironment } from "./environment";
73
34
  import { formatCliError } from "./format-error";
74
- import {
75
- cancelPipelineRun,
76
- listLivePipelines,
77
- listPipelineRuns,
78
- pickPipeline,
79
- pipelineRunResults,
80
- pipelineRunStatus,
81
- startPipelineRun,
82
- } from "./runs";
83
- import {
84
- cancelTrainingRun,
85
- createTraining,
86
- createTrainingRun,
87
- estimateTrainingCost,
88
- getTraining,
89
- getTrainingRun,
90
- readTrainingRunLogs,
91
- listTrainingRuns,
92
- resumeTrainingRun,
93
- listTrainings,
94
- updateTraining,
95
- } from "./trainings";
96
- import type { TrainingDraftOptions } from "./trainings";
97
-
98
- import type { RunOutcome } from "./runs";
99
- import { skillInstallCommand, skillListCommand, skillSearchCommand } from "./skill";
100
- import { cliVersionInfo } from "./version";
101
- import { findSourceRoot } from "./source";
102
-
103
- export type CliIo = {
104
- readonly stdout: (text: string) => void;
105
- readonly stderr: (text: string) => void;
106
- readonly readStdin: () => Promise<string>;
107
- readonly prompt?: AuthPrompt;
108
- readonly select?: (choices: readonly { name: string; value: string }[]) => Promise<string>;
109
- };
110
-
111
- export type ProgramContext = {
112
- readonly environment: TolokaEnvironment;
113
- readonly io: CliIo;
114
- readonly injectedClient?: PotatoClient;
115
- readonly runChecks?: typeof checkProject;
116
- readonly analytics?: CliAnalytics;
117
- };
118
-
119
- type GlobalOptions = {
120
- readonly sourceRoot?: string;
121
- readonly apiKey?: string;
122
- readonly baseUrl?: string;
123
- readonly projectId?: string;
35
+ import { readAgentSkillStalenessWarning } from "./init";
36
+ import { SDK_VERSION } from "./sdk-version";
37
+ import { terminalIo } from "./terminal-io";
38
+
39
+ export type { CliIo, CreatedProgram, ProgramContext } from "./context";
40
+ export { terminalIo } from "./terminal-io";
41
+
42
+ const argvFlag = (argv: readonly string[], flag: string): string | undefined => {
43
+ const index = argv.indexOf(flag);
44
+ if (index < 0) return undefined;
45
+ const value = argv[index + 1];
46
+ return value === undefined || value.startsWith("-") ? undefined : value;
124
47
  };
125
48
 
126
- const CREDENTIAL_PRECEDENCE = [
127
- "Credentials resolve in this order, highest first:",
128
- " 1. --api-key / --base-url",
129
- " 2. POTATO_API_KEY / POTATO_BASE_URL",
130
- " 3. the file written by toloka login",
131
- `The platform defaults to ${PRODUCTION_BASE_URL}.`,
132
- ].join("\n");
133
-
134
- const authoredByOption = (raw: string): IssueReportAuthor => {
135
- if (raw !== "human" && raw !== "agent")
136
- throw new InvalidArgumentError('Expected "human" or "agent".');
137
- return raw;
138
- };
139
-
140
- const positiveInteger = (raw: string): number => {
141
- const value = Number(raw);
142
- if (!Number.isInteger(value) || value <= 0)
143
- throw new InvalidArgumentError("Expected a positive integer.");
144
- return value;
145
- };
146
-
147
- const nonNegativeInteger = (raw: string): number => {
148
- const value = Number(raw);
149
- if (!Number.isInteger(value) || value < 0)
150
- throw new InvalidArgumentError("Expected a non-negative integer.");
151
- return value;
152
- };
153
-
154
- const trainingMethod = (raw: string): "sft" | "gisting" => {
155
- if (raw !== "sft" && raw !== "gisting") {
156
- throw new InvalidArgumentError('Expected training method "sft" or "gisting".');
157
- }
158
- return raw;
159
- };
160
-
161
- const trainingRegime = (raw: string): "low" | "medium" | "high" => {
162
- if (raw !== "low" && raw !== "medium" && raw !== "high") {
163
- throw new InvalidArgumentError('Expected regime "low", "medium", or "high".');
49
+ const resolveInternalCommandAccess = async (
50
+ argv: readonly string[],
51
+ environment: TolokaEnvironment,
52
+ injectedClient: PotatoClient | undefined,
53
+ ): Promise<boolean | undefined> => {
54
+ const commandName = commandNameFromArgv(argv);
55
+ if (
56
+ commandName !== "help" &&
57
+ commandName !== "project create" &&
58
+ !commandName.startsWith("delivery-order")
59
+ ) {
60
+ return undefined;
164
61
  }
165
- return raw;
166
- };
167
-
168
- type RawTrainingDraftOptions = {
169
- method?: TrainingDraftOptions["method"];
170
- baseModel?: TrainingDraftOptions["baseModelId"];
171
- modelVersion?: string;
172
- dataset?: string;
173
- validationDataset?: string;
174
- trainFilter?: string;
175
- validationFilter?: string;
176
- regime?: TrainingDraftOptions["regime"];
177
- epochs?: number;
178
- evalSteps?: number;
179
- promptFile?: string;
180
- };
181
-
182
- const trainingDraftOptions = (options: RawTrainingDraftOptions): TrainingDraftOptions => ({
183
- method: options.method,
184
- baseModelId: options.baseModel,
185
- modelVersionId: options.modelVersion,
186
- datasetId: options.dataset,
187
- validationDatasetId: options.validationDataset,
188
- trainingFilter: options.trainFilter,
189
- validationFilter: options.validationFilter,
190
- regime: options.regime,
191
- epochs: options.epochs,
192
- evalSteps: options.evalSteps,
193
- promptFilePath: options.promptFile,
194
- });
195
-
196
- const exitCodeForOutcome = (outcome: RunOutcome): number => {
197
- if (outcome === "failed") return 1;
198
- if (outcome === "timedOut") return 2;
199
- return 0;
200
- };
201
-
202
- type Resolved = {
203
- readonly sourceRoot: string;
204
- readonly client: PotatoClient;
205
- readonly baseUrl: string;
206
- readonly state: TolokaState | undefined;
207
- readonly stored: StoredCredentials | undefined;
208
- readonly projectId?: string;
209
- };
210
-
211
- const resolveSourceRoot = async (globals: GlobalOptions, io: CliIo): Promise<string> => {
212
- if (globals.sourceRoot !== undefined) return resolve(globals.sourceRoot);
213
-
214
- const workingDirectory = process.cwd();
215
- const discovered = await findSourceRoot(workingDirectory);
216
- if (discovered === undefined || discovered === workingDirectory) return workingDirectory;
217
-
218
- io.stderr(`Using project root ${discovered} (found from ${workingDirectory}).`);
219
- return discovered;
220
- };
221
-
222
- const resolveInitRoot = (globals: GlobalOptions): string =>
223
- globals.sourceRoot === undefined ? process.cwd() : resolve(globals.sourceRoot);
224
-
225
- const readCommandCredentials = async (globals: GlobalOptions, context: ProgramContext) => {
226
- const { found, warning } = await findCommandCredentials(
227
- context.environment,
228
- globals.apiKey ?? context.environment.POTATO_API_KEY,
229
- );
230
- if (warning !== undefined) context.io.stderr(warning);
231
- return found?.credentials;
232
- };
233
-
234
- const identifyClientUser = async (client: PotatoClient, analytics: CliAnalytics | undefined) => {
235
- if (analytics?.shouldIdentify !== true) return;
62
+ if (injectedClient !== undefined) return resolveInternalUser(injectedClient);
236
63
  try {
237
- analytics.identifyCurrentUser(await client.me());
238
- } catch {}
239
- };
240
-
241
- const resolveContext = async (
242
- globals: GlobalOptions,
243
- context: ProgramContext,
244
- ): Promise<Resolved> => {
245
- const sourceRoot = await resolveSourceRoot(globals, context.io);
246
- const state = await readTolokaState(sourceRoot);
247
- const stored = await readCommandCredentials(globals, context);
248
- const config = resolveTolokaConfig(globals, state, context.environment, stored);
249
- const client =
250
- context.injectedClient ?? createClient({ apiKey: config.apiKey, baseUrl: config.baseUrl });
251
- await identifyClientUser(client, context.analytics);
252
-
253
- return {
254
- sourceRoot,
255
- client,
256
- baseUrl: config.baseUrl,
257
- state,
258
- stored,
259
- ...(config.projectId === undefined ? {} : { projectId: config.projectId }),
260
- };
261
- };
262
-
263
- const resolveSelectedProject = (
264
- globals: GlobalOptions,
265
- context: ProgramContext,
266
- stored: StoredCredentials | undefined,
267
- state: TolokaState | undefined,
268
- ) =>
269
- resolveProjectContext({
270
- flags: globals,
271
- environment: context.environment,
272
- stored,
273
- ...(state === undefined ? {} : { state }),
274
- });
275
-
276
- export type CreatedProgram = {
277
- readonly program: Command;
278
- readonly exitCode: { value: number };
64
+ const apiKey = argvFlag(argv, "--api-key");
65
+ const baseUrl = argvFlag(argv, "--base-url");
66
+ const { found } = await findCommandCredentials(
67
+ environment,
68
+ apiKey ?? environment.TOLOKA_API_KEY,
69
+ );
70
+ const config = resolveTolokaConfig(
71
+ {
72
+ ...(apiKey === undefined ? {} : { apiKey }),
73
+ ...(baseUrl === undefined ? {} : { baseUrl }),
74
+ },
75
+ undefined,
76
+ environment,
77
+ found?.credentials,
78
+ );
79
+ return await resolveInternalUser(
80
+ createClient({ apiKey: config.apiKey, baseUrl: config.baseUrl }),
81
+ );
82
+ } catch {
83
+ return undefined;
84
+ }
279
85
  };
280
86
 
281
87
  export function createProgram(context: ProgramContext): CreatedProgram {
282
88
  const program = new Command("toloka");
283
89
  const exitCode = { value: 0 };
284
- const runChecks = context.runChecks ?? checkProject;
285
- const runChecksInterruptibly = async (
286
- options: CheckProjectOptions,
287
- runner: (options: CheckProjectOptions) => Promise<CheckProjectResult> = runChecks,
288
- ): Promise<{ result: CheckProjectResult; interruptedExitCode?: number }> => {
289
- const abortController = new AbortController();
290
- let interruptedExitCode: number | undefined;
291
- const interrupt = (exitCode: number) => {
292
- interruptedExitCode = exitCode;
293
- abortController.abort();
294
- };
295
- const interruptSigint = () => interrupt(130);
296
- const interruptSigterm = () => interrupt(143);
297
- process.on("SIGINT", interruptSigint);
298
- process.on("SIGTERM", interruptSigterm);
299
- try {
300
- const result = await runner({ ...options, signal: abortController.signal });
301
- return { result, ...(interruptedExitCode === undefined ? {} : { interruptedExitCode }) };
302
- } finally {
303
- process.off("SIGINT", interruptSigint);
304
- process.off("SIGTERM", interruptSigterm);
305
- }
306
- };
307
90
  let warnedAboutStaleSkills = false;
308
91
 
309
92
  program
@@ -326,6 +109,7 @@ export function createProgram(context: ProgramContext): CreatedProgram {
326
109
  .exitOverride();
327
110
 
328
111
  const globals = (): GlobalOptions => program.opts<GlobalOptions>();
112
+ const host = { context, exitCode, globals };
329
113
 
330
114
  program.hook("postAction", async (_thisCommand, actionCommand) => {
331
115
  if (warnedAboutStaleSkills) return;
@@ -349,1051 +133,52 @@ export function createProgram(context: ProgramContext): CreatedProgram {
349
133
  context.io.stderr(warning);
350
134
  });
351
135
 
352
- program
353
- .command("version")
354
- .option("--json", "print version and build provenance as JSON", false)
355
- .description("print the current CLI version")
356
- .action((options: { json: boolean }) =>
357
- context.io.stdout(options.json ? JSON.stringify(cliVersionInfo()) : SDK_VERSION),
358
- );
359
-
360
- const config = program.command("config").description("read and write machine CLI settings");
361
- config
362
- .command("get")
363
- .argument("[name]", "setting name; omit to print every setting")
364
- .description("print a CLI setting, or every setting")
365
- .action(async (name?: string) => {
366
- const settings = await readCliSettings(context.environment);
367
- context.io.stdout(getCliSetting(settings, name).output);
368
- });
369
- config
370
- .command("set")
371
- .argument("<name>", "setting name")
372
- .argument("<value>", "setting value")
373
- .description("write a CLI setting for this machine")
374
- .action(async (name: string, value: string) => {
375
- const result = await setCliSetting(context.environment, name, value);
376
- context.io.stdout(result.output);
377
- });
378
- config.action(async () => {
379
- const settings = await readCliSettings(context.environment);
380
- context.io.stdout(getCliSetting(settings, undefined).output);
381
- });
382
-
383
- program
384
- .command("login")
385
- .description("store an API key for this machine after verifying it")
386
- .action(async () => {
387
- const result = await login({
388
- ...(globals().apiKey === undefined ? {} : { apiKey: globals().apiKey }),
389
- ...(globals().baseUrl === undefined ? {} : { baseUrl: globals().baseUrl }),
390
- ...(context.io.prompt === undefined ? {} : { prompt: context.io.prompt }),
391
- write: context.io.stdout,
392
- environment: context.environment,
393
- });
394
- context.io.stdout(result.output);
395
- try {
396
- const stored = await readStoredCredentials(context.environment);
397
- if (stored === undefined) return;
398
- const client = context.injectedClient ?? createClient(stored);
399
- await identifyClientUser(client, context.analytics);
400
- } catch {}
401
- });
402
-
403
- program
404
- .command("logout")
405
- .description("remove stored credentials from this machine")
406
- .action(async () => {
407
- const result = await logout(context.environment);
408
- context.io.stdout(result.output);
409
- });
410
-
411
- program
412
- .command("auth-status")
413
- .alias("whoami")
414
- .description("show which platform and key the CLI would use")
415
- .option("--show-token", "print the API key instead of masking it", false)
416
- .action(async (options: { showToken: boolean }) => {
417
- const flags = globals();
418
- const credentialLookup = await findCommandCredentials(
419
- context.environment,
420
- flags.apiKey ?? context.environment.POTATO_API_KEY,
421
- );
422
- const stored = credentialLookup.found?.credentials;
423
- const sourceRoot = await resolveSourceRoot(flags, context.io);
424
- const state = await readTolokaState(sourceRoot);
425
- const project = resolveSelectedProject(flags, context, stored, state);
426
- const injectedClient = context.injectedClient;
427
-
428
- const result = await authStatus({
429
- environment: context.environment,
430
- showToken: options.showToken,
431
- credentialLookup,
432
- ...(flags.apiKey === undefined ? {} : { flagApiKey: flags.apiKey }),
433
- ...(flags.baseUrl === undefined ? {} : { flagBaseUrl: flags.baseUrl }),
434
- ...(injectedClient === undefined ? {} : { probe: () => injectedClient }),
435
- ...(project === undefined ? {} : { project }),
436
- });
437
-
438
- context.io.stdout(result.output);
439
- if (!result.authenticated) {
440
- exitCode.value = 1;
441
- return;
442
- }
443
- try {
444
- const config = resolveTolokaConfig(flags, state, context.environment, stored);
445
- const client =
446
- context.injectedClient ??
447
- createClient({ apiKey: config.apiKey, baseUrl: config.baseUrl });
448
- await identifyClientUser(client, context.analytics);
449
- } catch {}
450
- });
451
-
452
- const project = program
453
- .command("project")
454
- .description("select and manage the current platform project");
455
- project
456
- .command("list")
457
- .option("--limit <n>", "maximum projects to list", positiveInteger)
458
- .description("list remote projects this key can see")
459
- .action(async (options: { limit?: number }) => {
460
- const flags = globals();
461
- const { client, stored, state } = await resolveContext(flags, context);
462
- const selected = resolveSelectedProject(flags, context, stored, state);
463
- await listRemoteProjects({
464
- client,
465
- io: context.io,
466
- ...(selected === undefined ? {} : { context: selected }),
467
- ...(options.limit === undefined ? {} : { limit: options.limit }),
468
- });
469
- });
470
- project
471
- .command("add")
472
- .argument("[alias]", "local name for this project")
473
- .argument("[projectId]", "platform project id")
474
- .description("register a live project and select it")
475
- .action(async (alias?: string, projectId?: string) => {
476
- const flags = globals();
477
- const found = await requireStoredCredentials(context.environment);
478
- const client = context.injectedClient ?? createClient(found.credentials);
479
- await addProject({
480
- environment: context.environment,
481
- flags,
482
- client,
483
- io: context.io,
484
- afterPreflight: async () => identifyClientUser(client, context.analytics),
485
- ...(alias === undefined ? {} : { alias }),
486
- ...(projectId === undefined ? {} : { projectId }),
487
- });
488
- });
489
-
490
- project
491
- .command("switch")
492
- .argument("<alias>", "local project alias")
493
- .description("select a registered project")
494
- .action(async (alias: string) => {
495
- await switchProject({
496
- environment: context.environment,
497
- flags: globals(),
498
- alias,
499
- io: context.io,
500
- });
501
- });
502
- project
503
- .command("current")
504
- .description("show the project this command would use")
505
- .action(async () => {
506
- const flags = globals();
507
- const stored = await readCommandCredentials(flags, context);
508
- const sourceRoot = await resolveSourceRoot(flags, context.io);
509
- const state = await readTolokaState(sourceRoot);
510
- const result = currentProject({
511
- flags,
512
- environment: context.environment,
513
- stored,
514
- ...(state === undefined ? {} : { state }),
515
- io: context.io,
516
- });
517
- if (result.project === undefined) exitCode.value = 1;
518
- });
519
- project
520
- .command("url")
521
- .description("print the current project URL")
522
- .action(async () => {
523
- const { baseUrl, client, projectId } = await resolveContext(globals(), context);
524
- assert(projectId, MISSING_PROJECT_ID_MESSAGE);
525
- const live = await client.projects.get({ projectId });
526
- const url = new URL(
527
- `/teams/${encodeURIComponent(live.teamId)}/projects/${encodeURIComponent(live.id)}`,
528
- baseUrl,
529
- );
530
- context.io.stdout(url.href);
531
- });
532
-
533
- project
534
- .command("remove")
535
- .argument("<alias>", "local project alias")
536
- .description("forget a local project alias")
537
- .action(async (alias: string) => {
538
- await removeProject({
539
- environment: context.environment,
540
- flags: globals(),
541
- alias,
542
- io: context.io,
543
- });
544
- });
545
- project
546
- .command("untrack")
547
- .argument("<type>", "pipeline or dataset")
548
- .argument("<name>", "tracked resource name")
549
- .option("--confirm", "confirm untracking in a non-interactive environment", false)
550
- .description("remove a pipeline or dataset from local tracking without deleting it")
551
- .action(async (type: string, name: string, options: { confirm: boolean }) => {
552
- assert(
553
- type === "pipeline" || type === "dataset",
554
- "Resource type must be pipeline or dataset.",
555
- );
556
- const sourceRoot = await resolveSourceRoot(globals(), context.io);
557
- await untrackProjectResource({
558
- sourceRoot,
559
- type,
560
- name,
561
- confirm: options.confirm,
562
- io: context.io,
563
- });
564
- });
565
- project
566
- .command("create")
567
- .argument("[alias]", "local name for the new project")
568
- .option("--name <name>", "platform project name")
569
- .option("--team-id <id>", "team that owns the project")
570
- .description("create a live project and select it")
571
- .action(async (alias: string | undefined, options: { name?: string; teamId?: string }) => {
572
- const flags = globals();
573
- const found = await requireStoredCredentials(context.environment);
574
- const client = context.injectedClient ?? createClient(found.credentials);
575
- const sourceRoot = await resolveSourceRoot(flags, context.io);
576
- await createProject({
577
- environment: context.environment,
578
- flags,
579
- client,
580
- io: context.io,
581
- sourceRoot,
582
- afterPreflight: async () => identifyClientUser(client, context.analytics),
583
- ...(alias === undefined ? {} : { alias }),
584
- ...(options.name === undefined ? {} : { name: options.name }),
585
- ...(options.teamId === undefined ? {} : { teamId: options.teamId }),
586
- });
587
- });
588
-
589
- program
590
- .command("init")
591
- .description("initialize this project directory and install the agent bootstrap")
592
- .action(async () => {
593
- const flags = globals();
594
- const sourceRoot = resolveInitRoot(flags);
595
- const state = await readTolokaState(sourceRoot);
596
- const selected = resolveProjectContext({
597
- flags,
598
- environment: context.environment,
599
- ...(state === undefined ? {} : { state }),
600
- });
601
- const result = await initProject(
602
- sourceRoot,
603
- selected === undefined ? {} : { projectId: selected.id },
604
- );
605
- context.io.stdout(result.output);
606
- if (result.warning !== undefined) context.io.stderr(result.warning);
607
- });
608
-
609
- program
610
- .command("import")
611
- .argument(
612
- "[projectId]",
613
- "existing project to write into source; defaults to the selected project",
614
- )
615
- .option("-f, --force", "replace an existing pipelines/ directory", false)
616
- .description("generate TypeScript source from a live project")
617
- .action(async (projectId?: string, options?: { force?: boolean }) => {
618
- const {
619
- sourceRoot,
620
- client,
621
- projectId: selectedProjectId,
622
- } = await resolveContext(globals(), context);
623
- const id = projectId ?? selectedProjectId;
624
- assert(id, MISSING_PROJECT_ID_MESSAGE);
625
- const result = await importProject(client, {
626
- sourceRoot,
627
- projectId: id,
628
- ...(options?.force === true ? { force: true } : {}),
629
- });
630
- context.io.stdout(result.output);
631
- });
632
-
633
- program
634
- .command("plan")
635
- .description("show what apply would change")
636
- .action(async () => {
637
- const { sourceRoot, client, projectId } = await resolveContext(globals(), context);
638
- const result = await planProject(client, {
639
- sourceRoot,
640
- ...(projectId === undefined ? {} : { projectId }),
641
- });
642
- context.io.stdout(result.output);
643
- });
644
-
645
- program
646
- .command("validate")
647
- .description("run platform spec validation and report matching checks")
648
- .action(async () => {
649
- const { sourceRoot, client, projectId } = await resolveContext(globals(), context);
650
- const result = await validateProject(client, {
651
- sourceRoot,
652
- ...(projectId === undefined ? {} : { projectId }),
653
- });
654
- context.io.stdout(result.output);
655
- if (!result.valid) exitCode.value = 1;
656
- });
657
-
658
- program
659
- .command("check")
660
- .argument("[pipeline]", "pipeline source directory or name")
661
- .option("--node <node>", "check one code or human-work node by name or id")
662
- .option("--json", "print the structured check report", false)
663
- .option("--watch", "rerun checks when project files change", false)
664
- .description("run the complete local source, type, and test gate")
665
- .action(
666
- async (
667
- pipeline: string | undefined,
668
- options: { node?: string; json: boolean; watch: boolean },
669
- ) => {
670
- const sourceRoot = await resolveSourceRoot(globals(), context.io);
671
- const checkOptions = {
672
- environment: context.environment,
673
- sourceRoot,
674
- ...(pipeline === undefined ? {} : { pipeline }),
675
- ...(options.node === undefined ? {} : { node: options.node }),
676
- ...(options.json ? { json: true } : {}),
677
- };
678
- const writeCheckOutput = async (output: string): Promise<void> => {
679
- const resultPath = context.environment[CHECK_RESULT_FILE_ENV];
680
- if (resultPath === undefined) context.io.stdout(output);
681
- else await writeFile(resultPath, output, "utf8");
682
- };
683
- const checkRunner =
684
- context.runChecks === undefined &&
685
- context.environment[CHECK_RESULT_FILE_ENV] === undefined
686
- ? runFreshCheck
687
- : runChecks;
688
- if (options.watch) {
689
- const abortController = new AbortController();
690
- const stop = () => abortController.abort();
691
- process.on("SIGINT", stop);
692
- process.on("SIGTERM", stop);
693
- try {
694
- await watchProjectChecks(
695
- checkOptions,
696
- async (result) => {
697
- await writeCheckOutput(result.output);
698
- exitCode.value = result.exitCode;
699
- },
700
- undefined,
701
- abortController.signal,
702
- );
703
- } finally {
704
- process.off("SIGINT", stop);
705
- process.off("SIGTERM", stop);
706
- }
707
- return;
708
- }
709
- const { result, interruptedExitCode } = await runChecksInterruptibly(
710
- checkOptions,
711
- checkRunner,
712
- );
713
- await writeCheckOutput(result.output);
714
- exitCode.value = interruptedExitCode ?? result.exitCode;
715
- },
716
- );
717
-
718
- program
719
- .command("apply")
720
- .description("check source, then make the platform match it")
721
- .action(async () => {
722
- const sourceRoot = await resolveSourceRoot(globals(), context.io);
723
- const { result: checked, interruptedExitCode } = await runChecksInterruptibly({
724
- sourceRoot,
725
- environment: context.environment,
726
- });
727
- context.io.stdout(checked.output);
728
- if (interruptedExitCode !== undefined) {
729
- exitCode.value = interruptedExitCode;
730
- return;
731
- }
732
- if (checked.exitCode !== 0) {
733
- exitCode.value = checked.exitCode;
734
- return;
735
- }
736
- assert(checked.source, "Passing checks must return project source");
737
- const { client, projectId } = await resolveContext(globals(), context);
738
- const result = await applyProject(client, {
739
- sourceRoot,
740
- source: checked.source,
741
- ...(projectId === undefined ? {} : { projectId }),
742
- });
743
- context.io.stdout(result.output);
744
- });
745
-
746
- const secret = program.command("secret").description("manage project secrets");
747
- secret
748
- .command("set")
749
- .argument("<name>", "secret name referenced by a code node")
750
- .option(
751
- "--from-env <variable>",
752
- "read the value from this environment variable instead of stdin",
753
- )
754
- .description("set a secret value without writing it to disk")
755
- .action(async (name: string, options: { fromEnv?: string }) => {
756
- const { client, projectId } = await resolveContext(globals(), context);
757
- assert(projectId, MISSING_PROJECT_ID_MESSAGE);
758
-
759
- const value =
760
- options.fromEnv === undefined
761
- ? (await context.io.readStdin()).replace(/\r?\n$/u, "")
762
- : context.environment[options.fromEnv];
763
- assert(
764
- value !== undefined,
765
- `Missing secret value. Set ${options.fromEnv ?? "the named environment variable"} or pipe it to stdin.`,
766
- );
767
-
768
- try {
769
- const result = await setProjectSecret(client, { projectId, name, value });
770
- context.io.stdout(result.output);
771
- } catch {
772
- context.io.stderr(`Failed to set secret ${name}.`);
773
- exitCode.value = 1;
774
- }
775
- });
776
-
777
- const baseModel = program
778
- .command("base-model")
779
- .description("inspect the global base model catalog");
780
- baseModel
781
- .command("list")
782
- .option("--json", "print JSON", false)
783
- .description("list base models")
784
- .action(async (options: { json: boolean }) => {
785
- const { client } = await resolveContext(globals(), context);
786
- await listBaseModels(client, { json: options.json }, context.io);
787
- });
788
- baseModel
789
- .command("get")
790
- .argument("<id>", "base model ID")
791
- .option("--json", "print JSON", false)
792
- .description("show one base model")
793
- .action(async (id: string, options: { json: boolean }) => {
794
- const { client } = await resolveContext(globals(), context);
795
- await getBaseModel(client, { id: parseBaseModelId(id), json: options.json }, context.io);
796
- });
797
-
798
- const model = program
799
- .command("model")
800
- .description("manage project Models and immutable versions");
801
- model
802
- .command("list")
803
- .option("--json", "print JSON", false)
804
- .description("list Models owned by the selected project")
805
- .action(async (options: { json: boolean }) => {
806
- const { client, projectId } = await resolveContext(globals(), context);
807
- assert(projectId, MISSING_PROJECT_ID_MESSAGE);
808
- await listModels(client, { projectId, json: options.json }, context.io);
809
- });
810
- model
811
- .command("get")
812
- .argument("<name-or-id>", "project Model name or ID")
813
- .option("--json", "print JSON", false)
814
- .description("show one project Model")
815
- .action(async (nameOrId: string, options: { json: boolean }) => {
816
- const { client, projectId } = await resolveContext(globals(), context);
817
- assert(projectId, MISSING_PROJECT_ID_MESSAGE);
818
- await getModel(client, { projectId, nameOrId, json: options.json }, context.io);
819
- });
820
- model
821
- .command("versions")
822
- .argument("<name-or-id>", "project Model name or ID")
823
- .option("--json", "print JSON", false)
824
- .description("list immutable versions of a model")
825
- .action(async (nameOrId: string, options: { json: boolean }) => {
826
- const { client, projectId } = await resolveContext(globals(), context);
827
- assert(projectId, MISSING_PROJECT_ID_MESSAGE);
828
- await listModelVersions(client, { projectId, nameOrId, json: options.json }, context.io);
829
- });
830
- model
831
- .command("create")
832
- .argument("<name>", "new Model name")
833
- .requiredOption("--base <id>", "base model ID")
834
- .option("--description <text>", "Model description")
835
- .option("--json", "print JSON", false)
836
- .description("create project Model metadata without a Model Version")
837
- .action(
838
- async (name: string, options: { base: string; description?: string; json: boolean }) => {
839
- const { client, projectId } = await resolveContext(globals(), context);
840
- assert(projectId, MISSING_PROJECT_ID_MESSAGE);
841
- await createModel(
842
- client,
843
- {
844
- projectId,
845
- name,
846
- description: options.description,
847
- baseModelId: parseBaseModelId(options.base),
848
- json: options.json,
849
- },
850
- context.io,
851
- );
852
- },
853
- );
854
- model
855
- .command("archive")
856
- .option("--json", "print JSON", false)
857
- .argument("<name-or-id>", "project Model name or ID")
858
- .description("archive a project Model")
859
- .action(async (nameOrId: string, options: { json: boolean }) => {
860
- const { client, projectId } = await resolveContext(globals(), context);
861
- assert(projectId, MISSING_PROJECT_ID_MESSAGE);
862
- await archiveModel(client, { projectId, nameOrId, json: options.json }, context.io);
863
- });
864
-
865
- const modelVersion = model.command("version").description("manage one immutable Model Version");
866
- modelVersion
867
- .command("download")
868
- .argument("<model-version>", "Model Version ID or Model@name")
869
- .requiredOption("--out <directory>", "output directory")
870
- .description("download and verify Model Version content")
871
- .action(async (modelVersionId: string, options: { out: string }) => {
872
- const { client, projectId } = await resolveContext(globals(), context);
873
- await downloadModelVersion(
874
- client,
875
- { projectId, modelVersionId, out: options.out },
876
- context.io,
877
- );
878
- });
879
-
880
- const training = program.command("training").description("manage project Training drafts");
881
- training
882
- .command("list")
883
- .option("--json", "print JSON", false)
884
- .description("list active Trainings owned by the selected project")
885
- .action(async (options: { json: boolean }) => {
886
- const { client, projectId } = await resolveContext(globals(), context);
887
- assert(projectId, MISSING_PROJECT_ID_MESSAGE);
888
- await listTrainings(client, { projectId, json: options.json }, context.io);
889
- });
890
- training
891
- .command("get")
892
- .argument("<name-or-id>", "Training name or ID")
893
- .option("--json", "print JSON", false)
894
- .description("show one Training draft")
895
- .action(async (nameOrId: string, options: { json: boolean }) => {
896
- const { client, projectId } = await resolveContext(globals(), context);
897
- assert(projectId, MISSING_PROJECT_ID_MESSAGE);
898
- await getTraining(client, { projectId, nameOrId, json: options.json }, context.io);
899
- });
900
- training
901
- .command("create")
902
- .argument("<name>", "new Training name")
903
- .option("--method <method>", "Training method", trainingMethod, "sft")
904
- .option("--base-model <id>", "catalog Base Model source", parseBaseModelId)
905
- .option("--model-version <id>", "exact Model Version source")
906
- .requiredOption("--dataset <id>", "required training Dataset ID")
907
- .option("--validation-dataset <id>", "optional validation Dataset ID")
908
- .option("--train-filter <field=value>", "training Dataset field equality filter")
909
- .option("--validation-filter <field=value>", "validation Dataset field equality filter")
910
- .option("--regime <regime>", "Python training regime", trainingRegime)
911
- .option("--epochs <count>", "training epochs", nonNegativeInteger)
912
- .option("--eval-steps <count>", "SFT eval steps", positiveInteger)
913
- .option("--prompt-file <path>", "gisting prompt text file")
914
- .option("--json", "print JSON", false)
915
- .description("create a normalized Training draft")
916
- .action(async (name: string, options: RawTrainingDraftOptions & { json: boolean }) => {
917
- const { client, projectId } = await resolveContext(globals(), context);
918
- assert(projectId, MISSING_PROJECT_ID_MESSAGE);
919
- await createTraining(
920
- client,
921
- {
922
- ...trainingDraftOptions(options),
923
- projectId,
924
- name,
925
- json: options.json,
926
- },
927
- context.io,
928
- );
929
- });
930
- training
931
- .command("update")
932
- .argument("<name-or-id>", "Training name or ID")
933
- .option("--base-model <id>", "catalog Base Model source", parseBaseModelId)
934
- .option("--model-version <id>", "exact Model Version source")
935
- .option("--dataset <id>", "required training Dataset ID")
936
- .option("--validation-dataset <id>", "replacement validation Dataset ID")
937
- .option("--train-filter <field=value>", "replacement training Dataset field equality filter")
938
- .option("--validation-filter <field=value>", "replacement validation Dataset field equality filter")
939
- .option("--regime <regime>", "Python training regime", trainingRegime)
940
- .option("--epochs <count>", "training epochs", nonNegativeInteger)
941
- .option("--eval-steps <count>", "SFT eval steps", positiveInteger)
942
- .option("--prompt-file <path>", "replacement gisting prompt text file")
943
- .option("--json", "print JSON", false)
944
- .description("replace the complete Training draft")
945
- .action(async (nameOrId: string, options: RawTrainingDraftOptions & { json: boolean }) => {
946
- const { client, projectId } = await resolveContext(globals(), context);
947
- assert(projectId, MISSING_PROJECT_ID_MESSAGE);
948
- await updateTraining(
949
- client,
950
- {
951
- ...trainingDraftOptions(options),
952
- projectId,
953
- nameOrId,
954
- json: options.json,
955
- },
956
- context.io,
957
- );
958
- });
959
- training
960
- .command("cost")
961
- .argument("<name-or-id>", "Training name or ID")
962
- .option("--idempotency-key <key>", "reuse one Cost Estimate input preparation")
963
- .option("--json", "print JSON", false)
964
- .description("validate Training inputs and calculate the maximum charge")
965
- .action(async (nameOrId: string, options: { idempotencyKey?: string; json: boolean }) => {
966
- const { client, projectId } = await resolveContext(globals(), context);
967
- assert(projectId, MISSING_PROJECT_ID_MESSAGE);
968
- await estimateTrainingCost(
969
- client,
970
- { projectId, nameOrId, idempotencyKey: options.idempotencyKey, json: options.json },
971
- context.io,
972
- );
973
- });
974
- training
975
- .command("run")
976
- .argument("<name-or-id>", "Training name or ID")
977
- .requiredOption("--estimate-file <path>", "ready Training Cost Estimate JSON")
978
- .requiredOption("--version-name <name>", "unique Model Version name for this run")
979
- .option("--idempotency-key <key>", "reuse one Training Run creation")
980
- .option("--json", "print JSON", false)
981
- .description("create one queued Training Run from a ready Cost Estimate")
982
- .action(
983
- async (
984
- nameOrId: string,
985
- options: {
986
- estimateFile: string;
987
- versionName: string;
988
- idempotencyKey?: string;
989
- json: boolean;
990
- },
991
- ) => {
992
- const { client, projectId } = await resolveContext(globals(), context);
993
- assert(projectId, MISSING_PROJECT_ID_MESSAGE);
994
- await createTrainingRun(client, { projectId, nameOrId, ...options }, context.io);
995
- },
996
- );
997
- training
998
- .command("runs")
999
- .argument("<name-or-id>", "Training name or ID")
1000
- .option("--json", "print JSON", false)
1001
- .description("list Training Run history")
1002
- .action(async (nameOrId: string, options: { json: boolean }) => {
1003
- const { client, projectId } = await resolveContext(globals(), context);
1004
- assert(projectId, MISSING_PROJECT_ID_MESSAGE);
1005
- await listTrainingRuns(client, { projectId, nameOrId, json: options.json }, context.io);
1006
- });
1007
- training
1008
- .command("status")
1009
- .argument("<training-run-id>", "Training Run ID")
1010
- .option("--json", "print JSON", false)
1011
- .description("show one Training Run")
1012
- .action(async (trainingRunId: string, options: { json: boolean }) => {
1013
- const { client } = await resolveContext(globals(), context);
1014
- await getTrainingRun(client, { trainingRunId, json: options.json }, context.io);
1015
- });
1016
- training
1017
- .command("resume")
1018
- .argument("<training-run-id>", "Training Run ID")
1019
- .option("--json", "print JSON", false)
1020
- .description("resume a Training Run blocked on funds or payment")
1021
- .action(async (trainingRunId: string, options: { json: boolean }) => {
1022
- const { client } = await resolveContext(globals(), context);
1023
- await resumeTrainingRun(client, { trainingRunId, json: options.json }, context.io);
1024
- });
1025
- training
1026
- .command("cancel")
1027
- .argument("<training-run-id>", "Training Run ID")
1028
- .option("--json", "print JSON", false)
1029
- .description("cancel a Training Run before provider work completes")
1030
- .action(async (trainingRunId: string, options: { json: boolean }) => {
1031
- const { client } = await resolveContext(globals(), context);
1032
- await cancelTrainingRun(client, { trainingRunId, json: options.json }, context.io);
1033
- });
1034
- training
1035
- .command("logs")
1036
- .argument("<training-run-id>", "Training Run ID")
1037
- .option("--follow", "wait for new log entries until complete", false)
1038
- .description("read sanitized Training Run logs")
1039
- .action(async (trainingRunId: string, options: { follow: boolean }) => {
1040
- const { client } = await resolveContext(globals(), context);
1041
- await readTrainingRunLogs(client, { trainingRunId, follow: options.follow }, context.io);
1042
- });
1043
-
1044
- const dataset = program
1045
- .command("dataset")
1046
- .description("inspect and move dataset catalog and rows");
1047
- dataset
1048
- .command("list")
1049
- .description("list live datasets in the selected project")
1050
- .action(async () => {
1051
- const { client, projectId } = await resolveContext(globals(), context);
1052
- assert(projectId, MISSING_PROJECT_ID_MESSAGE);
1053
- const result = await listProjectDatasets(client, { projectId });
1054
- context.io.stdout(result.output);
1055
- });
1056
- dataset
1057
- .command("fields")
1058
- .argument("<name>", "dataset name or id")
1059
- .description("print field declarations inferred from uploaded rows")
1060
- .action(async (name: string) => {
1061
- const { sourceRoot, client, projectId } = await resolveContext(globals(), context);
1062
- const result = await generateDatasetFieldDeclarations(client, {
1063
- sourceRoot,
1064
- datasetName: name,
1065
- ...(projectId === undefined ? {} : { projectId }),
1066
- });
1067
- context.io.stdout(result.output);
1068
- });
1069
- dataset
1070
- .command("load")
1071
- .argument("<name>", "dataset name or id")
1072
- .requiredOption("--out <file>", "JSONL file to write")
1073
- .option("--limit <n>", "how many rows to keep after download", positiveInteger)
1074
- .option("--timeout <ms>", "how long to wait for the export", positiveInteger)
1075
- .description("download live dataset rows into a JSONL file")
1076
- .action(async (name: string, options: { out: string; limit?: number; timeout?: number }) => {
1077
- const { sourceRoot, client, projectId } = await resolveContext(globals(), context);
1078
- const result = await loadDataset(client, {
1079
- sourceRoot,
1080
- datasetName: name,
1081
- outPath: resolve(options.out),
1082
- ...(projectId === undefined ? {} : { projectId }),
1083
- ...(options.limit === undefined ? {} : { limit: options.limit }),
1084
- ...(options.timeout === undefined ? {} : { timeoutMs: options.timeout }),
1085
- });
1086
- context.io.stdout(result.output);
1087
- });
1088
- dataset
1089
- .command("upload")
1090
- .argument("<name>", "dataset name or id")
1091
- .argument("<file>", "JSON array, JSONL, or CSV file")
1092
- .description("append rows to a dataset")
1093
- .action(async (name: string, file: string) => {
1094
- const { sourceRoot, client, projectId } = await resolveContext(globals(), context);
1095
- const result = await uploadDataset(client, {
1096
- sourceRoot,
1097
- datasetName: name,
1098
- filePath: resolve(file),
1099
- ...(projectId === undefined ? {} : { projectId }),
1100
- });
1101
- context.io.stdout(result.output);
1102
- if (result.warning !== undefined) context.io.stderr(result.warning);
1103
- });
1104
-
1105
- const pipelineNameHelp =
1106
- "pipeline name or id; required unless a TTY picker can choose among several";
1107
- const pipeline = program
1108
- .command("pipeline")
1109
- .description("operate on a live pipeline in the selected project");
1110
- pipeline
1111
- .command("list")
1112
- .option("--limit <n>", "maximum pipelines to list", positiveInteger)
1113
- .description("list live pipelines in the selected project")
1114
- .action(async (options: { limit?: number }) => {
1115
- const { client, projectId, state } = await resolveContext(globals(), context);
1116
- assert(projectId, MISSING_PROJECT_ID_MESSAGE);
1117
- const result = await listLivePipelines({
1118
- client,
1119
- projectId,
1120
- state,
1121
- ...(options.limit === undefined ? {} : { limit: options.limit }),
1122
- });
1123
- context.io.stdout(result.output);
1124
- });
1125
- pipeline
1126
- .command("run")
1127
- .argument("[name]", pipelineNameHelp)
1128
- .option("--wait", "wait for the run to reach a terminal state", false)
1129
- .option("--timeout <ms>", "how long to wait", positiveInteger)
1130
- .description("start a run on a deployed pipeline")
1131
- .action(async (name: string | undefined, options: { wait: boolean; timeout?: number }) => {
1132
- const { client, state, projectId } = await resolveContext(globals(), context);
1133
- assert(projectId, MISSING_PROJECT_ID_MESSAGE);
1134
- const result = await startPipelineRun(client, {
1135
- projectId,
1136
- state,
1137
- ...(name === undefined ? {} : { pipeline: name }),
1138
- wait: options.wait,
1139
- ...(options.timeout === undefined ? {} : { timeoutMs: options.timeout }),
1140
- ...(context.io.select === undefined ? {} : { select: context.io.select }),
1141
- });
1142
- context.io.stdout(result.output);
1143
- exitCode.value = exitCodeForOutcome(result.outcome);
1144
- });
1145
- pipeline
1146
- .command("status")
1147
- .argument("[name]", pipelineNameHelp)
1148
- .description("report the latest run's state and item counts")
1149
- .action(async (name: string | undefined) => {
1150
- const { client, state, projectId } = await resolveContext(globals(), context);
1151
- assert(projectId, MISSING_PROJECT_ID_MESSAGE);
1152
- const result = await pipelineRunStatus(client, {
1153
- projectId,
1154
- state,
1155
- ...(name === undefined ? {} : { pipeline: name }),
1156
- ...(context.io.select === undefined ? {} : { select: context.io.select }),
1157
- });
1158
- context.io.stdout(result.output);
1159
- exitCode.value = exitCodeForOutcome(result.outcome);
1160
- });
1161
- pipeline
1162
- .command("runs")
1163
- .argument("[name]", pipelineNameHelp)
1164
- .option("--limit <n>", "how many runs to list", positiveInteger)
1165
- .description("list recent runs")
1166
- .action(async (name: string | undefined, options: { limit?: number }) => {
1167
- const { client, state, projectId } = await resolveContext(globals(), context);
1168
- assert(projectId, MISSING_PROJECT_ID_MESSAGE);
1169
- const result = await listPipelineRuns(client, {
1170
- projectId,
1171
- state,
1172
- ...(name === undefined ? {} : { pipeline: name }),
1173
- ...(options.limit === undefined ? {} : { limit: options.limit }),
1174
- ...(context.io.select === undefined ? {} : { select: context.io.select }),
1175
- });
1176
- context.io.stdout(result.output);
1177
- });
1178
- pipeline
1179
- .command("cost")
1180
- .argument("[name]", pipelineNameHelp)
1181
- .option("--item-limit <n>", "cap how many items to price", positiveInteger)
1182
- .description("forecast what a run of this pipeline would bill")
1183
- .action(async (name: string | undefined, options: { itemLimit?: number }) => {
1184
- const { client, state, projectId } = await resolveContext(globals(), context);
1185
- assert(projectId, MISSING_PROJECT_ID_MESSAGE);
1186
- const result = await estimatePipelineCost(client, {
1187
- projectId,
1188
- state,
1189
- ...(name === undefined ? {} : { pipeline: name }),
1190
- ...(options.itemLimit === undefined ? {} : { itemLimit: options.itemLimit }),
1191
- ...(context.io.select === undefined ? {} : { select: context.io.select }),
1192
- });
1193
- context.io.stdout(result.output);
1194
- });
1195
- pipeline
1196
- .command("cancel")
1197
- .argument("<runId>", "run to cancel")
1198
- .description("request cancellation of a run")
1199
- .action(async (runId: string) => {
1200
- const { client } = await resolveContext(globals(), context);
1201
- const result = await cancelPipelineRun(client, { runId });
1202
- context.io.stdout(result.output);
1203
- });
1204
- pipeline
1205
- .command("results")
1206
- .argument("<runId>", "run to read")
1207
- .option("--limit <n>", "how many items to scan", positiveInteger)
1208
- .option("--failed", "only report failed items", false)
1209
- .description("report a run's items")
1210
- .action(async (runId: string, options: { limit?: number; failed: boolean }) => {
1211
- const { client } = await resolveContext(globals(), context);
1212
- const result = await pipelineRunResults(client, {
1213
- runId,
1214
- failedOnly: options.failed,
1215
- ...(options.limit === undefined ? {} : { limit: options.limit }),
1216
- });
1217
- context.io.stdout(result.output);
1218
- exitCode.value = exitCodeForOutcome(result.outcome);
1219
- });
1220
-
1221
- program
1222
- .command("status")
1223
- .description("report project health: live counts and pending source changes")
1224
- .action(async () => {
1225
- const { sourceRoot, client, projectId } = await resolveContext(globals(), context);
1226
- assert(projectId, MISSING_PROJECT_ID_MESSAGE);
1227
- const result = await projectStatus(client, { sourceRoot, projectId });
1228
- context.io.stdout(result.output);
1229
- });
1230
- program
1231
- .command("report")
1232
- .argument("<markdownPath>", "path to a UTF-8 Markdown report")
1233
- .option("--authored-by <who>", "human or agent", authoredByOption, "human")
1234
- .option("--confirm", "confirm submission in a non-interactive environment", false)
1235
- .description("preview and send an issue report to Toloka through PostHog")
1236
- .action(
1237
- async (
1238
- markdownPath: string,
1239
- options: { authoredBy: IssueReportAuthor; confirm: boolean },
1240
- ) => {
1241
- assert(context.analytics, "Issue reporting requires CLI analytics.");
1242
- const sourceRoot = resolveInitRoot(globals());
1243
- const baseUrl =
1244
- globals().baseUrl ??
1245
- context.environment.POTATO_BASE_URL ??
1246
- resolveInternalBaseUrl(context.environment) ??
1247
- PRODUCTION_BASE_URL;
1248
- exitCode.value = await submitIssueReport({
1249
- path: markdownPath,
1250
- authoredBy: options.authoredBy,
1251
- platform: new URL(baseUrl).origin,
1252
- confirm: options.confirm,
1253
- sourceRoot,
1254
- cwd: process.cwd(),
1255
- analytics: context.analytics,
1256
- io: context.io,
1257
- });
1258
- },
1259
- );
1260
-
1261
- const skill = program
1262
- .command("skill")
1263
- .description("discover bundled agent skills and install them into .agents/skills");
1264
- skill
1265
- .command("list")
1266
- .option("--json", "print Skill Descriptors with their status as JSON", false)
1267
- .description("list bundled skills as not-installed, current, or stale")
1268
- .action(async (options: { json: boolean }) => {
1269
- const sourceRoot = await resolveSourceRoot(globals(), context.io);
1270
- await skillListCommand({ sourceRoot, json: options.json, io: context.io });
1271
- });
1272
- skill
1273
- .command("search")
1274
- .argument("<query>", "text matched case-insensitively against id, name, description, and tags")
1275
- .option("--json", "print matching Skill Descriptors with their status as JSON", false)
1276
- .description("find bundled skills for a task")
1277
- .action(async (query: string, options: { json: boolean }) => {
1278
- const sourceRoot = await resolveSourceRoot(globals(), context.io);
1279
- await skillSearchCommand({ sourceRoot, query, json: options.json, io: context.io });
1280
- });
1281
- skill
1282
- .command("install")
1283
- .argument("<skillId>", "skill id from toloka skill list")
1284
- .option("-f, --force", "replace an existing .agents/skills/<skillId> directory", false)
1285
- .description("copy one bundled skill into .agents/skills/<skillId>")
1286
- .action(async (skillId: string, options: { force: boolean }) => {
1287
- const sourceRoot = await resolveSourceRoot(globals(), context.io);
1288
- await skillInstallCommand({ sourceRoot, skillId, force: options.force, io: context.io });
1289
- });
136
+ registerVersionCommand(program, host);
137
+ registerConfigCommands(program, host);
138
+ registerAuthCommands(program, host);
139
+ registerProjectCommands(program, host);
140
+ if (context.internalUser === true) registerDeliveryOrderCommands(program, host);
141
+ registerSourceCommands(program, host);
142
+ registerSecretCommands(program, host);
143
+ registerModelCommands(program, host);
144
+ registerTrainingCommands(program, host);
145
+ registerDatasetCommands(program, host);
146
+ registerPipelineCommands(program, host);
147
+ registerStatusCommand(program, host);
148
+ registerReportCommand(program, host);
149
+ registerSkillCommands(program, host);
1290
150
 
1291
151
  return { program, exitCode };
1292
152
  }
1293
153
 
1294
- const readStandardInput = async (): Promise<string> => {
1295
- process.stdin.setEncoding("utf8");
1296
- const chunks: string[] = [];
1297
- for await (const chunk of process.stdin) chunks.push(String(chunk));
1298
- return chunks.join("");
1299
- };
1300
-
1301
- const readMaskedLine = async (question: string): Promise<string> => {
1302
- process.stdout.write(question);
1303
- const wasRaw = process.stdin.isRaw;
1304
- process.stdin.setRawMode(true);
1305
- process.stdin.resume();
1306
- process.stdin.setEncoding("utf8");
1307
-
1308
- const { promise, resolve, reject } = Promise.withResolvers<string>();
1309
- let value = "";
1310
-
1311
- const restore = () => {
1312
- process.stdin.off("data", onData);
1313
- process.stdin.off("end", onEnd);
1314
- process.stdin.off("error", onError);
1315
- process.stdin.setRawMode(wasRaw);
1316
- process.stdin.pause();
1317
- };
1318
-
1319
- const finish = () => {
1320
- restore();
1321
- process.stdout.write("\n");
1322
- };
1323
-
1324
- const onEnd = () => {
1325
- finish();
1326
- reject(new Error("Standard input closed before an API key was entered."));
1327
- };
1328
-
1329
- const onError = (error: Error) => {
1330
- finish();
1331
- reject(error);
1332
- };
1333
-
1334
- const onData = (chunk: string) => {
1335
- for (const character of chunk) {
1336
- if (character === "\r" || character === "\n") {
1337
- finish();
1338
- resolve(value);
1339
- return;
1340
- }
1341
- if (character === "\u0003" || character === "\u0004") {
1342
- finish();
1343
- reject(
1344
- new Error(
1345
- character === "\u0003"
1346
- ? "Canceled."
1347
- : "Standard input closed before an API key was entered.",
1348
- ),
1349
- );
1350
- return;
1351
- }
1352
- if (character === "\u007f" || character === "\b") {
1353
- if (value.length > 0) {
1354
- value = value.slice(0, -1);
1355
- process.stdout.write("\b \b");
1356
- }
1357
- continue;
1358
- }
1359
- value += character;
1360
- process.stdout.write("*");
1361
- }
1362
- };
1363
-
1364
- process.stdin.on("data", onData);
1365
- process.stdin.once("end", onEnd);
1366
- process.stdin.once("error", onError);
1367
-
1368
- try {
1369
- return await promise;
1370
- } finally {
1371
- restore();
1372
- }
154
+ type CommandResult = {
155
+ readonly code: number;
156
+ readonly outcome: CliAnalyticsCommandOutcome;
157
+ readonly error?: unknown;
1373
158
  };
1374
159
 
1375
- const promptFromTerminal: AuthPrompt = async (question, options = {}) => {
1376
- if (options.mask === true) return readMaskedLine(question);
1377
-
1378
- const input = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
1379
- try {
1380
- return await input.question(question);
1381
- } finally {
1382
- input.close();
160
+ const firstOperand = (program: Command, argv: readonly string[]): string | undefined => {
161
+ for (let index = 0; index < argv.length; index += 1) {
162
+ const token = argv[index];
163
+ if (token === undefined) return undefined;
164
+ if (token === "--") return argv[index + 1];
165
+ if (!token.startsWith("-")) return token;
166
+ const option = program.options.find(
167
+ (candidate) => candidate.short === token || candidate.long === token,
168
+ );
169
+ if (option !== undefined && (option.required || option.optional) && !token.includes("="))
170
+ index += 1;
1383
171
  }
172
+ return undefined;
1384
173
  };
1385
174
 
1386
- export const terminalIo = (): CliIo => ({
1387
- stdout: (text) => console.log(text),
1388
- stderr: (text) => console.error(text),
1389
- readStdin: readStandardInput,
1390
- ...(process.stdin.isTTY ? { prompt: promptFromTerminal, select: pickPipeline } : {}),
1391
- });
1392
-
1393
- type CommandResult = {
1394
- readonly code: number;
1395
- readonly outcome: CliAnalyticsCommandOutcome;
1396
- readonly error?: unknown;
175
+ const unknownCommand = (program: Command, argv: readonly string[]): string | undefined => {
176
+ const name = firstOperand(program, argv);
177
+ if (name === undefined || name === "help") return undefined;
178
+ const known = program.commands.some(
179
+ (command) => command.name() === name || command.aliases().includes(name),
180
+ );
181
+ return known ? undefined : name;
1397
182
  };
1398
183
 
1399
184
  const runProgram = async (
@@ -1402,6 +187,13 @@ const runProgram = async (
1402
187
  argv: readonly string[],
1403
188
  io: CliIo,
1404
189
  ): Promise<CommandResult> => {
190
+ const unknown = unknownCommand(program, argv);
191
+ if (unknown !== undefined) {
192
+ io.stderr(
193
+ `error: unknown command '${unknown}'\ntoloka ${SDK_VERSION} does not have it. Run toloka --help to list its commands; a runbook that uses it may need a newer release.`,
194
+ );
195
+ return { code: 1, outcome: "usage_error" };
196
+ }
1405
197
  try {
1406
198
  await program.parseAsync([...argv], { from: "user" });
1407
199
  const code = exitCode.value;
@@ -1416,15 +208,42 @@ const runProgram = async (
1416
208
  }
1417
209
  };
1418
210
 
211
+ const finalizeAnalytics = async ({
212
+ analytics,
213
+ argv,
214
+ startedAt,
215
+ result,
216
+ }: {
217
+ analytics: CliAnalytics | undefined;
218
+ argv: readonly string[];
219
+ startedAt: number;
220
+ result: CommandResult;
221
+ }): Promise<void> => {
222
+ if (analytics === undefined) return;
223
+
224
+ try {
225
+ analytics.captureCommand({
226
+ command: commandNameFromArgv(argv),
227
+ durationMs: Date.now() - startedAt,
228
+ exitCode: result.code,
229
+ outcome: result.outcome,
230
+ ...(result.error === undefined ? {} : { error: result.error }),
231
+ });
232
+ await analytics.shutdown();
233
+ } catch {}
234
+ };
235
+
1419
236
  export async function runTolokaCli(
1420
237
  argv: readonly string[],
1421
- environment: TolokaEnvironment = process.env,
238
+ rawEnvironment: TolokaEnvironment = process.env,
1422
239
  io: CliIo = terminalIo(),
1423
240
  injectedClient?: PotatoClient,
1424
241
  analytics?: CliAnalytics,
1425
242
  runChecks?: typeof checkProject,
1426
243
  autoUpdate?: AutoUpdateHooks,
1427
244
  ): Promise<number> {
245
+ const { environment, warnings } = normalizeEnvironment(rawEnvironment);
246
+ for (const warning of warnings) io.stderr(warning);
1428
247
  const autoUpdateResult = await maybeAutoUpdate({
1429
248
  argv,
1430
249
  environment,
@@ -1437,28 +256,33 @@ export async function runTolokaCli(
1437
256
  environment[CHECK_RESULT_FILE_ENV] === undefined
1438
257
  ? (analytics ?? createCliAnalytics({ environment }))
1439
258
  : undefined;
1440
- const { program, exitCode } = createProgram({
259
+ const baseContext: ProgramContext = {
1441
260
  environment,
1442
261
  io,
1443
262
  ...(activeAnalytics === undefined ? {} : { analytics: activeAnalytics }),
1444
263
  ...(injectedClient === undefined ? {} : { injectedClient }),
1445
264
  ...(runChecks === undefined ? {} : { runChecks }),
265
+ };
266
+ const internalUser = await resolveInternalCommandAccess(argv, environment, injectedClient);
267
+ const { program, exitCode } = createProgram({
268
+ ...baseContext,
269
+ ...(internalUser === undefined ? {} : { internalUser }),
1446
270
  });
1447
-
1448
271
  const startedAt = Date.now();
1449
- const result = await runProgram(program, exitCode, argv, io);
1450
- if (activeAnalytics !== undefined) {
1451
- try {
1452
- activeAnalytics.captureCommand({
1453
- command: commandNameFromArgv(argv),
1454
- durationMs: Date.now() - startedAt,
1455
- exitCode: result.code,
1456
- outcome: result.outcome,
1457
- ...(result.error === undefined ? {} : { error: result.error }),
1458
- });
1459
- await activeAnalytics.shutdown();
1460
- } catch {}
1461
- }
1462
272
 
1463
- return result.code;
273
+ try {
274
+ const result = await runProgram(program, exitCode, argv, io);
275
+ await finalizeAnalytics({ analytics: activeAnalytics, argv, startedAt, result });
276
+
277
+ return result.code;
278
+ } catch (error) {
279
+ await finalizeAnalytics({
280
+ analytics: activeAnalytics,
281
+ argv,
282
+ startedAt,
283
+ result: { code: 1, outcome: "failed", error },
284
+ });
285
+
286
+ throw error;
287
+ }
1464
288
  }