@toloka-platform/sdk 0.2.0 → 0.3.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 (62) hide show
  1. package/README.md +12 -6
  2. package/dist/cli/toloka.js +269 -142
  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 +2 -0
  7. package/docs/cli.md +86 -23
  8. package/docs/getting-started.md +11 -9
  9. package/docs/limitations.md +6 -0
  10. package/docs/testing-examples.md +2 -2
  11. package/docs/testing.md +10 -2
  12. package/docs/troubleshooting.md +16 -4
  13. package/openapi/gen/api.ts +30 -0
  14. package/openapi/gen/models/gistingTrainingConfigurationGistTokens.ts +1 -1
  15. package/openapi/gen/models/gistingTrainingConfigurationInputGistTokens.ts +1 -1
  16. package/openapi/gen/models/index.ts +3 -0
  17. package/openapi/gen/models/listAudienceSpecializations200.ts +7 -0
  18. package/openapi/gen/models/listAudienceSpecializationsParams.ts +5 -0
  19. package/openapi/gen/models/specialization.ts +6 -0
  20. package/openapi/gen/models/trainingCostEstimateOneOfTwoseven.ts +0 -2
  21. package/openapi/gen/models/trainingRunQuote.ts +0 -2
  22. package/package.json +1 -1
  23. package/skills/catalog.json +10 -10
  24. package/skills/toloka-build/SKILL.md +8 -5
  25. package/skills/toloka-build/references/node-semantics.md +2 -2
  26. package/skills/toloka-build/references/workflow-and-cli.md +50 -25
  27. package/skills/toloka-fine-tuning/SKILL.md +3 -2
  28. package/skills/toloka-get-started/SKILL.md +4 -3
  29. package/skills/toloka-gisting/SKILL.md +4 -3
  30. package/skills/toloka-report-issue/references/report-template.md +1 -1
  31. package/src/checks/task-ui-typecheck.ts +4 -1
  32. package/src/cli/analytics.ts +1 -0
  33. package/src/cli/audience.ts +31 -0
  34. package/src/cli/auth.ts +382 -81
  35. package/src/cli/auto-update.ts +363 -24
  36. package/src/cli/browser-login.ts +265 -0
  37. package/src/cli/browser-session.ts +244 -0
  38. package/src/cli/check.ts +51 -52
  39. package/src/cli/cli-entry.ts +31 -0
  40. package/src/cli/commands/audience.ts +22 -0
  41. package/src/cli/commands/auth.ts +22 -15
  42. package/src/cli/commands/project.ts +3 -3
  43. package/src/cli/commands/training.ts +10 -2
  44. package/src/cli/config.ts +41 -14
  45. package/src/cli/context.ts +45 -9
  46. package/src/cli/credentials.ts +13 -13
  47. package/src/cli/init.ts +7 -2
  48. package/src/cli/keycloak.ts +189 -0
  49. package/src/cli/models.ts +1 -21
  50. package/src/cli/native-runtime.ts +7 -0
  51. package/src/cli/program.ts +5 -0
  52. package/src/cli/sign-in-page.ts +153 -0
  53. package/src/cli/skill.ts +34 -26
  54. package/src/cli/style.ts +37 -0
  55. package/src/cli/table.ts +21 -0
  56. package/src/cli/terminal-io.ts +9 -2
  57. package/src/cli/trainings.ts +1 -22
  58. package/src/cli/version.ts +12 -1
  59. package/src/client.ts +20 -3
  60. package/src/index.ts +1 -0
  61. package/src/pipeline-graph-v1.contract.json +47 -24
  62. package/src/transport.ts +19 -11
