@toloka-platform/sdk 0.1.0-preview.3 → 0.1.0-preview.5

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 (38) hide show
  1. package/README.md +2 -2
  2. package/bin/toloka.js +7 -0
  3. package/dist/cli/toloka.js +119 -114
  4. package/dist/cli/toloka.js.jsc +0 -0
  5. package/dist/cli/toloka.js.jsc.sha256 +1 -1
  6. package/dist/cli/toloka.js.sha256 +1 -1
  7. package/docs/cli-v2.md +4 -0
  8. package/docs/cli.md +56 -5
  9. package/docs/getting-started.md +3 -3
  10. package/docs/limitations.md +1 -1
  11. package/openapi/gen/api.ts +22 -0
  12. package/openapi/gen/models/createDatasetBody.ts +2 -0
  13. package/openapi/gen/models/createDatasetBodyProfile.ts +2 -0
  14. package/openapi/gen/models/createDatasetUploadBodyFormat.ts +1 -1
  15. package/openapi/gen/models/dataset.ts +2 -0
  16. package/openapi/gen/models/datasetProfile.ts +3 -0
  17. package/openapi/gen/models/datasetProfileStamp.ts +6 -0
  18. package/openapi/gen/models/datasetProfileStampName.ts +2 -0
  19. package/openapi/gen/models/index.ts +5 -0
  20. package/openapi/gen/models/runCost.ts +7 -0
  21. package/package.json +4 -2
  22. package/skills/catalog.json +22 -0
  23. package/skills/{SKILL.md → toloka-build/SKILL.md} +1 -1
  24. package/skills/{references → toloka-build/references}/workflow-and-cli.md +14 -4
  25. package/skills/toloka-report-issue/SKILL.md +25 -0
  26. package/skills/toloka-report-issue/references/report-template.md +34 -0
  27. package/src/authoring/index.ts +216 -171
  28. package/src/cli/analytics.ts +53 -1
  29. package/src/cli/auth.ts +5 -5
  30. package/src/cli/credentials.ts +8 -2
  31. package/src/cli/file-system.ts +34 -0
  32. package/src/cli/format-error.ts +19 -0
  33. package/src/cli/init.ts +149 -194
  34. package/src/cli/program.ts +103 -21
  35. package/src/cli/report.ts +144 -0
  36. package/src/cli/skill.ts +363 -0
  37. package/src/pipeline-graph-v1.contract.json +5 -1
  38. /package/skills/{references → toloka-build/references}/node-semantics.md +0 -0
@@ -1,3 +1,4 @@
1
+ import { assert } from "es-toolkit";
1
2
  import { randomUUID } from "node:crypto";
2
3
  import { chmod, readFile, writeFile } from "node:fs/promises";
3
4
  import { dirname, join } from "node:path";
@@ -28,13 +29,22 @@ export type CliAnalyticsCapture = {
28
29
  readonly error?: unknown;
29
30
  };
30
31
 
32
+ export type IssueReportSubmission = {
33
+ readonly reportId: string;
34
+ readonly reportText: string;
35
+ readonly authoredBy: "human" | "agent";
36
+ readonly platform: string;
37
+ };
38
+
31
39
  export type CliAnalytics = {
32
40
  readonly shouldIdentify: boolean;
41
+ readonly issueReportingEnabled: boolean;
33
42
  readonly identifyCurrentUser: (user: {
34
43
  readonly id: string;
35
44
  readonly email: string | null;
36
45
  }) => void;
37
46
  readonly captureCommand: (event: CliAnalyticsCapture) => void;
47
+ readonly submitIssueReport: (report: IssueReportSubmission) => Promise<void>;
38
48
  readonly shutdown: () => Promise<void>;
39
49
  };
40
50
 
@@ -46,6 +56,8 @@ export type PosthogLike = {
46
56
  disableGeoip: boolean;
47
57
  }) => void;
48
58
  alias: (event: { distinctId: string; alias: string }) => void;
59
+ flush: () => Promise<void>;
60
+ on?: (event: "error", listener: (error: Error) => void) => void;
49
61
  shutdown: (shutdownTimeoutMs?: number) => Promise<void>;
50
62
  };
