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

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 (110) hide show
  1. package/README.md +10 -4
  2. package/dist/cli/toloka.js +162 -154
  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 +6 -4
  8. package/docs/cli.md +58 -11
  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/baseModel.ts +1 -0
  16. package/openapi/gen/models/checkReport.ts +2 -1
  17. package/openapi/gen/models/checkReportAnyOfTwoseven.ts +5 -0
  18. package/openapi/gen/models/checkReportAnyOfTwosevenResultsItem.ts +6 -0
  19. package/openapi/gen/models/checkReportAnyOfTwosevenResultsItemVerdict.ts +2 -0
  20. package/openapi/gen/models/currentUser.ts +3 -0
  21. package/openapi/gen/models/currentUserTeamsItem.ts +6 -0
  22. package/openapi/gen/models/deliveryOrder.ts +9 -0
  23. package/openapi/gen/models/deliveryOrderClientName.ts +2 -0
  24. package/openapi/gen/models/getTrainingRunMetricHistoryParams.ts +4 -0
  25. package/openapi/gen/models/gistingTrainingConfiguration.ts +6 -2
  26. package/openapi/gen/models/gistingTrainingConfigurationBatchSize.ts +2 -0
  27. package/openapi/gen/models/gistingTrainingConfigurationGistTokens.ts +2 -0
  28. package/openapi/gen/models/gistingTrainingConfigurationInput.ts +6 -2
  29. package/openapi/gen/models/gistingTrainingConfigurationInputBatchSize.ts +2 -0
  30. package/openapi/gen/models/gistingTrainingConfigurationInputGistTokens.ts +2 -0
  31. package/openapi/gen/models/index.ts +28 -4
  32. package/openapi/gen/models/listDeliveryOrders200.ts +5 -0
  33. package/openapi/gen/models/sftTrainingConfiguration.ts +6 -3
  34. package/openapi/gen/models/sftTrainingConfigurationBatchSize.ts +2 -0
  35. package/openapi/gen/models/sftTrainingConfigurationInput.ts +6 -3
  36. package/openapi/gen/models/sftTrainingConfigurationInputBatchSize.ts +2 -0
  37. package/openapi/gen/models/sftTrainingConfigurationInputLoraRank.ts +2 -0
  38. package/openapi/gen/models/sftTrainingConfigurationLoraRank.ts +2 -0
  39. package/openapi/gen/models/trainingRunMetricHistory.ts +9 -0
  40. package/openapi/gen/models/trainingRunMetricHistoryMetric.ts +2 -0
  41. package/openapi/gen/models/trainingRunMetricHistoryPointsItem.ts +9 -0
  42. package/openapi/gen/models/trainingRunMetricHistoryPointsItemNonFinite.ts +2 -0
  43. package/openapi/gen/models/trainingRunMetricHistoryPointsItemValue.ts +2 -0
  44. package/openapi/gen/models/trainingRunMetrics.ts +9 -0
  45. package/openapi/gen/models/trainingRunMetricsMetricsItem.ts +8 -0
  46. package/openapi/gen/models/trainingRunMetricsMetricsItemLatest.ts +9 -0
  47. package/openapi/gen/models/trainingRunMetricsMetricsItemLatestNonFinite.ts +2 -0
  48. package/openapi/gen/models/trainingRunMetricsMetricsItemLatestValue.ts +2 -0
  49. package/openapi/gen/models/trainingRunMetricsMetricsItemMetric.ts +2 -0
  50. package/openapi/gen/models/trainingRunMetricsUnavailableReason.ts +2 -0
  51. package/package.json +2 -2
  52. package/skills/catalog.json +55 -7
  53. package/skills/toloka-build/SKILL.md +35 -21
  54. package/skills/toloka-build/references/node-semantics.md +13 -11
  55. package/skills/toloka-build/references/workflow-and-cli.md +31 -20
  56. package/skills/toloka-fine-tuning/SKILL.md +155 -0
  57. package/skills/toloka-get-started/SKILL.md +70 -0
  58. package/skills/toloka-gisting/SKILL.md +166 -0
  59. package/skills/toloka-report-issue/references/report-template.md +1 -1
  60. package/src/authoring/index.ts +14 -0
  61. package/src/checks/task-ui-typecheck.ts +12 -10
  62. package/src/cli/analytics.ts +88 -15
  63. package/src/cli/auth.ts +93 -21
  64. package/src/cli/auto-update.ts +36 -39
  65. package/src/cli/check.ts +88 -39
  66. package/src/cli/cli-entry.ts +3 -2
  67. package/src/cli/cli-error.ts +43 -0
  68. package/src/cli/commands/auth.ts +89 -0
  69. package/src/cli/commands/config.ts +28 -0
  70. package/src/cli/commands/dataset.ts +85 -0
  71. package/src/cli/commands/delivery-order.ts +18 -0
  72. package/src/cli/commands/models.ts +121 -0
  73. package/src/cli/commands/pipeline.ts +131 -0
  74. package/src/cli/commands/project.ts +168 -0
  75. package/src/cli/commands/report.ts +43 -0
  76. package/src/cli/commands/secret.ts +40 -0
  77. package/src/cli/commands/skill.ts +36 -0
  78. package/src/cli/commands/source.ts +204 -0
  79. package/src/cli/commands/status.ts +17 -0
  80. package/src/cli/commands/training.ts +261 -0
  81. package/src/cli/commands/version.ts +14 -0
  82. package/src/cli/config.ts +7 -8
  83. package/src/cli/context.ts +251 -0
  84. package/src/cli/credentials.ts +12 -4
  85. package/src/cli/data.ts +96 -34
  86. package/src/cli/delivery-orders.ts +47 -0
  87. package/src/cli/environment.ts +26 -0
  88. package/src/cli/file-system.ts +6 -2
  89. package/src/cli/format-error.ts +45 -2
  90. package/src/cli/init.ts +14 -11
  91. package/src/cli/models.ts +43 -12
  92. package/src/cli/program.ts +120 -1347
  93. package/src/cli/project-context.ts +126 -43
  94. package/src/cli/project.ts +80 -28
  95. package/src/cli/report.ts +24 -9
  96. package/src/cli/runs.ts +33 -8
  97. package/src/cli/sdk-version.ts +38 -0
  98. package/src/cli/settings.ts +13 -5
  99. package/src/cli/skill.ts +67 -24
  100. package/src/cli/source.ts +3 -2
  101. package/src/cli/terminal-io.ts +110 -0
  102. package/src/cli/trainings.ts +242 -57
  103. package/src/cli/version.ts +3 -3
  104. package/src/client.ts +21 -9
  105. package/src/importer.ts +32 -2
  106. package/src/transport.ts +13 -1
  107. package/openapi/gen/models/gistingTrainingConfigurationInputRegime.ts +0 -2
  108. package/openapi/gen/models/gistingTrainingConfigurationRegime.ts +0 -2
  109. package/openapi/gen/models/sftTrainingConfigurationInputRegime.ts +0 -2
  110. package/openapi/gen/models/sftTrainingConfigurationRegime.ts +0 -2
