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

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 (75) hide show
  1. package/README.md +5 -6
  2. package/dist/cli/toloka.js +136 -136
  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/cli-v2.md +3 -2
  7. package/docs/cli.md +35 -7
  8. package/docs/getting-started.md +4 -2
  9. package/docs/limitations.md +2 -2
  10. package/openapi/gen/models/baseModel.ts +1 -0
  11. package/openapi/gen/models/currentUser.ts +2 -0
  12. package/openapi/gen/models/currentUserTeamsItem.ts +6 -0
  13. package/openapi/gen/models/gistingTrainingConfiguration.ts +6 -2
  14. package/openapi/gen/models/gistingTrainingConfigurationBatchSize.ts +2 -0
  15. package/openapi/gen/models/gistingTrainingConfigurationGistTokens.ts +2 -0
  16. package/openapi/gen/models/gistingTrainingConfigurationInput.ts +6 -2
  17. package/openapi/gen/models/gistingTrainingConfigurationInputBatchSize.ts +2 -0
  18. package/openapi/gen/models/gistingTrainingConfigurationInputGistTokens.ts +2 -0
  19. package/openapi/gen/models/index.ts +9 -4
  20. package/openapi/gen/models/sftTrainingConfiguration.ts +6 -3
  21. package/openapi/gen/models/sftTrainingConfigurationBatchSize.ts +2 -0
  22. package/openapi/gen/models/sftTrainingConfigurationInput.ts +6 -3
  23. package/openapi/gen/models/sftTrainingConfigurationInputBatchSize.ts +2 -0
  24. package/openapi/gen/models/sftTrainingConfigurationInputLoraRank.ts +2 -0
  25. package/openapi/gen/models/sftTrainingConfigurationLoraRank.ts +2 -0
  26. package/package.json +2 -2
  27. package/skills/catalog.json +11 -11
  28. package/skills/toloka-build/SKILL.md +29 -18
  29. package/skills/toloka-build/references/node-semantics.md +7 -7
  30. package/skills/toloka-build/references/workflow-and-cli.md +15 -12
  31. package/skills/toloka-fine-tuning/SKILL.md +145 -62
  32. package/skills/toloka-get-started/SKILL.md +37 -21
  33. package/skills/toloka-gisting/SKILL.md +156 -66
  34. package/skills/toloka-report-issue/references/report-template.md +1 -1
  35. package/src/cli/analytics.ts +50 -23
  36. package/src/cli/auth.ts +31 -12
  37. package/src/cli/auto-update.ts +12 -6
  38. package/src/cli/check.ts +76 -29
  39. package/src/cli/cli-entry.ts +3 -2
  40. package/src/cli/cli-error.ts +43 -0
  41. package/src/cli/commands/dataset.ts +13 -3
  42. package/src/cli/commands/models.ts +6 -7
  43. package/src/cli/commands/pipeline.ts +6 -7
  44. package/src/cli/commands/project.ts +9 -16
  45. package/src/cli/commands/report.ts +6 -2
  46. package/src/cli/commands/secret.ts +5 -4
  47. package/src/cli/commands/source.ts +4 -4
  48. package/src/cli/commands/status.ts +2 -3
  49. package/src/cli/commands/training.ts +50 -17
  50. package/src/cli/config.ts +2 -2
  51. package/src/cli/context.ts +55 -17
  52. package/src/cli/credentials.ts +12 -4
  53. package/src/cli/data.ts +96 -34
  54. package/src/cli/delivery-orders.ts +18 -1
  55. package/src/cli/file-system.ts +6 -2
  56. package/src/cli/format-error.ts +45 -2
  57. package/src/cli/init.ts +13 -7
  58. package/src/cli/models.ts +43 -12
  59. package/src/cli/program.ts +5 -56
  60. package/src/cli/project-context.ts +117 -32
  61. package/src/cli/project.ts +80 -28
  62. package/src/cli/report.ts +24 -9
  63. package/src/cli/runs.ts +33 -8
  64. package/src/cli/settings.ts +13 -5
  65. package/src/cli/skill.ts +61 -22
  66. package/src/cli/source.ts +3 -2
  67. package/src/cli/terminal-io.ts +13 -6
  68. package/src/cli/trainings.ts +129 -35
  69. package/src/cli/version.ts +2 -2
  70. package/src/client.ts +3 -9
  71. package/src/transport.ts +13 -1
  72. package/openapi/gen/models/gistingTrainingConfigurationInputRegime.ts +0 -2
  73. package/openapi/gen/models/gistingTrainingConfigurationRegime.ts +0 -2
  74. package/openapi/gen/models/sftTrainingConfigurationInputRegime.ts +0 -2
  75. package/openapi/gen/models/sftTrainingConfigurationRegime.ts +0 -2