51
63
 
@@ -86,6 +98,7 @@ const COMMAND_GROUPS: Readonly<Record<string, Readonly<Record<string, true>>>> =
86
98
  },
87
99
  dataset: { list: true, fields: true, load: true, upload: true },
88
100
  secret: { set: true },
101
+ skill: { list: true, search: true, install: true },
89
102
  };
90
103
 
91
104
  const ROOT_COMMANDS: Readonly<Record<string, true>> = {
@@ -99,6 +112,8 @@ const ROOT_COMMANDS: Readonly<Record<string, true>> = {
99
112
  plan: true,
100
113
  status: true,
101
114
  validate: true,
115
+ report: true,
116
+ version: true,
102
117
  whoami: true,
103
118
  };
104
119
 
@@ -116,6 +131,7 @@ const nameFromTokens = (tokens: readonly string[]): string => {
116
131
  };
117
132
 
118
133
  export function commandNameFromArgv(argv: readonly string[]): string {
134
+ if (argv.includes("-v") || argv.includes("--version")) return "version";
119
135
  const tokens: string[] = [];
120
136
  for (let index = 0; index < argv.length; index += 1) {
121
137
  const token = argv[index];
@@ -161,8 +177,12 @@ const readAnonymousId = async (environment: CliEnvironment): Promise<string> =>
161
177
 
162
178
  const noopAnalytics = (): CliAnalytics => ({
163
179
  shouldIdentify: false,
180
+ issueReportingEnabled: false,
164
181
  identifyCurrentUser() {},
165
182
  captureCommand() {},
183
+ async submitIssueReport() {
184
+ assert(false, "Issue reporting is disabled. Re-run with TOLOKA_TELEMETRY=1.");
185
+ },
166
186
  async shutdown() {},
167
187
  });
168
188
 
@@ -177,6 +197,11 @@ export function createCliAnalytics(options: {
177
197
  let client = options.client;
178
198
  let distinctId = options.distinctId;
179
199
  let identifiedUserId: string | undefined;
200
+ const deliveryErrors: Error[] = [];
201
+ const captureDeliveryError = (error: Error) => {
202
+ deliveryErrors.push(error);
203
+ };
204
+ client?.on?.("error", captureDeliveryError);
180
205
 
181
206
  const ensure = async () => {
182
207
  if (client !== undefined && distinctId !== undefined) return;
@@ -186,7 +211,7 @@ export function createCliAnalytics(options: {
186
211
  flushAt: 1,
187
212
  flushInterval: 0,
188
213
  });
189
- created.on("error", () => {});
214
+ created.on("error", captureDeliveryError);
190
215
  client = created;
191
216
  }
192
217
  distinctId ??= await readAnonymousId(environment);
@@ -202,6 +227,7 @@ export function createCliAnalytics(options: {
202
227
  let queue: Promise<void> = ensure().catch(() => {});
203
228
 
204
229
  return {
230
+ issueReportingEnabled: true,
205
231
  shouldIdentify: true,
206
232
  identifyCurrentUser(user) {
207
233
  queue = queue
@@ -252,6 +278,32 @@ export function createCliAnalytics(options: {
252
278
  })
253
279
  .catch(() => {});
254
280
  },
281
+ submitIssueReport(report) {
282
+ const submission = queue.then(async () => {
283
+ assert(client && distinctId, "PostHog issue reporting could not be initialized.");
284
+ deliveryErrors.length = 0;
285
+ client.capture({
286
+ distinctId,
287
+ event: "sdk_cli_issue_report_submitted",
288
+ disableGeoip: true,
289
+ properties: {
290
+ ...sharedProperties(),
291
+ report_id: report.reportId,
292
+ report_text: report.reportText,
293
+ report_length_bytes: new TextEncoder().encode(report.reportText).length,
294
+ authored_by: report.authoredBy,
295
+ platform: report.platform,
296
+ $process_person_profile: false,
297
+ },
298
+ });
299
+ await client.flush();
300
+ const reportError = deliveryErrors[0];
301
+ deliveryErrors.length = 0;
302
+ if (reportError !== undefined) throw reportError;
303
+ });
304
+ queue = submission.catch(() => {});
305
+ return submission;
306
+ },
255
307
  async shutdown() {
256
308
  try {
257
309
  await queue;
package/src/cli/auth.ts CHANGED
@@ -74,12 +74,12 @@ export type LoginResult = {
74
74
  export async function login(options: LoginOptions = {}): Promise<LoginResult> {
75
75
  const environment = options.environment ?? process.env;
76
76
  const write = options.write ?? (() => undefined);
77
- const baseUrl = (
77
+ const baseUrl = normalizeBaseUrl(
78
78
  options.baseUrl ??
79
- environment.POTATO_BASE_URL ??
80
- resolveInternalBaseUrl(environment) ??
81
- PRODUCTION_BASE_URL
82
- ).replace(/\/+$/u, "");
79
+ environment.POTATO_BASE_URL ??
80
+ resolveInternalBaseUrl(environment) ??
81
+ PRODUCTION_BASE_URL,
82
+ );
83
83
 
84
84
  let apiKey = options.apiKey;
85
85
  if (apiKey === undefined) {
@@ -52,13 +52,19 @@ export const credentialsPath = (environment: CliEnvironment = process.env): stri
52
52
  export const legacyCredentialsPath = (environment: CliEnvironment = process.env): string =>
53
53
  join(home(environment), ".toloka", "credentials.json");
54
54
 
55
+ const removeTrailingSlashes = (value: string): string => {
56
+ let end = value.length;
57
+ while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1;
58
+ return end === value.length ? value : value.slice(0, end);
59
+ };
60
+
55
61
  export const normalizeBaseUrl = (baseUrl: string): string => {
56
- const trimmed = baseUrl.trim().replace(/\/+$/u, "");
62
+ const trimmed = removeTrailingSlashes(baseUrl.trim());
57
63
  try {
58
64
  const url = new URL(trimmed);
59
65
  url.hash = "";
60
66
  url.search = "";
61
- return url.toString().replace(/\/+$/u, "");
67
+ return removeTrailingSlashes(url.toString());
62
68
  } catch {
63
69
  return trimmed;
64
70
  }
@@ -0,0 +1,34 @@
1
+ import { assert } from "es-toolkit";
2
+ import { lstat } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+
5
+ export const hasErrorCode = (error: unknown, ...codes: readonly string[]) =>
6
+ error instanceof Error &&
7
+ "code" in error &&
8
+ typeof error.code === "string" &&
9
+ codes.includes(error.code);
10
+
11
+ export const isMissingFileError = (error: unknown) => hasErrorCode(error, "ENOENT");
12
+
13
+ export const entryStats = async (path: string) => {
14
+ try {
15
+ return await lstat(path);
16
+ } catch (error) {
17
+ if (isMissingFileError(error)) return undefined;
18
+ throw error;
19
+ }
20
+ };
21
+
22
+ export const assertSafeDestination = async (
23
+ targetDirectory: string,
24
+ relativePath: string,
25
+ action = "write",
26
+ ) => {
27
+ let currentPath = targetDirectory;
28
+ for (const segment of relativePath.split("/")) {
29
+ currentPath = join(currentPath, segment);
30
+ const entry = await entryStats(currentPath);
31
+ if (entry === undefined) return;
32
+ assert(!entry.isSymbolicLink(), `Refusing to ${action} through symbolic link ${currentPath}.`);
33
+ }
34
+ };
@@ -0,0 +1,19 @@
1
+ import { PotatoApiError } from "../errors";
2
+
3
+ export const apiErrorDetails = (error: PotatoApiError): string | undefined => {
4
+ const details =
5
+ typeof error.body === "string"
6
+ ? error.body
7
+ : error.body === undefined
8
+ ? undefined
9
+ : JSON.stringify(error.body);
10
+ return details === undefined || details.length === 0 ? undefined : details;
11
+ };
12
+
13
+ export const formatCliError = (error: unknown): string => {
14
+ if (error instanceof PotatoApiError) {
15
+ const details = apiErrorDetails(error);
16
+ return details === undefined ? error.message : `${error.message}\n${details}`;
17
+ }
18
+ return error instanceof Error ? error.message : String(error);
19
+ };