@@ -265,6 +265,16 @@ const mapParseErrorsToBlocks = (
265
265
  return report;
266
266
  };
267
267
 
268
+ const runtimeSyntaxReport = (source: string): TaskUiDiagnosticsReport => {
269
+ const jsxReport = mapParseErrorsToBlocks(source, parseSync("runtime.jsx", source).errors);
270
+ const tsxReport = mapParseErrorsToBlocks(source, parseSync("runtime.tsx", source).errors);
271
+ return {
272
+ ...tsxReport,
273
+ preprocess: jsxReport.preprocess,
274
+ postprocess: jsxReport.postprocess,
275
+ };
276
+ };
277
+
268
278
  export const createTaskUiTypechecker = ({
269
279
  reactDeclaration,
270
280
  elementsDeclaration,
@@ -311,11 +321,7 @@ export const createTaskUiTypechecker = ({
311
321
  ...syntacticDiagnostics,
312
322
  ...otherDiagnostics,
313
323
  ]);
314
- const runtimeSource = buildRuntimeSource(input);
315
- const runtimeReport = mapParseErrorsToBlocks(
316
- runtimeSource,
317
- parseSync("runtime.jsx", runtimeSource).errors,
318
- );
324
+ const runtimeReport = runtimeSyntaxReport(buildRuntimeSource(input));
319
325
  return mergeRuntimeDiagnostics(typeScriptReport, syntacticReport, runtimeReport);
320
326
  },
321
327
 
@@ -325,11 +331,7 @@ export const createTaskUiTypechecker = ({
325
331
  typeScriptSource,
326
332
  parseSync(FILE_NAME, typeScriptSource).errors,
327
333
  );
328
- const runtimeSource = buildRuntimeSource(input);
329
- const runtimeReport = mapParseErrorsToBlocks(
330
- runtimeSource,
331
- parseSync("runtime.jsx", runtimeSource).errors,
332
- );
334
+ const runtimeReport = runtimeSyntaxReport(buildRuntimeSource(input));
333
335
  return mergeRuntimeDiagnostics(typeScriptReport, typeScriptReport, runtimeReport);
334
336
  },
335
337
  });