package/src/cli/runs.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import select from "@inquirer/select";
2
- import { assert } from "es-toolkit";
3
2
  import type { Pipeline, PotatoClient } from "../client";
4
3
  import { PotatoApiError } from "../errors";
5
4
  import type { TolokaState } from "../state";
6
5
  import type { RunItem, RunJob } from "../index";
6
+ import { assertCli } from "./cli-error";
7
7
 
8
8
  export type PipelineRef = {
9
9
  readonly pipelineId: string;
@@ -131,10 +131,18 @@ const uniqueMatch = (
131
131
  ambiguous: string,
132
132
  unknown: string,
133
133
  ): PipelineRef | undefined => {
134
- assert(matches.length <= 1, ambiguous);
134
+ assertCli(
135
+ matches.length <= 1,
136
+ ambiguous,
137
+ { hint: { description: "Pass the pipeline id shown by toloka pipeline list instead of the name." } },
138
+ );
135
139
  if (matches.length !== 1) return undefined;
136
140
  const [ref] = matches;
137
- assert(ref, unknown);
141
+ assertCli(
142
+ ref,
143
+ unknown,
144
+ { hint: { description: "Run toloka pipeline list to see the pipelines of this project." } },
145
+ );
138
146
  return ref;
139
147
  };
140
148
 
@@ -174,7 +182,11 @@ const resolveToken = (token: string, live: readonly PipelineRef[]): PipelineRef
174
182
  );
175
183
  if (basename !== undefined) return basename;
176
184
 
177
- assert(false, unknown);
185
+ assertCli(
186
+ false,
187
+ unknown,
188
+ { hint: { description: "Run toloka pipeline list to see the pipelines of this project." } },
189
+ );
178
190
  };
179
191
 
