@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.
- package/README.md +2 -2
- package/bin/toloka.js +7 -0
- package/dist/cli/toloka.js +119 -114
- package/dist/cli/toloka.js.jsc +0 -0
- package/dist/cli/toloka.js.jsc.sha256 +1 -1
- package/dist/cli/toloka.js.sha256 +1 -1
- package/docs/cli-v2.md +4 -0
- package/docs/cli.md +56 -5
- package/docs/getting-started.md +3 -3
- package/docs/limitations.md +1 -1
- package/openapi/gen/api.ts +22 -0
- package/openapi/gen/models/createDatasetBody.ts +2 -0
- package/openapi/gen/models/createDatasetBodyProfile.ts +2 -0
- package/openapi/gen/models/createDatasetUploadBodyFormat.ts +1 -1
- package/openapi/gen/models/dataset.ts +2 -0
- package/openapi/gen/models/datasetProfile.ts +3 -0
- package/openapi/gen/models/datasetProfileStamp.ts +6 -0
- package/openapi/gen/models/datasetProfileStampName.ts +2 -0
- package/openapi/gen/models/index.ts +5 -0
- package/openapi/gen/models/runCost.ts +7 -0
- package/package.json +4 -2
- package/skills/catalog.json +22 -0
- package/skills/{SKILL.md → toloka-build/SKILL.md} +1 -1
- package/skills/{references → toloka-build/references}/workflow-and-cli.md +14 -4
- package/skills/toloka-report-issue/SKILL.md +25 -0
- package/skills/toloka-report-issue/references/report-template.md +34 -0
- package/src/authoring/index.ts +216 -171
- package/src/cli/analytics.ts +53 -1
- package/src/cli/auth.ts +5 -5
- package/src/cli/credentials.ts +8 -2
- package/src/cli/file-system.ts +34 -0
- package/src/cli/format-error.ts +19 -0
- package/src/cli/init.ts +149 -194
- package/src/cli/program.ts +103 -21
- package/src/cli/report.ts +144 -0
- package/src/cli/skill.ts +363 -0
- package/src/pipeline-graph-v1.contract.json +5 -1
- /package/skills/{references → toloka-build/references}/node-semantics.md +0 -0
package/src/cli/program.ts
CHANGED
|
@@ -4,14 +4,13 @@ import { resolve } from "node:path";
|
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
5
5
|
import { createClient } from "../client";
|
|
6
6
|
import type { PotatoClient } from "../client";
|
|
7
|
-
import { PotatoApiError } from "../errors";
|
|
8
7
|
import { readTolokaState } from "../state";
|
|
9
8
|
import type { TolokaState } from "../state";
|
|
10
9
|
import { authStatus, login, logout } from "./auth";
|
|
11
10
|
import type { AuthPrompt } from "./auth";
|
|
12
11
|
import { commandNameFromArgv, createCliAnalytics } from "./analytics";
|
|
13
12
|
import type { CliAnalytics, CliAnalyticsCommandOutcome } from "./analytics";
|
|
14
|
-
import { PRODUCTION_BASE_URL, resolveTolokaConfig } from "./config";
|
|
13
|
+
import { PRODUCTION_BASE_URL, resolveInternalBaseUrl, resolveTolokaConfig } from "./config";
|
|
15
14
|
import type { TolokaEnvironment } from "./config";
|
|
16
15
|
import {
|
|
17
16
|
findCommandCredentials,
|
|
@@ -26,7 +25,7 @@ import {
|
|
|
26
25
|
setProjectSecret,
|
|
27
26
|
uploadDataset,
|
|
28
27
|
} from "./data";
|
|
29
|
-
import { initProject, readAgentSkillStalenessWarning } from "./init";
|
|
28
|
+
import { SDK_VERSION, initProject, readAgentSkillStalenessWarning } from "./init";
|
|
30
29
|
import {
|
|
31
30
|
applyProject,
|
|
32
31
|
importProject,
|
|
@@ -45,6 +44,9 @@ import {
|
|
|
45
44
|
switchProject,
|
|
46
45
|
} from "./project-context";
|
|
47
46
|
import { estimatePipelineCost } from "./cost";
|
|
47
|
+
import { submitIssueReport } from "./report";
|
|
48
|
+
import type { IssueReportAuthor } from "./report";
|
|
49
|
+
import { formatCliError } from "./format-error";
|
|
48
50
|
import {
|
|
49
51
|
cancelPipelineRun,
|
|
50
52
|
listLivePipelines,
|
|
@@ -56,6 +58,7 @@ import {
|
|
|
56
58
|
} from "./runs";
|
|
57
59
|
|
|
58
60
|
import type { RunOutcome } from "./runs";
|
|
61
|
+
import { skillInstallCommand, skillListCommand, skillSearchCommand } from "./skill";
|
|
59
62
|
import { findSourceRoot } from "./source";
|
|
60
63
|
|
|
61
64
|
export type CliIo = {
|
|
@@ -88,6 +91,12 @@ const CREDENTIAL_PRECEDENCE = [
|
|
|
88
91
|
`The platform defaults to ${PRODUCTION_BASE_URL}.`,
|
|
89
92
|
].join("\n");
|
|
90
93
|
|
|
94
|
+
const authoredByOption = (raw: string): IssueReportAuthor => {
|
|
95
|
+
if (raw !== "human" && raw !== "agent")
|
|
96
|
+
throw new InvalidArgumentError('Expected "human" or "agent".');
|
|
97
|
+
return raw;
|
|
98
|
+
};
|
|
99
|
+
|
|
91
100
|
const positiveInteger = (raw: string): number => {
|
|
92
101
|
const value = Number(raw);
|
|
93
102
|
if (!Number.isInteger(value) || value <= 0)
|
|
@@ -104,6 +113,7 @@ const exitCodeForOutcome = (outcome: RunOutcome): number => {
|
|
|
104
113
|
type Resolved = {
|
|
105
114
|
readonly sourceRoot: string;
|
|
106
115
|
readonly client: PotatoClient;
|
|
116
|
+
readonly baseUrl: string;
|
|
107
117
|
readonly state: TolokaState | undefined;
|
|
108
118
|
readonly stored: StoredCredentials | undefined;
|
|
109
119
|
readonly projectId?: string;
|
|
@@ -154,6 +164,7 @@ const resolveContext = async (
|
|
|
154
164
|
return {
|
|
155
165
|
sourceRoot,
|
|
156
166
|
client,
|
|
167
|
+
baseUrl: config.baseUrl,
|
|
157
168
|
state,
|
|
158
169
|
stored,
|
|
159
170
|
...(config.projectId === undefined ? {} : { projectId: config.projectId }),
|
|
@@ -187,6 +198,7 @@ export function createProgram(context: ProgramContext): CreatedProgram {
|
|
|
187
198
|
.description(
|
|
188
199
|
"Preview. Build and operate Toloka pipeline projects from TypeScript source. The public API may change.",
|
|
189
200
|
)
|
|
201
|
+
.version(SDK_VERSION, "-v, --version", "output the current version")
|
|
190
202
|
.option(
|
|
191
203
|
"--source-root <directory>",
|
|
192
204
|
"project root; discovered from the working directory when omitted",
|
|
@@ -203,19 +215,30 @@ export function createProgram(context: ProgramContext): CreatedProgram {
|
|
|
203
215
|
|
|
204
216
|
const globals = (): GlobalOptions => program.opts<GlobalOptions>();
|
|
205
217
|
|
|
206
|
-
program.hook("postAction", async (
|
|
218
|
+
program.hook("postAction", async (_thisCommand, actionCommand) => {
|
|
207
219
|
if (warnedAboutStaleSkills) return;
|
|
208
220
|
const commandNames = [];
|
|
209
|
-
for (let current: Command | null =
|
|
221
|
+
for (let current: Command | null = actionCommand; current !== null; current = current.parent) {
|
|
210
222
|
commandNames.push(current.name());
|
|
211
223
|
}
|
|
212
|
-
if (
|
|
224
|
+
if (
|
|
225
|
+
commandNames.includes("init") ||
|
|
226
|
+
commandNames.includes("help") ||
|
|
227
|
+
commandNames.includes("report") ||
|
|
228
|
+
commandNames.includes("url") ||
|
|
229
|
+
commandNames.includes("version")
|
|
230
|
+
)
|
|
231
|
+
return;
|
|
213
232
|
const sourceRoot = await resolveSourceRoot(globals(), context.io);
|
|
214
233
|
const warning = await readAgentSkillStalenessWarning(sourceRoot).catch(() => undefined);
|
|
215
234
|
if (warning === undefined) return;
|
|
216
235
|
warnedAboutStaleSkills = true;
|
|
217
236
|
context.io.stderr(warning);
|
|
218
237
|
});
|
|
238
|
+
program
|
|
239
|
+
.command("version")
|
|
240
|
+
.description("print the current CLI version")
|
|
241
|
+
.action(() => context.io.stdout(SDK_VERSION));
|
|
219
242
|
|
|
220
243
|
program
|
|
221
244
|
.command("login")
|
|
@@ -351,6 +374,19 @@ export function createProgram(context: ProgramContext): CreatedProgram {
|
|
|
351
374
|
});
|
|
352
375
|
if (result.project === undefined) exitCode.value = 1;
|
|
353
376
|
});
|
|
377
|
+
project
|
|
378
|
+
.command("url")
|
|
379
|
+
.description("print the current project URL")
|
|
380
|
+
.action(async () => {
|
|
381
|
+
const { baseUrl, client, projectId } = await resolveContext(globals(), context);
|
|
382
|
+
assert(projectId, MISSING_PROJECT_ID_MESSAGE);
|
|
383
|
+
const live = await client.projects.get({ projectId });
|
|
384
|
+
const url = new URL(
|
|
385
|
+
`/teams/${encodeURIComponent(live.teamId)}/projects/${encodeURIComponent(live.id)}`,
|
|
386
|
+
baseUrl,
|
|
387
|
+
);
|
|
388
|
+
context.io.stdout(url.href);
|
|
389
|
+
});
|
|
354
390
|
|
|
355
391
|
project
|
|
356
392
|
.command("remove")
|
|
@@ -390,7 +426,7 @@ export function createProgram(context: ProgramContext): CreatedProgram {
|
|
|
390
426
|
|
|
391
427
|
program
|
|
392
428
|
.command("init")
|
|
393
|
-
.description("initialize this project directory and install agent
|
|
429
|
+
.description("initialize this project directory and install the agent bootstrap")
|
|
394
430
|
.action(async () => {
|
|
395
431
|
const flags = globals();
|
|
396
432
|
const sourceRoot = resolveInitRoot(flags);
|
|
@@ -677,6 +713,66 @@ export function createProgram(context: ProgramContext): CreatedProgram {
|
|
|
677
713
|
const result = await projectStatus(client, { sourceRoot, projectId });
|
|
678
714
|
context.io.stdout(result.output);
|
|
679
715
|
});
|
|
716
|
+
program
|
|
717
|
+
.command("report")
|
|
718
|
+
.argument("<markdownPath>", "path to a UTF-8 Markdown report")
|
|
719
|
+
.option("--authored-by <who>", "human or agent", authoredByOption, "human")
|
|
720
|
+
.option("--confirm", "confirm submission in a non-interactive environment", false)
|
|
721
|
+
.description("preview and send an issue report to Toloka through PostHog")
|
|
722
|
+
.action(
|
|
723
|
+
async (
|
|
724
|
+
markdownPath: string,
|
|
725
|
+
options: { authoredBy: IssueReportAuthor; confirm: boolean },
|
|
726
|
+
) => {
|
|
727
|
+
assert(context.analytics, "Issue reporting requires CLI analytics.");
|
|
728
|
+
const sourceRoot = resolveInitRoot(globals());
|
|
729
|
+
const baseUrl =
|
|
730
|
+
globals().baseUrl ??
|
|
731
|
+
context.environment.POTATO_BASE_URL ??
|
|
732
|
+
resolveInternalBaseUrl(context.environment) ??
|
|
733
|
+
PRODUCTION_BASE_URL;
|
|
734
|
+
exitCode.value = await submitIssueReport({
|
|
735
|
+
path: markdownPath,
|
|
736
|
+
authoredBy: options.authoredBy,
|
|
737
|
+
platform: new URL(baseUrl).origin,
|
|
738
|
+
confirm: options.confirm,
|
|
739
|
+
sourceRoot,
|
|
740
|
+
cwd: process.cwd(),
|
|
741
|
+
analytics: context.analytics,
|
|
742
|
+
io: context.io,
|
|
743
|
+
});
|
|
744
|
+
},
|
|
745
|
+
);
|
|
746
|
+
|
|
747
|
+
const skill = program
|
|
748
|
+
.command("skill")
|
|
749
|
+
.description("discover bundled agent skills and install them into .agents/skills");
|
|
750
|
+
skill
|
|
751
|
+
.command("list")
|
|
752
|
+
.option("--json", "print Skill Descriptors with their status as JSON", false)
|
|
753
|
+
.description("list bundled skills as not-installed, current, or stale")
|
|
754
|
+
.action(async (options: { json: boolean }) => {
|
|
755
|
+
const sourceRoot = await resolveSourceRoot(globals(), context.io);
|
|
756
|
+
await skillListCommand({ sourceRoot, json: options.json, io: context.io });
|
|
757
|
+
});
|
|
758
|
+
skill
|
|
759
|
+
.command("search")
|
|
760
|
+
.argument("<query>", "text matched case-insensitively against id, name, description, and tags")
|
|
761
|
+
.option("--json", "print matching Skill Descriptors with their status as JSON", false)
|
|
762
|
+
.description("find bundled skills for a task")
|
|
763
|
+
.action(async (query: string, options: { json: boolean }) => {
|
|
764
|
+
const sourceRoot = await resolveSourceRoot(globals(), context.io);
|
|
765
|
+
await skillSearchCommand({ sourceRoot, query, json: options.json, io: context.io });
|
|
766
|
+
});
|
|
767
|
+
skill
|
|
768
|
+
.command("install")
|
|
769
|
+
.argument("<skillId>", "skill id from toloka skill list")
|
|
770
|
+
.option("-f, --force", "replace an existing .agents/skills/<skillId> directory", false)
|
|
771
|
+
.description("copy one bundled skill into .agents/skills/<skillId>")
|
|
772
|
+
.action(async (skillId: string, options: { force: boolean }) => {
|
|
773
|
+
const sourceRoot = await resolveSourceRoot(globals(), context.io);
|
|
774
|
+
await skillInstallCommand({ sourceRoot, skillId, force: options.force, io: context.io });
|
|
775
|
+
});
|
|
680
776
|
|
|
681
777
|
return { program, exitCode };
|
|
682
778
|
}
|
|
@@ -785,20 +881,6 @@ type CommandResult = {
|
|
|
785
881
|
readonly outcome: CliAnalyticsCommandOutcome;
|
|
786
882
|
readonly error?: unknown;
|
|
787
883
|
};
|
|
788
|
-
const formatCliError = (error: unknown): string => {
|
|
789
|
-
if (error instanceof PotatoApiError) {
|
|
790
|
-
const details =
|
|
791
|
-
typeof error.body === "string"
|
|
792
|
-
? error.body
|
|
793
|
-
: error.body === undefined
|
|
794
|
-
? undefined
|
|
795
|
-
: JSON.stringify(error.body);
|
|
796
|
-
return details === undefined || details.length === 0
|
|
797
|
-
? error.message
|
|
798
|
-
: `${error.message}\n${details}`;
|
|
799
|
-
}
|
|
800
|
-
return error instanceof Error ? error.message : String(error);
|
|
801
|
-
};
|
|
802
884
|
|
|
803
885
|
const runProgram = async (
|
|
804
886
|
program: Command,
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { assert } from "es-toolkit";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import { lstat, open, type FileHandle } from "node:fs/promises";
|
|
5
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
|
+
import type { CliAnalytics } from "./analytics";
|
|
7
|
+
|
|
8
|
+
export type IssueReportAuthor = "human" | "agent";
|
|
9
|
+
|
|
10
|
+
type IssueReportIo = {
|
|
11
|
+
readonly stdout: (text: string) => void;
|
|
12
|
+
readonly stderr: (text: string) => void;
|
|
13
|
+
readonly prompt?: (question: string) => Promise<string>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const ISSUE_REPORT_MAX_BYTES = 256 * 1024;
|
|
17
|
+
const REPORT_DECODER = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
18
|
+
|
|
19
|
+
const assertNoSymlinkParents = async (absolutePath: string, sourceRoot: string): Promise<void> => {
|
|
20
|
+
const relativePath = relative(sourceRoot, absolutePath);
|
|
21
|
+
if (
|
|
22
|
+
relativePath === "" ||
|
|
23
|
+
relativePath === ".." ||
|
|
24
|
+
relativePath.startsWith(`..${sep}`) ||
|
|
25
|
+
isAbsolute(relativePath)
|
|
26
|
+
) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const segments = relativePath.split(sep);
|
|
31
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
32
|
+
const directory = join(sourceRoot, ...segments.slice(0, index));
|
|
33
|
+
const stats = await lstat(directory);
|
|
34
|
+
assert(
|
|
35
|
+
!stats.isSymbolicLink(),
|
|
36
|
+
`Report file ${absolutePath} is inside the symbolic link ${directory}; select a path that does not traverse symbolic links.`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const openReportFile = async (
|
|
42
|
+
path: string,
|
|
43
|
+
sourceRoot: string,
|
|
44
|
+
cwd: string,
|
|
45
|
+
): Promise<{ handle: FileHandle; absolutePath: string; size: number }> => {
|
|
46
|
+
const absolutePath = resolve(cwd, path);
|
|
47
|
+
await assertNoSymlinkParents(absolutePath, sourceRoot);
|
|
48
|
+
let handle: FileHandle;
|
|
49
|
+
try {
|
|
50
|
+
handle = await open(absolutePath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
assert(
|
|
53
|
+
!(error instanceof Error && "code" in error && error.code === "ELOOP"),
|
|
54
|
+
`Report file ${absolutePath} is a symbolic link; select the file it points to directly.`,
|
|
55
|
+
);
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const stats = await handle.stat();
|
|
61
|
+
assert(stats.isFile(), `Report file ${absolutePath} is not a regular file.`);
|
|
62
|
+
assert(stats.size > 0, `Report file ${absolutePath} is empty.`);
|
|
63
|
+
assert(
|
|
64
|
+
stats.size <= ISSUE_REPORT_MAX_BYTES,
|
|
65
|
+
`Report file ${absolutePath} is ${stats.size} bytes; the limit is 256 KiB.`,
|
|
66
|
+
);
|
|
67
|
+
return { handle, absolutePath, size: stats.size };
|
|
68
|
+
} catch (error) {
|
|
69
|
+
await handle.close();
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const readReportText = async (path: string, sourceRoot: string, cwd: string): Promise<string> => {
|
|
75
|
+
const opened = await openReportFile(path, sourceRoot, cwd);
|
|
76
|
+
try {
|
|
77
|
+
const bytes = await opened.handle.readFile();
|
|
78
|
+
assert(
|
|
79
|
+
bytes.byteLength === opened.size,
|
|
80
|
+
`${opened.absolutePath} changed while it was being read.`,
|
|
81
|
+
);
|
|
82
|
+
try {
|
|
83
|
+
return REPORT_DECODER.decode(bytes);
|
|
84
|
+
} catch {
|
|
85
|
+
throw new Error(`Report file ${opened.absolutePath} is not valid UTF-8.`);
|
|
86
|
+
}
|
|
87
|
+
} finally {
|
|
88
|
+
await opened.handle.close();
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const confirmed = async (io: IssueReportIo, confirm: boolean): Promise<boolean> => {
|
|
93
|
+
if (confirm) return true;
|
|
94
|
+
if (io.prompt === undefined) return false;
|
|
95
|
+
const answer = (await io.prompt("Send this report to Toloka? [y/N] ")).trim().toLowerCase();
|
|
96
|
+
return answer === "y" || answer === "yes";
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export async function submitIssueReport(options: {
|
|
100
|
+
readonly path: string;
|
|
101
|
+
readonly authoredBy: IssueReportAuthor;
|
|
102
|
+
readonly platform: string;
|
|
103
|
+
readonly confirm: boolean;
|
|
104
|
+
readonly sourceRoot: string;
|
|
105
|
+
readonly cwd: string;
|
|
106
|
+
readonly analytics: CliAnalytics;
|
|
107
|
+
readonly io: IssueReportIo;
|
|
108
|
+
}): Promise<number> {
|
|
109
|
+
const { analytics, io } = options;
|
|
110
|
+
assert(
|
|
111
|
+
analytics.issueReportingEnabled,
|
|
112
|
+
"Issue reporting is disabled. Re-run with TOLOKA_TELEMETRY=1.",
|
|
113
|
+
);
|
|
114
|
+
const reportText = await readReportText(options.path, options.sourceRoot, options.cwd);
|
|
115
|
+
io.stdout("Issue report preview:");
|
|
116
|
+
io.stdout("");
|
|
117
|
+
io.stdout(
|
|
118
|
+
reportText.replace(
|
|
119
|
+
/(?![\t\n])(?!\r\n)(?:\p{Cc}|\p{Bidi_Control})/gu,
|
|
120
|
+
(character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`,
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
io.stdout("");
|
|
124
|
+
io.stdout(`Authored by: ${options.authoredBy}`);
|
|
125
|
+
io.stdout(`Platform: ${options.platform}`);
|
|
126
|
+
io.stdout("");
|
|
127
|
+
|
|
128
|
+
if (!(await confirmed(io, options.confirm))) {
|
|
129
|
+
io.stderr(
|
|
130
|
+
"Refusing to submit without confirmation in a non-interactive environment. Pass --confirm after reviewing the preview above.",
|
|
131
|
+
);
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const reportId = randomUUID();
|
|
136
|
+
await analytics.submitIssueReport({
|
|
137
|
+
reportId,
|
|
138
|
+
reportText,
|
|
139
|
+
authoredBy: options.authoredBy,
|
|
140
|
+
platform: options.platform,
|
|
141
|
+
});
|
|
142
|
+
io.stdout(`Submitted issue report: ${reportId}`);
|
|
143
|
+
return 0;
|
|
144
|
+
}
|