@@ -1,14 +1,15 @@
1
- import { assert } from "es-toolkit";
2
1
  import { randomUUID } from "node:crypto";
3
2
  import { chmod, readFile, writeFile } from "node:fs/promises";
4
3
  import { dirname, join } from "node:path";
5
4
  import { PostHog } from "posthog-node";
6
- import { PipelineGraphValidationError, PotatoApiError } from "../errors";
5
+ import { PipelineGraphValidationError } from "../errors";
7
6
  import type { TolokaEnvironment } from "./config";
8
7
  import type { CliEnvironment } from "./credentials";
9
8
  import { credentialsPath, preparePrivateFilePath } from "./credentials";
10
9
  import { isFalseyFlag, isTruthyFlag } from "./boolean-flag";
11
- import { SDK_VERSION } from "./init";
10
+ import { SDK_VERSION } from "./sdk-version";
11
+ import { assertCli, TolokaCliError } from "./cli-error";
12
+ import { findPotatoApiError } from "./format-error";
12
13
 
13
14
  export const POSTHOG_PROJECT_KEY = "phc_kqHt0f53B5MVqURyzWpW1aNlbOmYt1YbUYza0JPcg19";
14
15
  export const POSTHOG_HOST = "https://eu.posthog.com";
@@ -56,6 +57,11 @@ export type PosthogLike = {
56
57
  properties?: Record<string, unknown>;
57
58
  disableGeoip: boolean;
58
59
  }) => void;
60
+ captureException: (
61
+ error: unknown,
62
+ distinctId: string,
63
+ properties?: Record<string, unknown>,
64
+ ) => void;
59
65
  alias: (event: { distinctId: string; alias: string }) => void;
60
66
  flush: () => Promise<void>;
61
67
  on?: (event: "error", listener: (error: Error) => void) => void;
@@ -72,7 +78,9 @@ export function telemetryEnabled(environment: CliEnvironment): boolean {
72
78
  return true;
73
79
  }
74
80
 