180
192
  export async function resolvePipelineRef(options: {
@@ -188,7 +200,11 @@ export async function resolvePipelineRef(options: {
188
200
 
189
201
  if (options.pipeline !== undefined) return resolveToken(options.pipeline, live);
190
202
 
191
- assert(live.length > 0, "This project has no pipelines.");
203
+ assertCli(
204
+ live.length > 0,
205
+ "This project has no pipelines.",
206
+ { hint: { description: "Run toloka apply to create the pipelines declared in source." } },
207
+ );
192
208
 
193
209
  if (live.length > 1 && options.select !== undefined) {
194
210
  const chosen = await options.select(
@@ -199,13 +215,22 @@ export async function resolvePipelineRef(options: {
199
215
 
200
216
  if (live.length === 1) {
201
217
  const [only] = live;
202
- assert(only, "This project has no pipelines.");
203
- assert(false, `Pass a pipeline name. This project has: ${formatPipelineChoice(only)}.`);
218
+ assertCli(
219
+ only,
220
+ "This project has no pipelines.",
221
+ { hint: { description: "Run toloka apply to create the pipelines declared in source." } },
222
+ );
223
+ assertCli(
224
+ false,
225
+ `Pass a pipeline name. This project has: ${formatPipelineChoice(only)}.`,
226
+ { hint: { disabled: true } },
227
+ );
204
228
  }
205
229
 
206
- assert(
230
+ assertCli(
207
231
  false,
208
232
  `This project tracks several pipelines, so name one: ${live.map(formatPipelineChoice).join(", ")}.`,
233
+ { hint: { disabled: true } },
209
234
  );
210
235
  }
211
236
 
@@ -3,6 +3,8 @@ import { join } from "node:path";
3
3
  import { isFalseyFlag, isTruthyFlag } from "./boolean-flag";
4
4
  import type { CliEnvironment } from "./credentials";
5
5
  import { configDirectory, preparePrivateFilePath } from "./credentials";
6
+ import { TolokaCliError } from "./cli-error";
7
+ import type { TolokaCliErrorOptions } from "./cli-error";
6
8
 
7
9
  export const AUTO_UPDATE_SETTING = "auto-update";
8
10
 
@@ -14,8 +16,8 @@ export const DEFAULT_CLI_SETTINGS: CliSettings = {
14
16
  autoUpdate: true,
15
17
  };
16
18
 
17
- export class SettingsError extends Error {
18
- constructor(message: string, options?: { readonly cause?: unknown }) {
19
+ export class SettingsError extends TolokaCliError {
20
+ constructor(message: string, options: TolokaCliErrorOptions) {
19
21
  super(message, options);
20
22
  this.name = "SettingsError";
21
23
  }
@@ -59,7 +61,9 @@ export async function writeCliSettings(
59
61
  export const parseBooleanSetting = (value: string): boolean => {
60
62
  if (isTruthyFlag(value)) return true;
61
63
  if (isFalseyFlag(value)) return false;
62
- throw new SettingsError(`Expected a boolean (true or false), got ${JSON.stringify(value)}.`);
64
+ throw new SettingsError(`Expected a boolean (true or false), got ${JSON.stringify(value)}.`, {
65
+ hint: { description: "Run toloka config set auto-update true or false." },
66
+ });
63
67
  };
64
68
 
65
69
  export const formatCliSettings = (settings: CliSettings): string =>
@@ -71,7 +75,9 @@ export const getCliSetting = (
71
75
  ): { readonly output: string } => {
72
76
  if (name === undefined || name.length === 0) return { output: formatCliSettings(settings) };
73
77
  if (name !== AUTO_UPDATE_SETTING) {
74
- throw new SettingsError(`Unknown setting "${name}". Known settings: ${AUTO_UPDATE_SETTING}.`);
78
+ throw new SettingsError(`Unknown setting "${name}". Known settings: ${AUTO_UPDATE_SETTING}.`, {
79
+ hint: { disabled: true },
80
+ });
75
81
  }
76
82
  return { output: String(settings.autoUpdate) };
77
83
  };
@@ -82,7 +88,9 @@ export async function setCliSetting(
82
88
  value: string,
83
89
  ): Promise<{ readonly output: string; readonly settings: CliSettings }> {
84
90
  if (name !== AUTO_UPDATE_SETTING) {
85
- throw new SettingsError(`Unknown setting "${name}". Known settings: ${AUTO_UPDATE_SETTING}.`);
91
+ throw new SettingsError(`Unknown setting "${name}". Known settings: ${AUTO_UPDATE_SETTING}.`, {
92
+ hint: { disabled: true },
93
+ });
86
94
  }
87
95
  const settings = { autoUpdate: parseBooleanSetting(value) };
88
96
  await writeCliSettings(settings, environment);
package/src/cli/skill.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { assert } from "es-toolkit";
2
1
  import { createHash } from "node:crypto";
3
2
  import { copyFile, mkdir, mkdtemp, readFile, realpath, rename, rm, stat } from "node:fs/promises";
4
3
  import { dirname, join } from "node:path";
5
4
  import { assertSafeDestination, entryStats, hasErrorCode } from "./file-system";
6
5
  import { formatCliError } from "./format-error";
7
6
  import { releaseChannelFor, SDK_PACKAGE_NAME, SDK_VERSION } from "./sdk-version";
7
+ import { assertCli, REINSTALL_HINT, TolokaCliError } from "./cli-error";
8
8
 
9
9
  export const SKILL_CATALOG_MANIFEST = "catalog.json";
10
10
  export const SKILL_INSTALL_ROOT = ".agents/skills";
@@ -76,31 +76,51 @@ const isSafeRelativePath = (path: string) =>
76
76
  path.split("/").every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
77
77
 
78
78
  const parseDescriptor = (value: unknown, invalid: (detail: string) => string): SkillDescriptor => {
79
- assert(isRecord(value), invalid("every skill must be an object."));
79
+ assertCli(isRecord(value), invalid("every skill must be an object."), { hint: REINSTALL_HINT });
80
80
  const { id, name, description, tags, version, digest, files } = value;
81
- assert(
81
+ assertCli(
82
82
  isSafeSkillId(id),
83
83
  invalid(
84
84
  `skill id ${JSON.stringify(id)} must be at most ${MAX_SKILL_ID_LENGTH} lowercase letters, digits, and single hyphens.`,
85
85
  ),
86
+ { hint: REINSTALL_HINT },
86
87
  );
87
- assert(isNonEmptyString(name), invalid(`skill ${id} needs a name.`));
88
- assert(isNonEmptyString(description), invalid(`skill ${id} needs a description.`));
89
- assert(isStringArray(tags), invalid(`skill ${id} tags must be strings.`));
90
- assert(isNonEmptyString(version), invalid(`skill ${id} needs a version.`));
91
- assert(
88
+ assertCli(isNonEmptyString(name), invalid(`skill ${id} needs a name.`), { hint: REINSTALL_HINT });
89
+ assertCli(
90
+ isNonEmptyString(description),
91
+ invalid(`skill ${id} needs a description.`),
92
+ { hint: REINSTALL_HINT },
93
+ );
94
+ assertCli(
95
+ isStringArray(tags),
96
+ invalid(`skill ${id} tags must be strings.`),
97
+ { hint: REINSTALL_HINT },
98
+ );
99
+ assertCli(
100
+ isNonEmptyString(version),
101
+ invalid(`skill ${id} needs a version.`),
102
+ { hint: REINSTALL_HINT },
103
+ );
104
+ assertCli(
92
105
  typeof digest === "string" && DIGEST_PATTERN.test(digest),
93
106
  invalid(`skill ${id} digest must be the sha256 hex digest of its files.`),
107
+ { hint: REINSTALL_HINT },
94
108
  );
95
- assert(
109
+ assertCli(
96
110
  isStringArray(files) && files.includes(SKILL_ENTRY_POINT),
97
111
  invalid(`skill ${id} files must include ${SKILL_ENTRY_POINT}.`),
112
+ { hint: REINSTALL_HINT },
98
113
  );
99
- assert(
114
+ assertCli(
100
115
  files.every(isSafeRelativePath),
101
116
  invalid(`skill ${id} files must be relative paths inside its directory.`),
117
+ { hint: REINSTALL_HINT },
118
+ );
119
+ assertCli(
120
+ new Set(files).size === files.length,
121
+ invalid(`skill ${id} lists a file more than once.`),
122
+ { hint: REINSTALL_HINT },
102
123
  );
103
- assert(new Set(files).size === files.length, invalid(`skill ${id} lists a file more than once.`));
104
124
  return { id, name, description, tags, version, digest, files };
105
125
  };
106
126
 
@@ -108,7 +128,10 @@ const parseManifest = (source: string, manifestPath: string): unknown => {
108
128
  try {
109
129
  return JSON.parse(source);
110
130
  } catch (error) {
111
- throw new Error(`Invalid skill catalog ${manifestPath}: ${formatCliError(error)}`);
131
+ throw new TolokaCliError(`Invalid skill catalog ${manifestPath}: ${formatCliError(error)}`, {
132
+ cause: error,
133
+ hint: REINSTALL_HINT,
134
+ });
112
135
  }
113
136
  };
114
137
 
@@ -140,9 +163,10 @@ export async function resolveBundledSkillCatalogRoot(
140
163
  entry === undefined
141
164
  ? "process.argv[1] names no runtime entry"
142
165
  : `none exists between ${entry} and the filesystem root`;
143
- assert(
166
+ assertCli(
144
167
  workingDirectoryRoot !== undefined,
145
168
  `Cannot locate the bundled skill catalog. An installed @toloka-platform/sdk must ship ${SKILL_CATALOG_DIRECTORY}/${SKILL_CATALOG_MANIFEST} in a directory above its runtime entry, but ${searched}, and no ${SKILL_CATALOG_DIRECTORY}/${SKILL_CATALOG_MANIFEST} exists above ${process.cwd()} either. Reinstall @toloka-platform/sdk.`,
169
+ { hint: { disabled: true } },
146
170
  );
147
171
  return workingDirectoryRoot;
148
172
  }
@@ -154,23 +178,30 @@ export async function loadSkillCatalog(catalogRoot: string): Promise<SkillCatalo
154
178
  const manifestPath = join(catalogRoot, SKILL_CATALOG_MANIFEST);
155
179
  const invalid = (detail: string) => `Invalid skill catalog ${manifestPath}: ${detail}`;
156
180
  const { size } = await stat(manifestPath);
157
- assert(
181
+ assertCli(
158
182
  size <= MAX_MANIFEST_BYTES,
159
183
  invalid(`the manifest is ${size} bytes; the limit is ${MAX_MANIFEST_BYTES} bytes.`),
184
+ { hint: REINSTALL_HINT },
160
185
  );
161
186
  const manifest = parseManifest(await readFile(manifestPath, "utf8"), manifestPath);
162
- assert(
187
+ assertCli(
163
188
  isRecord(manifest) && Array.isArray(manifest.skills),
164
189
  invalid("expected an object with a skills array."),
190
+ { hint: REINSTALL_HINT },
165
191
  );
166
- assert(
192
+ assertCli(
167
193
  manifest.skills.length <= MAX_SKILL_COUNT,
168
194
  invalid(`${manifest.skills.length} skills exceed the limit of ${MAX_SKILL_COUNT}.`),
195
+ { hint: REINSTALL_HINT },
169
196
  );
170
197
  const skills = manifest.skills.map((skill) => parseDescriptor(skill, invalid));
171
198
  const ids = new Set<string>();
172
199
  for (const skill of skills) {
173
- assert(!ids.has(skill.id), invalid(`skill id ${skill.id} appears more than once.`));
200
+ assertCli(
201
+ !ids.has(skill.id),
202
+ invalid(`skill id ${skill.id} appears more than once.`),
203
+ { hint: REINSTALL_HINT },
204
+ );
174
205
  ids.add(skill.id);
175
206
  }
176
207
  return { root: catalogRoot, manifestPath, skills };
@@ -253,9 +284,14 @@ const swapDirectories = async (
253
284
  try {
254
285
  await renameEntry(backup, destination);
255
286
  } catch (restoreError) {
256
- throw new Error(
287
+ throw new TolokaCliError(
257
288
  `Failed to replace ${destination}; the previous version is preserved at ${backup}. ${formatCliError(restoreError)}`,
258
- { cause: error },
289
+ {
290
+ cause: error,
291
+ hint: {
292
+ description: `Move ${backup} back to ${destination} by hand, then re-run toloka skill install <skill-id> --force.`,
293
+ },
294
+ },
259
295
  );
260
296
  }
261
297
  throw error;
@@ -271,17 +307,19 @@ export async function installSkill({
271
307
  rename: renameEntry = rename,
272
308
  }: InstallSkillOptions): Promise<InstalledSkill> {
273
309
  const skill = catalog.skills.find((candidate) => candidate.id === skillId);
274
- assert(
310
+ assertCli(
275
311
  skill,
276
312
  `Skill ${JSON.stringify(skillId)} is not in the catalog bundled with ${SDK_PACKAGE_NAME} ${SDK_VERSION}. Run \`toloka skill list\` to see the available skills; a newer release may ship it (${UPGRADE_HINT}).`,
313
+ { hint: { disabled: true } },
277
314
  );
278
315
  const relativeDestination = `${SKILL_INSTALL_ROOT}/${skill.id}`;
279
316
  await assertSafeDestination(sourceRoot, relativeDestination);
280
317
  const destination = join(sourceRoot, relativeDestination);
281
318
  const existing = await entryStats(destination);
282
- assert(
319
+ assertCli(
283
320
  existing === undefined || force,
284
321
  `${relativeDestination} already exists. Re-run with --force to replace it with the catalog version of ${skill.id}.`,
322
+ { hint: { disabled: true } },
285
323
  );
286
324
 
287
325
  const skillsRoot = dirname(destination);
@@ -293,9 +331,10 @@ export async function installSkill({
293
331
  await mkdir(dirname(target), { recursive: true });
294
332
  await copyFile(join(catalog.root, skill.id, path), target);
295
333
  }
296
- assert(
334
+ assertCli(
297
335
  (await computeSkillDigest(temporary, skill.files)) === skill.digest,
298
336
  `Skill ${skill.id} failed its digest check: the packaged files do not match the catalog digest ${skill.digest}. Nothing was installed.`,
337
+ { hint: REINSTALL_HINT },
299
338
  );
300
339
  if (existing === undefined) {
301
340
  await renameEntry(temporary, destination);
package/src/cli/source.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { assert } from "es-toolkit";
2
1
  import { readdir, stat } from "node:fs/promises";
3
2
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
4
3
  import { pathToFileURL } from "node:url";
5
4
  import { validatePipelineDefinition } from "../authoring";
6
5
  import type { PipelineDefinition } from "../authoring";
6
+ import { assertCli } from "./cli-error";
7
7
 
8
8
  export type ProjectPipelineSource = {
9
9
  readonly sourceDirectory: string;
@@ -64,9 +64,10 @@ export async function loadProjectSource(sourceRoot: string): Promise<ProjectSour
64
64
  const moduleUrl = pathToFileURL(modulePath);
65
65
  moduleUrl.searchParams.set("version", `${moduleStat.mtimeMs}-${moduleStat.size}`);
66
66
  const module: unknown = await import(moduleUrl.href);
67
- assert(
67
+ assertCli(
68
68
  typeof module === "object" && module !== null,
69
69
  `Invalid ${modulePath}: expected a module`,
70
+ { hint: { description: "Make the file a TypeScript module that exports the pipeline definition." } },
70
71
  );
71
72
  const definition: unknown = Reflect.get(module, "default");
72
73
  validatePipelineDefinition(definition);
@@ -2,6 +2,15 @@ import { createInterface } from "node:readline/promises";
2
2
  import type { AuthPrompt } from "./auth";
3
3
  import type { CliIo } from "./context";
4
4
  import { pickPipeline } from "./runs";
5
+ import { TolokaCliError } from "./cli-error";
6
+
7
+ const stdinClosedError = () =>
8
+ new TolokaCliError("Standard input closed before an API key was entered.", {
9
+ hint: {
10
+ description:
11
+ "Run toloka login in an interactive terminal, or pipe the key with toloka login --api-key-stdin.",
12
+ },
13
+ });
5
14
 
6
15
  const readStandardInput = async (): Promise<string> => {
7
16
  process.stdin.setEncoding("utf8");
@@ -35,7 +44,7 @@ const readMaskedLine = async (question: string): Promise<string> => {
35
44
 
36
45
  const onEnd = () => {
37
46
  finish();
38
- reject(new Error("Standard input closed before an API key was entered."));
47
+ reject(stdinClosedError());
39
48
  };
40
49
 
41
50
  const onError = (error: Error) => {
@@ -53,11 +62,9 @@ const readMaskedLine = async (question: string): Promise<string> => {
53
62
  if (character === "\u0003" || character === "\u0004") {
54
63
  finish();
55
64
  reject(
56
- new Error(
57
- character === "\u0003"
58
- ? "Canceled."
59
- : "Standard input closed before an API key was entered.",
60
- ),
65
+ character === "\u0003"
66
+ ? new TolokaCliError("Canceled.", { hint: { disabled: true } })
67
+ : stdinClosedError(),
61
68
  );
62
69
  return;
63
70
  }