@@ -1,8 +1,20 @@
1
1
  import { existsSync, realpathSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { assertCli } from "./cli-error";
4
+ import { isNativeCli } from "./native-runtime";
4
5
 
5
6
  const CLI_ENTRY = join("bin", "toloka.js");
7
+ export type CliRelaunchCommand = {
8
+ readonly executable: string;
9
+ readonly args: readonly string[];
10
+ readonly environment: NodeJS.ProcessEnv;
11
+ };
12
+
13
+ export type CliRelaunchOptions = {
14
+ readonly native?: boolean;
15
+ readonly executable?: string;
16
+ readonly runtimeEntry?: string;
17
+ };
6
18
 
7
19
  const runtimePath = (entry: string): string => {
8
20
  try {
@@ -40,3 +52,22 @@ export const resolveCliEntry = (runtimeEntry: string | undefined = process.argv[
40
52
  );
41
53
  return workingDirectoryEntry;
42
54
  };
55
+
56
+ export const cliRelaunchCommand = (
57
+ argv: readonly string[],
58
+ environment: NodeJS.ProcessEnv,
59
+ options: CliRelaunchOptions = {},
60
+ ): CliRelaunchCommand => {
61
+ const executable = options.executable ?? process.execPath;
62
+ if (!(options.native ?? isNativeCli())) {
63
+ return {
64
+ executable:
65
+ options.executable ?? (Reflect.get(process.versions, "bun") ? process.execPath : "bun"),
66
+ args: [resolveCliEntry(options.runtimeEntry), ...argv],
67
+ environment,
68
+ };
69
+ }
70
+ const reexecEnvironment = { ...environment };
71
+ delete reexecEnvironment.BUN_BE_BUN;
72
+ return { executable, args: [...argv], environment: reexecEnvironment };
73
+ };
@@ -0,0 +1,22 @@
1
+ import type { Command } from "commander";
2
+ import { listSpecializations } from "../audience";
3
+ import { resolveContext, type CommandHost } from "../context";
4
+
5
+ export const registerAudienceCommands = (program: Command, host: CommandHost): void => {
6
+ const { context, globals } = host;
7
+
8
+ const audience = program
9
+ .command("audience")
10
+ .description("inspect the expert audience catalog used by node audience filters");
11
+ const specialization = audience
12
+ .command("specialization")
13
+ .description("expert specializations (platform qualifications)");
14
+ specialization
15
+ .command("list")
16
+ .option("--json", "print JSON", false)
17
+ .description("list specializations with their id, name, and expert count")
18
+ .action(async (options: { json: boolean }) => {
19
+ const { client } = await resolveContext(globals(), context);
20
+ await listSpecializations(client, { json: options.json }, context.io);
21
+ });
22
+ };
@@ -1,56 +1,66 @@
1
1
  import type { Command } from "commander";
2
- import { createClient } from "../../client";
3
2
  import { authStatus, login, logout } from "../auth";
4
3
  import {
5
4
  identifyClientUser,
5
+ resolveClient,
6
6
  resolveSelectedProject,
7
7
  resolveSourceRoot,
8
8
  type CommandHost,
9
9
  } from "../context";
10
10
  import { findCommandCredentials, readStoredCredentials } from "../credentials";
11
- import { resolveTolokaConfig } from "../config";
12
11
  import { readTolokaState } from "../../state";
13
12
 
13
+ type LoginCommandOptions = {
14
+ readonly apiKeyStdin: boolean;
15
+ readonly browser?: boolean;
16
+ };
17
+
14
18
  export const registerAuthCommands = (program: Command, host: CommandHost): void => {
15
19
  const { context, exitCode, globals } = host;
16
20
 
17
21
  program
18
22
  .command("login")
19
- .description("store an API key for this machine after verifying it")
20
- .option("--api-key-stdin", "read the API key from standard input instead of prompting", false)
21
- .action(async (options: { apiKeyStdin: boolean }) => {
23
+ .description("sign in through your browser, or store an API key, after verifying access")
24
+ .option("--browser", "sign in through your browser without asking; pick the provider on the page it opens")
25
+ .option("--no-browser", "the same, but print the sign-in link instead of opening a browser")
26
+ .option("--api-key-stdin", "store an API key read from standard input instead", false)
27
+ .action(async (options: LoginCommandOptions) => {
22
28
  const apiKey = options.apiKeyStdin ? (await context.io.readStdin()).trim() : globals().apiKey;
23
29
  const injectedClient = context.injectedClient;
24
30
  const result = await login({
25
31
  ...(apiKey === undefined ? {} : { apiKey }),
32
+ ...(options.browser === undefined ? {} : { browser: options.browser }),
26
33
  ...(globals().baseUrl === undefined ? {} : { baseUrl: globals().baseUrl }),
27
34
  ...(context.io.prompt === undefined ? {} : { prompt: context.io.prompt }),
35
+ ...(context.io.choose === undefined ? {} : { choose: context.io.choose }),
28
36
  ...(injectedClient === undefined ? {} : { probe: () => injectedClient }),
37
+ ...(context.browserLogin === undefined ? {} : { browserLogin: context.browserLogin }),
38
+ ...(context.io.style === undefined ? {} : { style: context.io.style }),
29
39
  write: context.io.stdout,
30
40
  environment: context.environment,
31
41
  });
32
42
  context.io.stdout(result.output);
33
43
  try {
34
44
  const stored = await readStoredCredentials(context.environment);
35
- if (stored === undefined) return;
36
- const client = context.injectedClient ?? createClient(stored);
45
+ const { client } = await resolveClient(globals(), context, undefined, stored);
37
46
  await identifyClientUser(client, context.analytics);
38
47
  } catch {}
39
48
  });
40
49
 
41
50
  program
42
51
  .command("logout")
43
- .description("remove stored credentials from this machine")
52
+ .description("remove stored credentials from this machine and end the browser login")
44
53
  .action(async () => {
45
- const result = await logout(context.environment);
54
+ const baseUrl = globals().baseUrl;
55
+ const result = await logout(context.environment, baseUrl === undefined ? {} : { baseUrl });
46
56
  context.io.stdout(result.output);
47
57
  });
48
58
 
49
59
  program
50
60
  .command("auth-status")
51
61
  .alias("whoami")
52
- .description("show which platform and key the CLI would use")
53
- .option("--show-token", "print the API key instead of masking it", false)
62
+ .description("show which platform and login the CLI would use")
63
+ .option("--show-token", "print the API key or access token instead of masking it", false)
54
64
  .action(async (options: { showToken: boolean }) => {
55
65
  const flags = globals();
56
66
  const credentialLookup = await findCommandCredentials(
@@ -79,10 +89,7 @@ export const registerAuthCommands = (program: Command, host: CommandHost): void
79
89
  return;
80
90
  }
81
91
  try {
82
- const config = resolveTolokaConfig(flags, state, context.environment, stored);
83
- const client =
84
- context.injectedClient ??
85
- createClient({ apiKey: config.apiKey, baseUrl: config.baseUrl });
92
+ const { client } = await resolveClient(flags, context, state, stored);
86
93
  await identifyClientUser(client, context.analytics);
87
94
  } catch {}
88
95
  });
@@ -1,7 +1,7 @@
1
1
  import type { Command } from "commander";
2
- import { createClient } from "../../client";
3
2
  import {
4
3
  identifyClientUser,
4
+ resolveClient,
5
5
  positiveInteger,
6
6
  readCommandCredentials,
7
7
  resolveContext,
@@ -42,7 +42,7 @@ export const registerProjectCommands = (program: Command, host: CommandHost): vo
42
42
  .action(async (alias?: string, projectId?: string) => {
43
43
  const flags = globals();
44
44
  const found = await requireStoredCredentials(context.environment);
45
- const client = context.injectedClient ?? createClient(found.credentials);
45
+ const { client } = await resolveClient(flags, context, undefined, found.credentials);
46
46
  await addProject({
47
47
  environment: context.environment,
48
48
  flags,
@@ -147,7 +147,7 @@ export const registerProjectCommands = (program: Command, host: CommandHost): vo
147
147
  ) => {
148
148
  const flags = globals();
149
149
  const found = await requireStoredCredentials(context.environment);
150
- const client = context.injectedClient ?? createClient(found.credentials);
150
+ const { client } = await resolveClient(flags, context, undefined, found.credentials);
151
151
  const sourceRoot = await resolveSourceRoot(flags, context.io);
152
152
  await createProject({
153
153
  environment: context.environment,
@@ -64,7 +64,11 @@ export const registerTrainingCommands = (program: Command, host: CommandHost): v
64
64
  .option("--train-filter <field=value>", "training Dataset field equality filter")
65
65
  .option("--validation-filter <field=value>", "validation Dataset field equality filter")
66
66
  .option("--lora-rank <rank>", "SFT LoRA adapter rank: 8, 16, or 32 (default 16)", trainingLoraRank)
67
- .option("--gist-tokens <count>", "gisting soft tokens: 8, 32, or 64 (default 32)", trainingGistTokens)
67
+ .option(
68
+ "--gist-tokens <count>",
69
+ 'gisting soft tokens: 8, 32, or 64, or a fraction of the prompt "1/4", "1/8", "1/16", "1/32" (default 32)',
70
+ trainingGistTokens,
71
+ )
68
72
  .option("--epochs <count>", "training epochs, 1 through 5 (default 3)", trainingEpochs)
69
73
  .option(
70
74
  "--learning-rate <rate>",
@@ -111,7 +115,11 @@ export const registerTrainingCommands = (program: Command, host: CommandHost): v
111
115
  "replacement validation Dataset field equality filter",
112
116
  )
113
117
  .option("--lora-rank <rank>", "SFT LoRA adapter rank: 8, 16, or 32 (default 16)", trainingLoraRank)
114
- .option("--gist-tokens <count>", "gisting soft tokens: 8, 32, or 64 (default 32)", trainingGistTokens)
118
+ .option(
119
+ "--gist-tokens <count>",
120
+ 'gisting soft tokens: 8, 32, or 64, or a fraction of the prompt "1/4", "1/8", "1/16", "1/32" (default 32)',
121
+ trainingGistTokens,
122
+ )
115
123
  .option("--epochs <count>", "training epochs, 1 through 5 (default 3)", trainingEpochs)
116
124
  .option(
117
125
  "--learning-rate <rate>",
package/src/cli/config.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { TolokaState } from "../state";
2
2
  import type { StoredCredentials } from "./credentials";
3
+ import type { BrowserLoginProvider } from "./keycloak";
3
4
  import { resolveProjectContext } from "./project-context";
4
5
  import { assertCli } from "./cli-error";
5
6
 
@@ -15,9 +16,13 @@ export type TolokaEnvironment = Readonly<Record<string, string | undefined>> & {
15
16
  readonly TOLOKA_PROJECT_ID?: string;
16
17
  };
17
18
 
19
+ export type CliCredential =
20
+ | { readonly type: "apiKey"; readonly apiKey: string }
21
+ | { readonly type: "browser"; readonly provider: BrowserLoginProvider };
22
+
18
23
  export type ResolvedTolokaConfig = {
19
- readonly apiKey: string;
20
24
  readonly baseUrl: string;
25
+ readonly credential: CliCredential;
21
26
  readonly projectId?: string;
22
27
  };
23
28
 
@@ -34,6 +39,9 @@ export const resolveInternalBaseUrl = (environment: TolokaEnvironment): string |
34
39
  const PRODUCTION_CONSOLE_URL = "https://console.toloka.ai";
35
40
  const INTERNAL_CONSOLE_URL = "https://prestable.console.toloka-test.ai";
36
41
 
42
+ export const registrationUrlFor = (baseUrl: string): string =>
43
+ new URL("/auth?redirectPath=%2Fregistration", baseUrl).href;
44
+
37
45
  export const consoleUrlFor = (baseUrl: string): string | undefined => {
38
46
  let host: string;
39
47
  try {
@@ -47,28 +55,41 @@ export const consoleUrlFor = (baseUrl: string): string | undefined => {
47
55
  return undefined;
48
56
  };
49
57
 
50
- const missingApiKeyMessage = [
51
- "Missing API key. Run toloka login once to store credentials for this machine:",
58
+ export const MISSING_CREDENTIALS_MESSAGE = [
59
+ "Not logged in. Run toloka login once to sign in through your browser on this machine:",
52
60
  "",
53
61
  " toloka login",
54
62
  "",
55
- "It explains how to generate an API key and where it is saved. To override per command,",
56
- "pass --api-key or set TOLOKA_API_KEY. Run toloka auth-status to see what is in effect.",
63
+ "Automation can pass --api-key, set TOLOKA_API_KEY, or pipe a key to toloka login --api-key-stdin.",
64
+ "Run toloka auth-status to see what is in effect.",
57
65
  ].join("\n");
58
66
 
67
+ export const resolveApiKey = (
68
+ flags: TolokaCliFlags,
69
+ environment: TolokaEnvironment,
70
+ stored?: StoredCredentials,
71
+ ): string | undefined => flags.apiKey ?? environment.TOLOKA_API_KEY ?? stored?.apiKey;
72
+
73
+ export const resolveBaseUrl = (
74
+ flags: TolokaCliFlags,
75
+ environment: TolokaEnvironment,
76
+ stored?: StoredCredentials,
77
+ ): string =>
78
+ flags.baseUrl ??
79
+ environment.TOLOKA_BASE_URL ??
80
+ resolveInternalBaseUrl(environment) ??
81
+ stored?.baseUrl ??
82
+ PRODUCTION_BASE_URL;
83
+
59
84
  export function resolveTolokaConfig(
60
85
  flags: TolokaCliFlags,
61
86
  state: TolokaState | undefined,
62
87
  environment: TolokaEnvironment,
63
88
  stored?: StoredCredentials,
89
+ browserLogin?: BrowserLoginProvider,
64
90
  ): ResolvedTolokaConfig {
65
- const apiKey = flags.apiKey ?? environment.TOLOKA_API_KEY ?? stored?.apiKey;
66
- const baseUrl =
67
- flags.baseUrl ??
68
- environment.TOLOKA_BASE_URL ??
69
- resolveInternalBaseUrl(environment) ??
70
- stored?.baseUrl ??
71
- PRODUCTION_BASE_URL;
91
+ const apiKey = resolveApiKey(flags, environment, stored);
92
+ const baseUrl = resolveBaseUrl(flags, environment, stored);
72
93
  const project = resolveProjectContext({
73
94
  flags,
74
95
  state,
@@ -76,6 +97,12 @@ export function resolveTolokaConfig(
76
97
  stored,
77
98
  effectiveBaseUrl: baseUrl,
78
99
  });
79
- assertCli(apiKey, missingApiKeyMessage, { hint: { disabled: true } });
80
- return { apiKey, baseUrl, ...(project === undefined ? {} : { projectId: project.id }) };
100
+ const credential: CliCredential | undefined =
101
+ apiKey !== undefined
102
+ ? { type: "apiKey", apiKey }
103
+ : browserLogin !== undefined
104
+ ? { type: "browser", provider: browserLogin }
105
+ : undefined;
106
+ assertCli(credential, MISSING_CREDENTIALS_MESSAGE, { hint: { disabled: true } });
107
+ return { baseUrl, credential, ...(project === undefined ? {} : { projectId: project.id }) };
81
108
  }
@@ -1,14 +1,17 @@
1
1
  import { InvalidArgumentError, type Command } from "commander";
2
+ import type { Style } from "./style";
2
3
  import { resolve } from "node:path";
3
4
  import { createClient } from "../client";
4
5
  import type { PotatoClient } from "../client";
5
6
  import { readTolokaState } from "../state";
6
7
  import type { TolokaState } from "../state";
7
- import type { AuthPrompt } from "./auth";
8
+ import type { AuthChoose, AuthPrompt } from "./auth";
8
9
  import type { CliAnalytics } from "./analytics";
10
+ import type { BrowserLoginFlow } from "./browser-login";
11
+ import { createBrowserSessionAuth, findBrowserLogin } from "./browser-session";
9
12
  import type { checkProject } from "./check";
10
- import { PRODUCTION_BASE_URL, resolveTolokaConfig } from "./config";
11
- import type { TolokaEnvironment } from "./config";
13
+ import { PRODUCTION_BASE_URL, resolveApiKey, resolveBaseUrl, resolveTolokaConfig } from "./config";
14
+ import type { ResolvedTolokaConfig, TolokaEnvironment } from "./config";
12
15
  import { findCommandCredentials } from "./credentials";
13
16
  import type { StoredCredentials } from "./credentials";
14
17
  import { resolveProjectContext } from "./project-context";
@@ -21,13 +24,16 @@ export type CliIo = {
21
24
  readonly stderr: (text: string) => void;
22
25
  readonly readStdin: () => Promise<string>;
23
26
  readonly prompt?: AuthPrompt;
27
+ readonly choose?: AuthChoose;
24
28
  readonly select?: (choices: readonly { name: string; value: string }[]) => Promise<string>;
29
+ readonly style?: Style;
25
30
  };
26
31
 
27
32
  export type ProgramContext = {
28
33
  readonly environment: TolokaEnvironment;
29
34
  readonly io: CliIo;
30
35
  readonly injectedClient?: PotatoClient;
36
+ readonly browserLogin?: BrowserLoginFlow;
31
37
  readonly runChecks?: typeof checkProject;
32
38
  readonly analytics?: CliAnalytics;
33
39
  };
@@ -57,7 +63,8 @@ export const CREDENTIAL_PRECEDENCE = [
57
63
  "Credentials resolve in this order, highest first:",
58
64
  " 1. --api-key / --base-url",
59
65
  " 2. TOLOKA_API_KEY / TOLOKA_BASE_URL",
60
- " 3. the file written by toloka login",
66
+ " 3. the API key stored by toloka login --api-key-stdin",
67
+ " 4. the browser login stored by toloka login",
61
68
  `The platform defaults to ${PRODUCTION_BASE_URL}.`,
62
69
  ].join("\n");
63
70
 
@@ -96,10 +103,15 @@ export const trainingLoraRank = (raw: string): 8 | 16 | 32 => {
96
103
  return value;
97
104
  };
98
105
 
99
- export const trainingGistTokens = (raw: string): 8 | 32 | 64 => {
106
+ export const trainingGistTokens = (raw: string): 8 | 32 | 64 | "1/4" | "1/8" | "1/16" | "1/32" => {
107
+ if (raw === "1/4" || raw === "1/8" || raw === "1/16" || raw === "1/32") {
108
+ return raw;
109
+ }
100
110
  const value = Number(raw);
101
111
  if (value !== 8 && value !== 32 && value !== 64) {
102
- throw new InvalidArgumentError("Expected 8, 32, or 64 gist tokens.");
112
+ throw new InvalidArgumentError(
113
+ 'Expected 8, 32, or 64 gist tokens, or a fraction of the prompt: "1/4", "1/8", "1/16", or "1/32".',
114
+ );
103
115
  }
104
116
  return value;
105
117
  };
@@ -209,6 +221,32 @@ export const identifyClientUser = async (
209
221
  } catch {}
210
222
  };
211
223
 
224
+ export const resolveClient = async (
225
+ globals: GlobalOptions,
226
+ context: ProgramContext,
227
+ state: TolokaState | undefined,
228
+ stored: StoredCredentials | undefined,
229
+ ): Promise<{ readonly config: ResolvedTolokaConfig; readonly client: PotatoClient }> => {
230
+ const { environment } = context;
231
+ const browserLogin =
232
+ resolveApiKey(globals, environment, stored) === undefined
233
+ ? (await findBrowserLogin(resolveBaseUrl(globals, environment, stored), environment))?.provider
234
+ : undefined;
235
+ const config = resolveTolokaConfig(globals, state, environment, stored, browserLogin);
236
+ const { baseUrl, credential } = config;
237
+ const client =
238
+ context.injectedClient ??
239
+ createClient(
240
+ credential.type === "apiKey"
241
+ ? { baseUrl, apiKey: credential.apiKey }
242
+ : {
243
+ baseUrl,
244
+ accessToken: createBrowserSessionAuth({ provider: credential.provider, baseUrl, environment }),
245
+ },
246
+ );
247
+ return { config, client };
248
+ };
249
+
212
250
  export const resolveContext = async (
213
251
  globals: GlobalOptions,
214
252
  context: ProgramContext,
@@ -216,9 +254,7 @@ export const resolveContext = async (
216
254
  const sourceRoot = await resolveSourceRoot(globals, context.io);
217
255
  const state = await readTolokaState(sourceRoot);
218
256
  const stored = await readCommandCredentials(globals, context);
219
- const config = resolveTolokaConfig(globals, state, context.environment, stored);
220
- const client =
221
- context.injectedClient ?? createClient({ apiKey: config.apiKey, baseUrl: config.baseUrl });
257
+ const { config, client } = await resolveClient(globals, context, state, stored);
222
258
  await identifyClientUser(client, context.analytics);
223
259
 
224
260
  return {
@@ -11,7 +11,7 @@ export type StoredProjectAlias = {
11
11
 
12
12
  export type StoredCredentials = {
13
13
  readonly baseUrl: string;
14
- readonly apiKey: string;
14
+ readonly apiKey?: string;
15
15
  readonly current?: string;
16
16
  readonly projects?: Readonly<Record<string, StoredProjectAlias>>;
17
17
  };
@@ -132,15 +132,16 @@ const asCredentials = (payload: unknown): StoredCredentials | undefined => {
132
132
  const baseUrl = readField(payload, "baseUrl");
133
133
  const apiKey = readField(payload, "apiKey");
134
134
  if (typeof baseUrl !== "string" || baseUrl.length === 0) return undefined;
135
- if (typeof apiKey !== "string" || apiKey.length === 0) return undefined;
135
+ if (apiKey !== undefined && (typeof apiKey !== "string" || apiKey.length === 0)) return undefined;
136
+ const identity = apiKey === undefined ? { baseUrl } : { baseUrl, apiKey };
136
137
 
137
138
  const currentField = readField(payload, "current");
138
139
  const projectsField = readField(payload, "projects");
139
- if (currentField === undefined && projectsField === undefined) return { baseUrl, apiKey };
140
+ if (currentField === undefined && projectsField === undefined) return identity;
140
141
 
141
142
  const registry = asProjectRegistry(projectsField, currentField);
142
143
  if (registry === undefined) return undefined;
143
- return { baseUrl, apiKey, ...registry };
144
+ return { ...identity, ...registry };
144
145
  };
145
146
 
146
147
  export async function inspectCredentialsFile(path: string): Promise<InspectedCredentialsFile> {
@@ -223,7 +224,7 @@ const serializedCredentials = (credentials: StoredCredentials): StoredCredential
223
224
  const hasProjects = projects !== undefined && Object.keys(projects).length > 0;
224
225
  return {
225
226
  baseUrl: credentials.baseUrl,
226
- apiKey: credentials.apiKey,
227
+ ...(credentials.apiKey === undefined ? {} : { apiKey: credentials.apiKey }),
227
228
  ...(credentials.current === undefined ? {} : { current: credentials.current }),
228
229
  ...(hasProjects ? { projects } : {}),
229
230
  };
@@ -274,18 +275,17 @@ const fileExists = async (path: string): Promise<boolean> => {
274
275
  }
275
276
  };
276
277
 
278
+ export const storedCredentialsPaths = (environment: CliEnvironment = process.env): string[] => [
279
+ credentialsPath(environment),
280
+ ...(environment.TOLOKA_CREDENTIALS_FILE === undefined ? [legacyCredentialsPath(environment)] : []),
281
+ ];
282
+
277
283
  export async function clearStoredCredentials(
278
284
  environment: CliEnvironment = process.env,
279
285
  ): Promise<string[]> {
280
286
  const removed: string[] = [];
281
- const paths = [
282
- credentialsPath(environment),
283
- ...(environment.TOLOKA_CREDENTIALS_FILE === undefined
284
- ? [legacyCredentialsPath(environment)]
285
- : []),
286
- ];
287
-
288
- for (const path of paths) {
287
+
288
+ for (const path of storedCredentialsPaths(environment)) {
289
289
  if (!(await fileExists(path))) continue;
290
290
  await rm(path, { force: true });
291
291
  removed.push(path);
package/src/cli/init.ts CHANGED
@@ -223,7 +223,12 @@ const bindCheckout = async (
223
223
  assertCli(
224
224
  existing === undefined || existing.projectId === projectId,
225
225
  `This directory is already bound to ${existing?.projectId}.`,
226
- { hint: { description: "Run toloka init without --project-id to keep the current binding, or remove toloka.json to bind another project." } },
226
+ {
227
+ hint: {
228
+ description:
229
+ "Run toloka init without --project-id to keep the current binding, or remove toloka.json to bind another project.",
230
+ },
231
+ },
227
232
  );
228
233
  if (existing !== undefined) return undefined;
229
234
  await writeTolokaState(targetRoot, { projectId, pipelines: {}, datasets: {} });
@@ -275,7 +280,7 @@ export async function initProject(
275
280
  ];
276
281
  const warning = dependencyReady
277
282
  ? undefined
278
- : `Warning: ${SDK_PACKAGE_NAME} is not listed in dependencies or devDependencies in ${join(targetRoot, "package.json")}. Run \`bun add --dev ${SDK_PACKAGE_NAME}\` before using \`toloka\`, then re-run \`toloka init\`.`;
283
+ : `Warning: ${SDK_PACKAGE_NAME} is not listed in dependencies or devDependencies in ${join(targetRoot, "package.json")}. Install it with npm or Bun before authoring pipelines, then re-run \`toloka init\`. Training commands do not require it.`;
279
284
  return {
280
285
  targetDirectory: targetRoot,
281
286
  files,