75
- const COMMAND_GROUPS: Readonly<Record<string, Readonly<Record<string, true>>>> = {
81
+ type CommandGroup = Readonly<Record<string, true | Readonly<Record<string, true>>>>;
82
+
83
+ const COMMAND_GROUPS: Readonly<Record<string, CommandGroup>> = {
76
84
  project: { list: true, add: true, switch: true, current: true, remove: true, create: true },
77
85
  pipeline: {
78
86
  list: true,
@@ -83,7 +91,31 @@ const COMMAND_GROUPS: Readonly<Record<string, Readonly<Record<string, true>>>> =
83
91
  cancel: true,
84
92
  results: true,
85
93
  },
86
- dataset: { list: true, fields: true, load: true, upload: true },
94
+ dataset: { list: true, create: true, fields: true, load: true, upload: true },
95
+ "delivery-order": { list: true },
96
+ training: {
97
+ list: true,
98
+ get: true,
99
+ create: true,
100
+ update: true,
101
+ cost: true,
102
+ run: true,
103
+ runs: true,
104
+ status: true,
105
+ resume: true,
106
+ cancel: true,
107
+ metrics: true,
108
+ logs: true,
109
+ },
110
+ "base-model": { list: true, get: true },
111
+ model: {
112
+ list: true,
113
+ get: true,
114
+ versions: true,
115
+ create: true,
116
+ archive: true,
117
+ version: { download: true },
118
+ },
87
119
  secret: { set: true },
88
120
  skill: { list: true, search: true, install: true },
89
121
  config: { get: true, set: true },
@@ -115,9 +147,13 @@ const nameFromTokens = (tokens: readonly string[]): string => {
115
147
  if (subcommands === undefined) return ROOT_COMMANDS[command] === true ? command : "unknown";
116
148
 
117
149
  const subcommand = tokens[1];
118
- return subcommand !== undefined && subcommands[subcommand] === true
119
- ? `${command} ${subcommand}`
120
- : command;
150
+ const nested = subcommand === undefined ? undefined : subcommands[subcommand];
151
+ if (nested === undefined) return command;
152
+ if (nested === true) return `${command} ${subcommand}`;
153
+ const leaf = tokens[2];
154
+ return leaf !== undefined && nested[leaf] === true
155
+ ? `${command} ${subcommand} ${leaf}`
156
+ : `${command} ${subcommand}`;
121
157
  };
122
158
 
123
159
  export function commandNameFromArgv(argv: readonly string[]): string {
@@ -138,12 +174,19 @@ export function commandNameFromArgv(argv: readonly string[]): string {
138
174
 
139
175
  export function classifyCliError(error: unknown): { readonly error_code: string } {
140
176
  if (error === undefined) return { error_code: "command_failed" };
141
- if (error instanceof PotatoApiError || error instanceof PipelineGraphValidationError) {
142
- return { error_code: error.code };
143
- }
177
+ const apiError = findPotatoApiError(error);
178
+ if (apiError !== undefined) return { error_code: apiError.code };
179
+ if (error instanceof PipelineGraphValidationError) return { error_code: error.code };
144
180
  return { error_code: error instanceof Error ? "error" : "unknown" };
145
181
  }
146
182
 
183
+ const cliErrorProperties = (error: unknown): Record<string, unknown> => {
184
+ const apiError = findPotatoApiError(error);
185
+ return apiError === undefined
186
+ ? {}
187
+ : { status_code: apiError.status, request_id: apiError.requestId };
188
+ };
189
+
147
190
  const anonymousIdPath = (environment: CliEnvironment): string =>
148
191
  join(dirname(credentialsPath(environment)), "anonymous-id");
149
192
 
@@ -171,7 +214,11 @@ const noopAnalytics = (): CliAnalytics => ({
171
214
  identifyCurrentUser() {},
172
215
  captureCommand() {},
173
216
  async submitIssueReport() {
174
- assert(false, "Issue reporting is disabled. Re-run with TOLOKA_TELEMETRY=1.");
217
+ assertCli(
218
+ false,
219
+ "Issue reporting is disabled. Re-run with TOLOKA_TELEMETRY=1.",
220
+ { hint: { disabled: true } },
221
+ );
175
222
  },
176
223
  async shutdown() {},
177
224
  });
@@ -237,18 +284,40 @@ export function createCliAnalytics(options: {
237
284
  .then(() => {
238
285
  if (client === undefined || distinctId === undefined) return;
239
286
  const shared = sharedProperties();
287
+ const command = nameFromTokens(event.command.split(" "));
288
+
240
289
  if (event.outcome === "failed") {
241
290
  const classified = classifyCliError(event.error);
291
+ const errorProperties = cliErrorProperties(event.error);
292
+
293
+ if (event.error !== undefined) {
294
+ const exception = new TolokaCliError(`CLI command failed: ${classified.error_code}`, {
295
+ hint: { disabled: true },
296
+ });
297
+ exception.name = "PotatoCliError";
298
+ client.captureException(exception, distinctId, {
299
+ ...shared,
300
+ command,
301
+ duration_ms: event.durationMs,
302
+ exit_code: event.exitCode,
303
+ $process_person_profile: false,
304
+ $geoip_disable: true,
305
+ ...classified,
306
+ ...errorProperties,
307
+ });
308
+ }
309
+
242
310
  client.capture({
243
311
  distinctId,
244
312
  event: "sdk_cli_command_failed",
245
313
  disableGeoip: true,
246
314
  properties: {
247
315
  ...shared,
248
- command: nameFromTokens(event.command.split(" ")),
316
+ command,
249
317
  duration_ms: event.durationMs,
250
318
  exit_code: event.exitCode,
251
319
  ...classified,
320
+ ...errorProperties,
252
321
  },
253
322
  });
254
323
  return;
@@ -259,7 +328,7 @@ export function createCliAnalytics(options: {
259
328
  disableGeoip: true,
260
329
  properties: {
261
330
  ...shared,
262
- command: nameFromTokens(event.command.split(" ")),
331
+ command,
263
332
  duration_ms: event.durationMs,
264
333
  exit_code: event.exitCode,
265
334
  outcome: event.outcome,
@@ -270,7 +339,11 @@ export function createCliAnalytics(options: {
270
339
  },
271
340
  submitIssueReport(report) {
272
341
  const submission = queue.then(async () => {
273
- assert(client && distinctId, "PostHog issue reporting could not be initialized.");
342
+ assertCli(
343
+ client && distinctId,
344
+ "PostHog issue reporting could not be initialized.",
345
+ { hint: { description: "Set TOLOKA_TELEMETRY=1 and check that this machine can reach PostHog, then retry." } },
346
+ );
274
347
  deliveryErrors.length = 0;
275
348
  client.capture({
276
349
  distinctId,
package/src/cli/auth.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { assert } from "es-toolkit";
2
1
  import { createClient } from "../client";
2
+ import { PotatoApiError } from "../errors";
3
3
  import { PRODUCTION_BASE_URL, consoleUrlFor, resolveInternalBaseUrl } from "./config";
4
4
  import {
5
5
  clearStoredCredentials,
@@ -18,6 +18,41 @@ import type {
18
18
  } from "./credentials";
19
19
  import type { ProjectContext } from "./project-context";
20
20
  import { formatProjectContextLine } from "./project-context";
21
+ import { assertCli, TolokaCliError } from "./cli-error";
22
+
23
+ type ProbeFailureKind = "rejected" | "unreachable" | "platform";
24
+
25
+ type ProbeFailure = { readonly kind: ProbeFailureKind; readonly reason: string };
26
+
27
+ const classifyProbeFailure = (error: unknown): ProbeFailure => {
28
+ const reason = error instanceof Error ? error.message : String(error);
29
+ if (!(error instanceof PotatoApiError) || error.status === undefined)
30
+ return { kind: "unreachable", reason };
31
+ return {
32
+ kind: error.status === 401 || error.status === 403 ? "rejected" : "platform",
33
+ reason,
34
+ };
35
+ };
36
+
37
+ const PROBE_FAILURE_HEADLINE: Record<ProbeFailureKind, string> = {
38
+ rejected: "Rejected",
39
+ unreachable: "Not reachable",
40
+ platform: "Platform error",
41
+ };
42
+
43
+ const LOGIN_FAILURE_ADVICE: Record<ProbeFailureKind, (baseUrl: string) => string> = {
44
+ rejected: () => "Check that the key was copied whole and has not been revoked.",
45
+ unreachable: (baseUrl) =>
46
+ `${baseUrl} did not answer, so the key was not evaluated. Check network access, VPN, or proxy settings and retry.`,
47
+ platform: () => "The platform failed before confirming the key. Retry later.",
48
+ };
49
+
50
+ const STATUS_FAILURE_ADVICE: Record<ProbeFailureKind, (baseUrl: string) => string> = {
51
+ rejected: () => "Run toloka login to store a working key.",
52
+ unreachable: (baseUrl) =>
53
+ `Check network access to ${baseUrl}; the stored key was not evaluated.`,
54
+ platform: () => "The platform failed before confirming the key. Retry later.",
55
+ };
21
56
 
22
57
  export const apiKeyInstructions = (baseUrl: string): string => {
23
58
  const console = consoleUrlFor(baseUrl);
@@ -54,6 +89,14 @@ export type AuthProbe = {
54
89
  readonly pagination: { readonly total: number };
55
90
  }>;
56
91
  };
92
+ readonly me?: () => Promise<{
93
+ readonly internal: boolean;
94
+ readonly teams?: ReadonlyArray<{
95
+ readonly id: string;
96
+ readonly writable: boolean;
97
+ readonly internal?: boolean;
98
+ }>;
99
+ }>;
57
100
  };
58
101
 
59
102
  export type LoginOptions = {
@@ -76,21 +119,26 @@ export async function login(options: LoginOptions = {}): Promise<LoginResult> {
76
119
  const write = options.write ?? (() => undefined);
77
120
  const baseUrl = normalizeBaseUrl(
78
121
  options.baseUrl ??
79
- environment.POTATO_BASE_URL ??
122
+ environment.TOLOKA_BASE_URL ??
80
123
  resolveInternalBaseUrl(environment) ??
81
124
  PRODUCTION_BASE_URL,
82
125
  );
83
126
 
84
- let apiKey = options.apiKey ?? environment.POTATO_API_KEY;
127
+ let apiKey = options.apiKey ?? environment.TOLOKA_API_KEY;
85
128
  if (apiKey === undefined) {
86
- assert(
129
+ assertCli(
87
130
  options.prompt,
88
- "Missing API key. Pass --api-key, or set POTATO_API_KEY, when stdin is not a terminal.",
131
+ "Missing API key. Without a terminal, pipe the key to toloka login --api-key-stdin, pass --api-key, or set TOLOKA_API_KEY.",
132
+ { hint: { disabled: true } },
89
133
  );
90
134
  write(`\n${apiKeyInstructions(baseUrl)}\n`);
91
135
  apiKey = (await options.prompt("API key: ", { mask: true })).trim();
92
136
  }
93
- assert(apiKey.length > 0, "No API key entered.");
137
+ assertCli(
138
+ apiKey.length > 0,
139
+ "No API key entered.",
140
+ { hint: { description: "Re-run toloka login and paste the key at the prompt, or pass --api-key." } },
141
+ );
94
142
  const credentials: StoredCredentials = { baseUrl, apiKey };
95
143
  const probe: AuthProbe = options.probe?.(credentials) ?? createClient(credentials);
96
144
 
@@ -99,14 +147,14 @@ export async function login(options: LoginOptions = {}): Promise<LoginResult> {
99
147
  const page = await probe.projects.listPage({ limit: 1 });
100
148
  reachable = `Authenticated against ${baseUrl}: ${page.pagination.total} project(s) visible.`;
101
149
  } catch (error) {
102
- const reason = error instanceof Error ? error.message : String(error);
103
- throw new Error(
150
+ const failure = classifyProbeFailure(error);
151
+ throw new TolokaCliError(
104
152
  [
105
- `Could not authenticate against ${baseUrl}: ${reason}`,
153
+ `Could not authenticate against ${baseUrl}: ${failure.reason}`,
106
154
  "",
107
- "Nothing was saved. Check that the key was copied whole and has not been revoked.",
155
+ `Nothing was saved. ${LOGIN_FAILURE_ADVICE[failure.kind](baseUrl)}`,
108
156
  ].join("\n"),
109
- { cause: error },
157
+ { cause: error, hint: { disabled: true } },
110
158
  );
111
159
  }
112
160
 
@@ -136,7 +184,7 @@ export async function login(options: LoginOptions = {}): Promise<LoginResult> {
136
184
  reachable,
137
185
  `Saved credentials to ${credentialsFile}.`,
138
186
  "",
139
- "toloka reads them automatically. --api-key and POTATO_API_KEY still win when set.",
187
+ "toloka reads them automatically. --api-key and TOLOKA_API_KEY still win when set.",
140
188
  ].join("\n"),
141
189
  };
142
190
  }
@@ -183,9 +231,9 @@ const resolveApiKey = (
183
231
  options.flagApiKey === undefined
184
232
  ? undefined
185
233
  : { value: options.flagApiKey, source: "from --api-key" },
186
- environment.POTATO_API_KEY === undefined
234
+ environment.TOLOKA_API_KEY === undefined
187
235
  ? undefined
188
- : { value: environment.POTATO_API_KEY, source: "from POTATO_API_KEY" },
236
+ : { value: environment.TOLOKA_API_KEY, source: "from TOLOKA_API_KEY" },
189
237
  found === undefined
190
238
  ? undefined
191
239
  : { value: found.credentials.apiKey, source: `stored in ${storedPath}` },
@@ -202,9 +250,9 @@ const resolvePlatform = (
202
250
  options.flagBaseUrl === undefined
203
251
  ? undefined
204
252
  : { value: options.flagBaseUrl.replace(/\/+$/u, ""), source: "from --base-url" },
205
- environment.POTATO_BASE_URL === undefined
253
+ environment.TOLOKA_BASE_URL === undefined
206
254
  ? undefined
207
- : { value: environment.POTATO_BASE_URL, source: "from POTATO_BASE_URL" },
255
+ : { value: environment.TOLOKA_BASE_URL, source: "from TOLOKA_BASE_URL" },
208
256
  internalBaseUrl === undefined
209
257
  ? undefined
210
258
  : { value: internalBaseUrl, source: `from ENV=${environment.ENV}` },
@@ -213,11 +261,29 @@ const resolvePlatform = (
213
261
  );
214
262
  };
215
263
 
264
+ const accountLines = async (probe: AuthProbe): Promise<readonly string[]> => {
265
+ if (probe.me === undefined) return [];
266
+ try {
267
+ const me = await probe.me();
268
+ if (me.teams === undefined) return [`Internal: ${me.internal}`];
269
+ const teams = me.teams.map((team) => {
270
+ const qualifiers = [...(team.internal ? ["internal"] : []), ...(team.writable ? [] : ["read-only"])];
271
+ return qualifiers.length === 0 ? team.id : `${team.id} (${qualifiers.join(", ")})`;
272
+ });
273
+ return [
274
+ `Internal: ${me.internal}`,
275
+ `Teams: ${teams.length === 0 ? "(none)" : teams.join(", ")}`,
276
+ ];
277
+ } catch {
278
+ return [];
279
+ }
280
+ };
281
+
216
282
  export async function authStatus(options: AuthStatusOptions = {}): Promise<AuthStatusResult> {
217
283
  const environment = options.environment ?? process.env;
218
284
  const credentialLookup =
219
285
  options.credentialLookup ??
220
- (await findCommandCredentials(environment, options.flagApiKey ?? environment.POTATO_API_KEY));
286
+ (await findCommandCredentials(environment, options.flagApiKey ?? environment.TOLOKA_API_KEY));
221
287
  const { found, warning } = credentialLookup;
222
288
  const storedPath = found?.path ?? credentialsPath(environment);
223
289
  const apiKey = resolveApiKey(options, environment, found, storedPath);
@@ -244,8 +310,8 @@ export async function authStatus(options: AuthStatusOptions = {}): Promise<AuthS
244
310
  projectLine,
245
311
  );
246
312
 
247
- if (environment.POTATO_API_KEY !== undefined && found !== undefined) {
248
- lines.push("", `POTATO_API_KEY is set, so the key stored in ${found.path} is ignored.`);
313
+ if (environment.TOLOKA_API_KEY !== undefined && found !== undefined) {
314
+ lines.push("", `TOLOKA_API_KEY is set, so the key stored in ${found.path} is ignored.`);
249
315
  }
250
316
  if (found?.legacy === true) {
251
317
  lines.push(
@@ -260,10 +326,16 @@ export async function authStatus(options: AuthStatusOptions = {}): Promise<AuthS
260
326
  try {
261
327
  const page = await probe.projects.listPage({ limit: 1 });
262
328
  lines.push("", `Reachable: ${page.pagination.total} project(s) visible.`);
329
+ lines.push(...(await accountLines(probe)));
263
330
  return { authenticated: true, output: lines.join("\n") };
264
331
  } catch (error) {
265
- const reason = error instanceof Error ? error.message : String(error);
266
- lines.push("", `Not reachable: ${reason}`, "", "Run toloka login to store a working key.");
332
+ const failure = classifyProbeFailure(error);
333
+ lines.push(
334
+ "",
335
+ `${PROBE_FAILURE_HEADLINE[failure.kind]}: ${failure.reason}`,
336
+ "",
337
+ STATUS_FAILURE_ADVICE[failure.kind](baseUrl),
338
+ );
267
339
  return { authenticated: false, output: lines.join("\n") };
268
340
  }
269
341
  }
@@ -6,16 +6,22 @@ import { join, sep } from "node:path";
6
6
  import { findCliEntry } from "./cli-entry";
7
7
  import type { CliEnvironment } from "./credentials";
8
8
  import { configDirectory, preparePrivateFilePath } from "./credentials";
9
- import { SDK_VERSION } from "./init";
9
+ import {
10
+ compareSdkVersions,
11
+ releaseChannelFor,
12
+ SDK_PACKAGE_NAME,
13
+ SDK_VERSION,
14
+ type ReleaseChannel,
15
+ } from "./sdk-version";
10
16
  import { commandNameFromArgv } from "./analytics";
11
17
  import { readCliSettings } from "./settings";
12
18
  import { CHECK_RESULT_FILE_ENV } from "./check";
13
19
  import { isFalseyFlag, isTruthyFlag } from "./boolean-flag";
14
- import packageMetadata from "../../package.json" with { type: "json" };
20
+ import { TolokaCliError } from "./cli-error";
21
+ import { formatCliError } from "./format-error";
15
22
 
16
23
  export const AUTO_UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
17
24
  export const AUTO_UPDATE_QUARANTINE_MS = 24 * 60 * 60 * 1000;
18
- export const SDK_PACKAGE_NAME = packageMetadata.name;
19
25
  export const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
20
26
 
21
27
  const SKIP_COMMANDS: Readonly<Record<string, true>> = {
@@ -75,32 +81,6 @@ export const npmRegistryUrl = (): string => DEFAULT_NPM_REGISTRY;
75
81
  export const npmPackageMetadataUrl = (packageName: string): string =>
76
82
  new URL(encodeURIComponent(packageName), DEFAULT_NPM_REGISTRY).toString();
77
83
 
78
- export const parseSdkVersion = (version: string): readonly number[] | undefined => {
79
- const match = /^(\d+)\.(\d+)\.(\d+)(?:-preview\.(\d+))?$/u.exec(version);
80
- if (match === null) return undefined;
81
- const [, major, minor, patch, preview] = match;
82
- return [
83
- Number(major),
84
- Number(minor),
85
- Number(patch),
86
- preview === undefined ? Number.POSITIVE_INFINITY : Number(preview),
87
- ];
88
- };
89
-
90
- export const compareSdkVersions = (left: string, right: string): number | undefined => {
91
- const parsedLeft = parseSdkVersion(left);
92
- const parsedRight = parseSdkVersion(right);
93
- if (parsedLeft === undefined || parsedRight === undefined) return undefined;
94
- const length = Math.max(parsedLeft.length, parsedRight.length);
95
- for (let index = 0; index < length; index += 1) {
96
- const leftPart = parsedLeft[index] ?? 0;
97
- const rightPart = parsedRight[index] ?? 0;
98
- if (leftPart < rightPart) return -1;
99
- if (leftPart > rightPart) return 1;
100
- }
101
- return 0;
102
- };
103
-
104
84
  const asStringRecord = (value: unknown): Readonly<Record<string, string>> | undefined => {
105
85
  if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
106
86
  const record: Record<string, string> = {};
@@ -122,9 +102,22 @@ export const asNpmPackageView = (payload: unknown): NpmPackageView | undefined =
122
102
  };
123
103
  };
124
104
 
125
- export const latestPublishedVersion = (view: NpmPackageView): string | undefined => {
126
- const latest = view["dist-tags"]?.latest;
127
- return typeof latest === "string" && latest.length > 0 ? latest : undefined;
105
+ export const latestPublishedVersion = (
106
+ view: NpmPackageView,
107
+ currentVersion: string,
108
+ ): string | undefined => {
109
+ const tags = view["dist-tags"];
110
+ if (tags === undefined) return undefined;
111
+ const channels: readonly ReleaseChannel[] =
112
+ releaseChannelFor(currentVersion) === "preview" ? ["latest", "preview"] : ["latest"];
113
+ let newest: string | undefined;
114
+ for (const channel of channels) {
115
+ const candidate = tags[channel];
116
+ if (candidate === undefined || candidate.length === 0) continue;
117
+ if (newest === undefined || (compareSdkVersions(candidate, newest) ?? 0) > 0)
118
+ newest = candidate;
119
+ }
120
+ return newest;
128
121
  };
129
122
 
130
123
  const publishedAt = (view: NpmPackageView, version: string): Date | undefined => {
@@ -208,9 +201,9 @@ const collectOutput = async (stream: NodeJS.ReadableStream | null): Promise<stri
208
201
  return Buffer.concat(chunks).toString("utf8");
209
202
  };
210
203
 
211
- class RegistryError extends Error {
204
+ class RegistryError extends TolokaCliError {
212
205
  constructor(message: string) {
213
- super(message);
206
+ super(message, { hint: { disabled: true } });
214
207
  this.name = "RegistryError";
215
208
  }
216
209
  }
@@ -248,10 +241,16 @@ const defaultInstall: AutoUpdateInstall = async ({
248
241
  const exitCode = close[0];
249
242
  if (exitCode === 0) return;
250
243
  const detail = [stderr.trim(), stdout.trim()].filter((part) => part.length > 0).join("\n");
251
- throw new Error(
244
+ throw new TolokaCliError(
252
245
  detail.length > 0
253
246
  ? `bun add --global failed: ${detail}`
254
247
  : `bun add --global exited ${String(exitCode)}`,
248
+ {
249
+ hint: {
250
+ description:
251
+ "Run the bun add --global command yourself, or turn the check off with toloka config set auto-update false.",
252
+ },
253
+ },
255
254
  );
256
255
  };
257
256
 
@@ -310,7 +309,7 @@ const selectUpdateCandidate = async (options: {
310
309
  return { status: "skip" };
311
310
  }
312
311
 
313
- const version = latestPublishedVersion(view);
312
+ const version = latestPublishedVersion(view, currentVersion);
314
313
  if (version === undefined) return { status: "skip" };
315
314
  const order = compareSdkVersions(currentVersion, version);
316
315
  if (order === undefined || order >= 0) return { status: "skip" };
@@ -349,9 +348,7 @@ export async function maybeAutoUpdate(options: {
349
348
  environment,
350
349
  });
351
350
  } catch (error) {
352
- io.stderr(
353
- `Auto-update to ${candidate.version} failed: ${error instanceof Error ? error.message : String(error)}.`,
354
- );
351
+ io.stderr(`Auto-update to ${candidate.version} failed: ${formatCliError(error)}`);
355
352
  return continueResult();
356
353
  }
357
354