@toloka-platform/sdk 0.1.0-preview.4 → 0.1.0-preview.6
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 +1 -1
- package/dist/cli/toloka.js +132 -126
- 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 +82 -6
- package/docs/datasets.md +8 -9
- package/docs/getting-started.md +2 -2
- package/docs/limitations.md +5 -8
- package/docs/troubleshooting.md +0 -11
- package/openapi/gen/api.ts +782 -876
- package/openapi/gen/models/baseModel.ts +9 -0
- package/openapi/gen/models/baseModelId.ts +2 -0
- package/openapi/gen/models/baseModelIdProperty.ts +2 -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/createProjectModelBody.ts +8 -0
- package/openapi/gen/models/createProjectModelBodyDescription.ts +2 -0
- 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/generationNodeSettingsAudienceAndPricingOneOfOneone.ts +1 -0
- package/openapi/gen/models/index.ts +21 -0
- package/openapi/gen/models/listBaseModels200.ts +5 -0
- package/openapi/gen/models/listModelVersions200.ts +7 -0
- package/openapi/gen/models/listModelVersionsParams.ts +5 -0
- package/openapi/gen/models/listProjectModels200.ts +7 -0
- package/openapi/gen/models/listProjectModelsParams.ts +5 -0
- package/openapi/gen/models/model.ts +16 -0
- package/openapi/gen/models/modelArchivedAt.ts +2 -0
- package/openapi/gen/models/modelDescription.ts +2 -0
- package/openapi/gen/models/modelId.ts +2 -0
- package/openapi/gen/models/modelVersion.ts +9 -0
- package/openapi/gen/models/modelVersionId.ts +2 -0
- package/openapi/gen/models/pipelineGraphSnapshotGraphNodesItemOneOfThreeoneSettingsAudienceAndPricingOneOfOneone.ts +1 -0
- package/openapi/gen/models/runCost.ts +7 -0
- package/openapi/gen/models/validatePipelineGraphBodyGraphNodesItemOneOfThreeoneSettingsAudienceAndPricingOneOfOneone.ts +1 -0
- package/package.json +2 -1
- package/skills/catalog.json +22 -0
- package/skills/{SKILL.md → toloka-build/SKILL.md} +2 -2
- package/skills/{references → toloka-build/references}/workflow-and-cli.md +26 -5
- package/skills/toloka-report-issue/SKILL.md +25 -0
- package/skills/toloka-report-issue/references/report-template.md +34 -0
- package/src/cli/analytics.ts +53 -1
- package/src/cli/data.ts +148 -25
- 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/models.ts +188 -0
- package/src/cli/program.ts +202 -22
- package/src/cli/report.ts +144 -0
- package/src/cli/skill.ts +363 -0
- package/src/client.ts +109 -35
- package/src/errors.ts +25 -10
- package/src/pipeline-graph-v1.contract.json +5 -1
- package/src/transport.ts +16 -13
- /package/skills/{references → toloka-build/references}/node-semantics.md +0 -0
package/src/cli/init.ts
CHANGED
|
@@ -1,45 +1,39 @@
|
|
|
1
1
|
import { assert } from "es-toolkit";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { dirname, join, sep } from "node:path";
|
|
2
|
+
import { lstat, mkdir, readdir, readFile, realpath, rm, rmdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
5
4
|
import packageMetadata from "../../package.json" with { type: "json" };
|
|
6
5
|
import { readTolokaState, writeTolokaState } from "../state";
|
|
6
|
+
import { assertSafeDestination, entryStats, isMissingFileError } from "./file-system";
|
|
7
|
+
import { SKILL_ENTRY_POINT, SKILL_INSTALL_ROOT } from "./skill";
|
|
7
8
|
|
|
8
9
|
export const SDK_VERSION = packageMetadata.version;
|
|
9
10
|
|
|
10
11
|
const SDK_PACKAGE_NAME = "@toloka-platform/sdk";
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
12
|
+
const BOOTSTRAP_NAME = "toloka-bootstrap";
|
|
13
|
+
const CLAUDE_POINTER_PATH = `.claude/skills/${BOOTSTRAP_NAME}/SKILL.md`;
|
|
14
|
+
const CURSOR_POINTER_PATH = `.cursor/rules/${BOOTSTRAP_NAME}.mdc`;
|
|
14
15
|
const AGENTS_PATH = "AGENTS.md";
|
|
15
|
-
const
|
|
16
|
-
const
|
|
16
|
+
const MANAGED_PATHS = [CLAUDE_POINTER_PATH, CURSOR_POINTER_PATH, AGENTS_PATH];
|
|
17
|
+
const CURRENT_BLOCK = { begin: "<toloka>", end: "</toloka>" };
|
|
18
|
+
const LEGACY_BLOCK = { begin: "<!-- toloka:begin -->", end: "<!-- toloka:end -->" };
|
|
17
19
|
const VERSION_PATTERN = /<!-- toloka:sdk-version ([^\s>]+) -->/u;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const LEGACY_ENTRY_PATHS = [join(LEGACY_SKILL_DIRECTORY, "SKILL.md"), LEGACY_CURSOR_RULE];
|
|
25
|
-
const BUNDLED_SKILL_FILES = [
|
|
26
|
-
["SKILL.md", new URL("../../skills/SKILL.md", import.meta.url)],
|
|
27
|
-
[
|
|
28
|
-
"references/node-semantics.md",
|
|
29
|
-
new URL("../../skills/references/node-semantics.md", import.meta.url),
|
|
30
|
-
],
|
|
31
|
-
[
|
|
32
|
-
"references/workflow-and-cli.md",
|
|
33
|
-
new URL("../../skills/references/workflow-and-cli.md", import.meta.url),
|
|
20
|
+
type LegacyEntry = { readonly file: string; readonly directory?: string };
|
|
21
|
+
|
|
22
|
+
const LEGACY_ENTRIES: readonly LegacyEntry[] = ["toloka-build", "toloka-pipeline-as-code"].flatMap(
|
|
23
|
+
(skillName): LegacyEntry[] => [
|
|
24
|
+
{ file: `.claude/skills/${skillName}/SKILL.md`, directory: `.claude/skills/${skillName}` },
|
|
25
|
+
{ file: `.cursor/rules/${skillName}.mdc` },
|
|
34
26
|
],
|
|
35
|
-
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
type BlockMarkers = { readonly begin: string; readonly end: string };
|
|
36
30
|
|
|
37
31
|
export type InitProjectResult = {
|
|
38
32
|
readonly targetDirectory: string;
|
|
39
33
|
readonly files: readonly string[];
|
|
40
34
|
readonly writtenFiles: readonly string[];
|
|
35
|
+
readonly removedFiles: readonly string[];
|
|
41
36
|
readonly sdkVersion: string;
|
|
42
|
-
readonly skillDigest: string;
|
|
43
37
|
readonly dependencyReady: boolean;
|
|
44
38
|
readonly output: string;
|
|
45
39
|
readonly warning?: string;
|
|
@@ -50,9 +44,6 @@ type AgentEntry = {
|
|
|
50
44
|
readonly source: string;
|
|
51
45
|
};
|
|
52
46
|
|
|
53
|
-
const isMissingFileError = (error: unknown) =>
|
|
54
|
-
error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
55
|
-
|
|
56
47
|
const readOptionalFile = async (path: string) => {
|
|
57
48
|
try {
|
|
58
49
|
return await readFile(path, "utf8");
|
|
@@ -71,116 +62,85 @@ const isDirectory = async (path: string) => {
|
|
|
71
62
|
}
|
|
72
63
|
};
|
|
73
64
|
|
|
74
|
-
const
|
|
75
|
-
try {
|
|
76
|
-
return (await lstat(path)).isFile();
|
|
77
|
-
} catch (error) {
|
|
78
|
-
if (isMissingFileError(error)) return false;
|
|
79
|
-
throw error;
|
|
80
|
-
}
|
|
81
|
-
};
|
|
65
|
+
const isGenerated = (source: string) => VERSION_PATTERN.test(source);
|
|
82
66
|
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
for (const segment of relativePath.split("/")) {
|
|
86
|
-
currentPath = join(currentPath, segment);
|
|
87
|
-
try {
|
|
88
|
-
const entry = await lstat(currentPath);
|
|
89
|
-
assert(!entry.isSymbolicLink(), `Refusing to write through symbolic link ${currentPath}.`);
|
|
90
|
-
} catch (error) {
|
|
91
|
-
if (isMissingFileError(error)) return;
|
|
92
|
-
throw error;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
};
|
|
67
|
+
const PROVENANCE = `Generated by \`${SDK_PACKAGE_NAME}\` version \`${SDK_VERSION}\`.
|
|
68
|
+
<!-- toloka:sdk-version ${SDK_VERSION} -->`;
|
|
96
69
|
|
|
97
|
-
|
|
98
|
-
const digest = createHash("sha256");
|
|
99
|
-
for (const [relativePath, url] of BUNDLED_SKILL_FILES) {
|
|
100
|
-
digest.update(relativePath);
|
|
101
|
-
digest.update("\0");
|
|
102
|
-
digest.update(await readFile(url));
|
|
103
|
-
digest.update("\0");
|
|
104
|
-
}
|
|
105
|
-
return digest.digest("hex");
|
|
106
|
-
}
|
|
70
|
+
const BOOTSTRAP_BLOCK = `${CURRENT_BLOCK.begin}
|
|
107
71
|
|
|
108
|
-
|
|
109
|
-
const INSTALLED_REFERENCE_PREFIX = `node_modules/${SDK_PACKAGE_NAME}/skills/references/`;
|
|
110
|
-
|
|
111
|
-
const bundledSkillBody = async (sectionHeading: "##" | "###"): Promise<string> => {
|
|
112
|
-
const source = await readFile(BUNDLED_SKILL_BODY, "utf8");
|
|
113
|
-
const withoutFrontmatter = source.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/u, "");
|
|
114
|
-
const withoutTitle = withoutFrontmatter.replace(/^#\s.*\r?\n+/u, "");
|
|
115
|
-
const withInstalledReferences = withoutTitle
|
|
116
|
-
.replaceAll("](references/", `](${INSTALLED_REFERENCE_PREFIX}`)
|
|
117
|
-
.replaceAll("](../examples/", `](node_modules/${SDK_PACKAGE_NAME}/examples/`);
|
|
118
|
-
return sectionHeading === "##"
|
|
119
|
-
? withInstalledReferences
|
|
120
|
-
: withInstalledReferences.replaceAll(/^## /gmu, "### ");
|
|
121
|
-
};
|
|
72
|
+
## Toloka Agent Bootstrap
|
|
122
73
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
skillDigest: string,
|
|
126
|
-
) => `Generated by \`${SDK_PACKAGE_NAME}\` version \`${SDK_VERSION}\`.
|
|
127
|
-
<!-- toloka:sdk-version ${SDK_VERSION} -->
|
|
128
|
-
<!-- toloka:skill-digest sha256:${skillDigest} -->
|
|
74
|
+
${PROVENANCE}
|
|
75
|
+
Re-run \`toloka init\` to refresh this block. Keep your own instructions outside it.
|
|
129
76
|
|
|
130
|
-
|
|
131
|
-
Use the globally installed \`toloka\` binary. Install it with \`curl -fsSL https://platform.toloka.ai/cli/install.sh | sh\` if it is not on \`PATH\`.
|
|
77
|
+
This repository works with the Toloka platform through the \`toloka\` CLI. Before starting a Toloka task:
|
|
132
78
|
|
|
133
|
-
|
|
79
|
+
1. Run \`toloka --help\`, then \`toloka <resource> --help\` for the resource you need.
|
|
80
|
+
2. Run \`toloka skill search <query>\` to find a bundled skill for the task; \`toloka skill list\` shows every skill and whether it is installed.
|
|
81
|
+
3. Run \`toloka skill install <skill-id>\` and read \`${SKILL_INSTALL_ROOT}/<skill-id>/${SKILL_ENTRY_POINT}\` before acting; add \`--force\` to refresh a stale skill.
|
|
82
|
+
4. Follow the installed skill. When no skill matches, continue with the CLI help and never invent commands or SDK helpers.
|
|
83
|
+
5. To report a Toloka platform problem or unexpected failure to support, run \`toloka skill search report-issue\` and install \`toloka-report-issue\` before collecting evidence.
|
|
134
84
|
|
|
135
|
-
`;
|
|
85
|
+
${CURRENT_BLOCK.end}`;
|
|
136
86
|
|
|
137
|
-
const
|
|
138
|
-
name: ${SKILL_NAME}
|
|
139
|
-
description: Build, inspect, and change Toloka platform pipeline projects as TypeScript source with @toloka-platform/sdk and toloka. Use when the user wants to create, adopt, inspect, plan, or apply a pipeline project as code.
|
|
140
|
-
---
|
|
87
|
+
const POINTER_BODY = `# Toloka Agent Bootstrap pointer
|
|
141
88
|
|
|
142
|
-
|
|
89
|
+
${PROVENANCE}
|
|
143
90
|
|
|
144
|
-
|
|
91
|
+
Read the \`${CURRENT_BLOCK.begin}\` block in \`${AGENTS_PATH}\` at the source root and follow it before doing Toloka work. It tells you to run \`toloka --help\`, search the bundled Skill Catalog with \`toloka skill search <query>\`, install the match with \`toloka skill install <skill-id>\`, and read \`${SKILL_INSTALL_ROOT}/<skill-id>/${SKILL_ENTRY_POINT}\` before acting.
|
|
92
|
+
`;
|
|
145
93
|
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
94
|
+
const CLAUDE_POINTER_SOURCE = `---
|
|
95
|
+
name: ${BOOTSTRAP_NAME}
|
|
96
|
+
description: Toloka platform work with the toloka CLI and ${SDK_PACKAGE_NAME}, such as pipeline projects, datasets, runs, secrets, and issue reporting. Use when the user mentions Toloka or the toloka CLI. Start by reading the ${CURRENT_BLOCK.begin} Agent Bootstrap block in ${AGENTS_PATH}, then find and install the matching skill.
|
|
149
97
|
---
|
|
150
98
|
|
|
151
|
-
|
|
99
|
+
${POINTER_BODY}`;
|
|
152
100
|
|
|
153
|
-
|
|
101
|
+
const CURSOR_POINTER_SOURCE = `---
|
|
102
|
+
description: Toloka Agent Bootstrap pointer for the toloka CLI and ${SDK_PACKAGE_NAME}
|
|
103
|
+
alwaysApply: true
|
|
104
|
+
---
|
|
154
105
|
|
|
155
|
-
|
|
156
|
-
## Build Toloka pipelines as code
|
|
106
|
+
${POINTER_BODY}`;
|
|
157
107
|
|
|
158
|
-
|
|
159
|
-
|
|
108
|
+
const locateBlock = (source: string, markers: BlockMarkers, path: string) => {
|
|
109
|
+
const beginIndex = source.indexOf(markers.begin);
|
|
110
|
+
const endIndex = source.indexOf(markers.end);
|
|
111
|
+
if (beginIndex === -1 && endIndex === -1) return undefined;
|
|
112
|
+
assert(
|
|
113
|
+
beginIndex !== -1 && endIndex !== -1 && beginIndex < endIndex,
|
|
114
|
+
`Malformed Toloka block in ${path}: expected one ${markers.begin} followed by one ${markers.end}. Repair the file by hand, then re-run \`toloka init\`.`,
|
|
115
|
+
);
|
|
116
|
+
assert(
|
|
117
|
+
!source.includes(markers.begin, beginIndex + markers.begin.length) &&
|
|
118
|
+
!source.includes(markers.end, endIndex + markers.end.length),
|
|
119
|
+
`Multiple Toloka blocks in ${path}: keep exactly one ${markers.begin} ... ${markers.end} block, then re-run \`toloka init\`.`,
|
|
120
|
+
);
|
|
121
|
+
return { beginIndex, endIndex: endIndex + markers.end.length };
|
|
122
|
+
};
|
|
160
123
|
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
const hasBegin = beginIndex !== -1;
|
|
165
|
-
const hasEnd = endIndex !== -1;
|
|
166
|
-
const lineEnding = source.includes("\r\n") ? "\r\n" : "\n";
|
|
167
|
-
const normalizedBlock = block.replaceAll("\n", lineEnding);
|
|
168
|
-
if (!hasBegin && !hasEnd) {
|
|
169
|
-
if (source.length === 0) return `${normalizedBlock}${lineEnding}`;
|
|
170
|
-
const gap = source.endsWith(`${lineEnding}${lineEnding}`)
|
|
171
|
-
? ""
|
|
172
|
-
: source.endsWith(lineEnding)
|
|
173
|
-
? lineEnding
|
|
174
|
-
: `${lineEnding}${lineEnding}`;
|
|
175
|
-
return `${source}${gap}${normalizedBlock}${lineEnding}`;
|
|
176
|
-
}
|
|
177
|
-
assert(hasBegin && hasEnd && beginIndex < endIndex, `Malformed toloka block in ${AGENTS_PATH}.`);
|
|
124
|
+
const mergeBootstrap = (source: string, path: string) => {
|
|
125
|
+
const current = locateBlock(source, CURRENT_BLOCK, path);
|
|
126
|
+
const legacy = locateBlock(source, LEGACY_BLOCK, path);
|
|
178
127
|
assert(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
`Multiple toloka blocks found in ${AGENTS_PATH}.`,
|
|
128
|
+
current === undefined || legacy === undefined,
|
|
129
|
+
`Ambiguous Toloka blocks in ${path}: both ${CURRENT_BLOCK.begin} and ${LEGACY_BLOCK.begin} are present. Keep one, then re-run \`toloka init\`.`,
|
|
182
130
|
);
|
|
183
|
-
|
|
131
|
+
const lineEnding = source.includes("\r\n") ? "\r\n" : "\n";
|
|
132
|
+
const block = BOOTSTRAP_BLOCK.replaceAll("\n", lineEnding);
|
|
133
|
+
const existing = current ?? legacy;
|
|
134
|
+
if (existing !== undefined) {
|
|
135
|
+
return `${source.slice(0, existing.beginIndex)}${block}${source.slice(existing.endIndex)}`;
|
|
136
|
+
}
|
|
137
|
+
if (source.length === 0) return `${block}${lineEnding}`;
|
|
138
|
+
const gap = source.endsWith(`${lineEnding}${lineEnding}`)
|
|
139
|
+
? ""
|
|
140
|
+
: source.endsWith(lineEnding)
|
|
141
|
+
? lineEnding
|
|
142
|
+
: `${lineEnding}${lineEnding}`;
|
|
143
|
+
return `${source}${gap}${block}${lineEnding}`;
|
|
184
144
|
};
|
|
185
145
|
|
|
186
146
|
const hasSdkDependency = async (targetDirectory: string) => {
|
|
@@ -210,43 +170,46 @@ const writeIfChanged = async (path: string, source: string) => {
|
|
|
210
170
|
return true;
|
|
211
171
|
};
|
|
212
172
|
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
173
|
+
const assertRefreshablePointer = async (targetRoot: string, relativePath: string) => {
|
|
174
|
+
const path = join(targetRoot, relativePath);
|
|
175
|
+
const existing = await readOptionalFile(path);
|
|
176
|
+
assert(
|
|
177
|
+
existing === undefined || isGenerated(existing),
|
|
178
|
+
`Refusing to overwrite ${path}: it carries no \`<!-- toloka:sdk-version ... -->\` marker, so it is not a pointer generated by ${SDK_PACKAGE_NAME}. Move it aside, then re-run \`toloka init\`.`,
|
|
179
|
+
);
|
|
220
180
|
};
|
|
221
181
|
|
|
222
|
-
const
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
const path = join(targetRoot,
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
!entry.isSymbolicLink(),
|
|
230
|
-
`Refusing to remove legacy agent entry point through symbolic link ${path}.`,
|
|
231
|
-
);
|
|
232
|
-
await assertSafeDestination(targetRoot, relativePath.split(sep).join("/"));
|
|
233
|
-
await rm(path, { recursive: true, force: true });
|
|
234
|
-
return relativePath;
|
|
182
|
+
const recognizeLegacyEntries = async (targetRoot: string): Promise<LegacyEntry[]> => {
|
|
183
|
+
const recognized = await Promise.all(
|
|
184
|
+
LEGACY_ENTRIES.map(async (entry) => {
|
|
185
|
+
const path = join(targetRoot, entry.file);
|
|
186
|
+
if ((await entryStats(path)) === undefined) return undefined;
|
|
187
|
+
await assertSafeDestination(targetRoot, entry.file, "remove legacy agent entry point");
|
|
188
|
+
return isGenerated(await readFile(path, "utf8")) ? entry : undefined;
|
|
235
189
|
}),
|
|
236
190
|
);
|
|
237
|
-
return
|
|
191
|
+
return recognized.filter((entry): entry is LegacyEntry => entry !== undefined);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const removeLegacyEntry = async (targetRoot: string, entry: LegacyEntry) => {
|
|
195
|
+
await rm(join(targetRoot, entry.file), { force: true });
|
|
196
|
+
if (entry.directory === undefined) return;
|
|
197
|
+
const directory = join(targetRoot, entry.directory);
|
|
198
|
+
if ((await readdir(directory)).length === 0) await rmdir(directory);
|
|
238
199
|
};
|
|
239
200
|
|
|
240
201
|
const formatInitOutput = (
|
|
241
202
|
targetRoot: string,
|
|
242
203
|
files: readonly string[],
|
|
243
204
|
writtenFiles: readonly string[],
|
|
244
|
-
|
|
205
|
+
removedFiles: readonly string[],
|
|
245
206
|
): string =>
|
|
246
207
|
[
|
|
247
|
-
`${writtenFiles.length === 0 ? "Agent entry points are up to date" : "Initialized agent entry points"} for ${SDK_PACKAGE_NAME} ${SDK_VERSION} in ${targetRoot}:`,
|
|
208
|
+
`${writtenFiles.length === 0 && removedFiles.length === 0 ? "Agent entry points are up to date" : "Initialized agent entry points"} for ${SDK_PACKAGE_NAME} ${SDK_VERSION} in ${targetRoot}:`,
|
|
248
209
|
...files.map((file) => ` ${file}`),
|
|
249
|
-
...
|
|
210
|
+
...removedFiles.map(
|
|
211
|
+
(file) => ` removed ${file} (replaced by the ${AGENTS_PATH} Agent Bootstrap)`,
|
|
212
|
+
),
|
|
250
213
|
].join("\n");
|
|
251
214
|
|
|
252
215
|
const bindCheckout = async (
|
|
@@ -270,44 +233,27 @@ export async function initProject(
|
|
|
270
233
|
): Promise<InitProjectResult> {
|
|
271
234
|
await mkdir(targetDirectory, { recursive: true });
|
|
272
235
|
const targetRoot = await realpath(targetDirectory);
|
|
273
|
-
const
|
|
274
|
-
const
|
|
236
|
+
const agentsPath = join(targetRoot, AGENTS_PATH);
|
|
237
|
+
const existingAgents = await readOptionalFile(agentsPath);
|
|
238
|
+
const agentsSource = mergeBootstrap(existingAgents ?? "", agentsPath);
|
|
239
|
+
const [hasClaude, hasCursor] = await Promise.all([
|
|
275
240
|
isDirectory(join(targetRoot, ".claude")),
|
|
276
241
|
isDirectory(join(targetRoot, ".cursor", "rules")),
|
|
277
|
-
isFile(join(targetRoot, AGENTS_PATH)),
|
|
278
|
-
computeBundledSkillDigest(),
|
|
279
242
|
]);
|
|
280
|
-
const useDefaults = !hasClaude && !hasCursor &&
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
relativePath:
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
},
|
|
287
|
-
{
|
|
288
|
-
relativePath: CURSOR_RULE_PATH,
|
|
289
|
-
wanted: hasCursor,
|
|
290
|
-
source: () => cursorRuleSource(skillDigest),
|
|
291
|
-
},
|
|
292
|
-
{
|
|
293
|
-
relativePath: AGENTS_PATH,
|
|
294
|
-
wanted: hasAgents || useDefaults,
|
|
295
|
-
source: async () => {
|
|
296
|
-
const existingSource = await readOptionalFile(join(targetRoot, AGENTS_PATH));
|
|
297
|
-
return mergeAgentsBlock(existingSource ?? "", await agentsBlockSource(skillDigest));
|
|
298
|
-
},
|
|
299
|
-
},
|
|
243
|
+
const useDefaults = !hasClaude && !hasCursor && existingAgents === undefined;
|
|
244
|
+
const pointers: AgentEntry[] = [
|
|
245
|
+
...(hasClaude || useDefaults
|
|
246
|
+
? [{ relativePath: CLAUDE_POINTER_PATH, source: CLAUDE_POINTER_SOURCE }]
|
|
247
|
+
: []),
|
|
248
|
+
...(hasCursor ? [{ relativePath: CURSOR_POINTER_PATH, source: CURSOR_POINTER_SOURCE }] : []),
|
|
300
249
|
];
|
|
301
|
-
const
|
|
250
|
+
const entries: AgentEntry[] = [...pointers, { relativePath: AGENTS_PATH, source: agentsSource }];
|
|
251
|
+
await Promise.all(entries.map((entry) => assertSafeDestination(targetRoot, entry.relativePath)));
|
|
302
252
|
await Promise.all(
|
|
303
|
-
|
|
304
|
-
);
|
|
305
|
-
const entries: AgentEntry[] = await Promise.all(
|
|
306
|
-
selected.map(async (candidate) => ({
|
|
307
|
-
relativePath: candidate.relativePath,
|
|
308
|
-
source: await candidate.source(),
|
|
309
|
-
})),
|
|
253
|
+
pointers.map((pointer) => assertRefreshablePointer(targetRoot, pointer.relativePath)),
|
|
310
254
|
);
|
|
255
|
+
const legacyEntries = await recognizeLegacyEntries(targetRoot);
|
|
256
|
+
const boundCheckout = await bindCheckout(targetRoot, options.projectId);
|
|
311
257
|
|
|
312
258
|
const writtenFiles = boundCheckout === undefined ? [] : [boundCheckout];
|
|
313
259
|
for (const entry of entries) {
|
|
@@ -315,7 +261,10 @@ export async function initProject(
|
|
|
315
261
|
writtenFiles.push(entry.relativePath);
|
|
316
262
|
}
|
|
317
263
|
}
|
|
318
|
-
const
|
|
264
|
+
for (const entry of legacyEntries) {
|
|
265
|
+
await removeLegacyEntry(targetRoot, entry);
|
|
266
|
+
}
|
|
267
|
+
const removedFiles = legacyEntries.map((entry) => entry.file);
|
|
319
268
|
const dependencyReady = await hasSdkDependency(targetRoot);
|
|
320
269
|
const files = [
|
|
321
270
|
...entries.map((entry) => entry.relativePath),
|
|
@@ -328,10 +277,10 @@ export async function initProject(
|
|
|
328
277
|
targetDirectory: targetRoot,
|
|
329
278
|
files,
|
|
330
279
|
writtenFiles,
|
|
280
|
+
removedFiles,
|
|
331
281
|
sdkVersion: SDK_VERSION,
|
|
332
|
-
skillDigest,
|
|
333
282
|
dependencyReady,
|
|
334
|
-
output: formatInitOutput(targetRoot, files, writtenFiles,
|
|
283
|
+
output: formatInitOutput(targetRoot, files, writtenFiles, removedFiles),
|
|
335
284
|
...(warning === undefined ? {} : { warning }),
|
|
336
285
|
};
|
|
337
286
|
}
|
|
@@ -339,23 +288,29 @@ export async function initProject(
|
|
|
339
288
|
export async function readAgentSkillStalenessWarning(
|
|
340
289
|
targetDirectory: string,
|
|
341
290
|
): Promise<string | undefined> {
|
|
342
|
-
const
|
|
343
|
-
const
|
|
344
|
-
const
|
|
345
|
-
|
|
291
|
+
const outdated: string[] = [];
|
|
292
|
+
const versions = new Set<string>();
|
|
293
|
+
const record = (relativePath: string, source: string) => {
|
|
294
|
+
outdated.push(relativePath);
|
|
295
|
+
const version = VERSION_PATTERN.exec(source)?.[1];
|
|
296
|
+
if (version !== undefined) versions.add(version);
|
|
297
|
+
};
|
|
298
|
+
for (const { file } of LEGACY_ENTRIES) {
|
|
299
|
+
const source = await readOptionalFile(join(targetDirectory, file));
|
|
300
|
+
if (source !== undefined && isGenerated(source)) record(file, source);
|
|
301
|
+
}
|
|
302
|
+
for (const relativePath of MANAGED_PATHS) {
|
|
346
303
|
const source = await readOptionalFile(join(targetDirectory, relativePath));
|
|
347
304
|
if (source === undefined) continue;
|
|
348
|
-
const
|
|
349
|
-
if (
|
|
350
|
-
|
|
351
|
-
const recordedVersion = VERSION_PATTERN.exec(source)?.[1];
|
|
352
|
-
if (recordedVersion !== undefined) generatedVersions.add(recordedVersion);
|
|
305
|
+
const version = VERSION_PATTERN.exec(source)?.[1];
|
|
306
|
+
if (source.includes(LEGACY_BLOCK.begin) || (version !== undefined && version !== SDK_VERSION)) {
|
|
307
|
+
record(relativePath, source);
|
|
353
308
|
}
|
|
354
309
|
}
|
|
355
|
-
if (
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
?
|
|
359
|
-
: [...
|
|
360
|
-
return `Warning: Agent instructions generated by ${
|
|
310
|
+
if (outdated.length === 0) return undefined;
|
|
311
|
+
const generatedBy =
|
|
312
|
+
versions.size === 0
|
|
313
|
+
? `an earlier ${SDK_PACKAGE_NAME} build`
|
|
314
|
+
: `${SDK_PACKAGE_NAME} ${[...versions].toSorted().join(", ")}`;
|
|
315
|
+
return `Warning: Agent instructions generated by ${generatedBy} are out of date (${outdated.join(", ")}). The installed ${SDK_VERSION} manages one Agent Bootstrap in ${AGENTS_PATH} plus pointer files. Re-run \`toloka init\`.`;
|
|
361
316
|
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { assert } from "es-toolkit";
|
|
2
|
+
|
|
3
|
+
import type { BaseModel, BaseModelId, Model, ModelVersion, PotatoClient } from "../client";
|
|
4
|
+
|
|
5
|
+
type ModelApi = Pick<PotatoClient, "baseModels" | "models">;
|
|
6
|
+
type ModelIo = { readonly stdout: (text: string) => void };
|
|
7
|
+
type TableRow = readonly string[];
|
|
8
|
+
|
|
9
|
+
const BASE_MODEL_IDS = [
|
|
10
|
+
"qwen3-4b",
|
|
11
|
+
"qwen3-8b",
|
|
12
|
+
"qwen3-32b",
|
|
13
|
+
"qwen3-235b",
|
|
14
|
+
] satisfies readonly BaseModelId[];
|
|
15
|
+
|
|
16
|
+
const formatTable = (rows: readonly TableRow[]): string => {
|
|
17
|
+
const widths = rows.reduce<number[]>(
|
|
18
|
+
(current, row) => row.map((cell, index) => Math.max(current[index] ?? 0, cell.length)),
|
|
19
|
+
[],
|
|
20
|
+
);
|
|
21
|
+
return rows
|
|
22
|
+
.map((row) =>
|
|
23
|
+
row
|
|
24
|
+
.map((cell, index) => cell.padEnd(widths[index] ?? cell.length))
|
|
25
|
+
.join(" ")
|
|
26
|
+
.trimEnd(),
|
|
27
|
+
)
|
|
28
|
+
.join("\n");
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const writeOutput = (io: ModelIo, json: boolean, value: unknown, formatted: string): void => {
|
|
32
|
+
io.stdout(json ? JSON.stringify(value, null, 2) : formatted);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const formatBaseModel = (model: BaseModel): string =>
|
|
36
|
+
formatTable([
|
|
37
|
+
["ID", model.id],
|
|
38
|
+
["Name", model.displayName],
|
|
39
|
+
["Provider", model.provider],
|
|
40
|
+
["Upstream model", model.upstreamModel],
|
|
41
|
+
["Upstream revision", model.upstreamRevision],
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const formatModel = (model: Model): string =>
|
|
45
|
+
formatTable([
|
|
46
|
+
["ID", model.id],
|
|
47
|
+
["Project", model.projectId],
|
|
48
|
+
["Name", model.name],
|
|
49
|
+
["Base model", model.baseModelId],
|
|
50
|
+
["Description", model.description ?? "-"],
|
|
51
|
+
["Archived at", model.archivedAt ?? "-"],
|
|
52
|
+
["Created at", model.createdAt],
|
|
53
|
+
["Updated at", model.updatedAt],
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
export const parseBaseModelId = (value: string): BaseModelId => {
|
|
57
|
+
const modelId = BASE_MODEL_IDS.find((candidate) => candidate === value);
|
|
58
|
+
assert(modelId, `Unknown base model ${value}.`);
|
|
59
|
+
return modelId;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const resolveModel = async (
|
|
63
|
+
client: ModelApi,
|
|
64
|
+
projectId: string,
|
|
65
|
+
nameOrId: string,
|
|
66
|
+
): Promise<Model> => {
|
|
67
|
+
if (nameOrId.startsWith("mdl.")) return client.models.get({ projectId, modelId: nameOrId });
|
|
68
|
+
const matches: Model[] = [];
|
|
69
|
+
for await (const model of client.models.listAll({ projectId, limit: 100 })) {
|
|
70
|
+
if (model.name === nameOrId) matches.push(model);
|
|
71
|
+
}
|
|
72
|
+
assert(matches.length === 1, `Model ${nameOrId} was not found or is ambiguous.`);
|
|
73
|
+
return matches[0]!;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const listBaseModels = async (
|
|
77
|
+
client: ModelApi,
|
|
78
|
+
input: { json: boolean },
|
|
79
|
+
io: ModelIo,
|
|
80
|
+
): Promise<void> => {
|
|
81
|
+
const models = await client.baseModels.list();
|
|
82
|
+
writeOutput(
|
|
83
|
+
io,
|
|
84
|
+
input.json,
|
|
85
|
+
models,
|
|
86
|
+
models.length === 0
|
|
87
|
+
? "No base models found."
|
|
88
|
+
: formatTable([
|
|
89
|
+
["ID", "NAME", "PROVIDER", "UPSTREAM MODEL"],
|
|
90
|
+
...models.map((model) => [
|
|
91
|
+
model.id,
|
|
92
|
+
model.displayName,
|
|
93
|
+
model.provider,
|
|
94
|
+
model.upstreamModel,
|
|
95
|
+
]),
|
|
96
|
+
]),
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const getBaseModel = async (
|
|
101
|
+
client: ModelApi,
|
|
102
|
+
input: { id: BaseModelId; json: boolean },
|
|
103
|
+
io: ModelIo,
|
|
104
|
+
): Promise<void> => {
|
|
105
|
+
const model = await client.baseModels.get({ id: input.id });
|
|
106
|
+
writeOutput(io, input.json, model, formatBaseModel(model));
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const listModels = async (
|
|
110
|
+
client: ModelApi,
|
|
111
|
+
input: { projectId: string; json: boolean },
|
|
112
|
+
io: ModelIo,
|
|
113
|
+
): Promise<void> => {
|
|
114
|
+
const page = await client.models.listPage({ projectId: input.projectId, limit: 100, offset: 0 });
|
|
115
|
+
writeOutput(
|
|
116
|
+
io,
|
|
117
|
+
input.json,
|
|
118
|
+
page.models,
|
|
119
|
+
page.models.length === 0
|
|
120
|
+
? "No models found."
|
|
121
|
+
: formatTable([
|
|
122
|
+
["ID", "NAME", "BASE MODEL"],
|
|
123
|
+
...page.models.map((model) => [model.id, model.name, model.baseModelId]),
|
|
124
|
+
]),
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export const getModel = async (
|
|
129
|
+
client: ModelApi,
|
|
130
|
+
input: { projectId: string; nameOrId: string; json: boolean },
|
|
131
|
+
io: ModelIo,
|
|
132
|
+
): Promise<void> => {
|
|
133
|
+
const model = await resolveModel(client, input.projectId, input.nameOrId);
|
|
134
|
+
writeOutput(io, input.json, model, formatModel(model));
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const listModelVersions = async (
|
|
138
|
+
client: ModelApi,
|
|
139
|
+
input: { projectId: string; nameOrId: string; json: boolean },
|
|
140
|
+
io: ModelIo,
|
|
141
|
+
): Promise<void> => {
|
|
142
|
+
const model = await resolveModel(client, input.projectId, input.nameOrId);
|
|
143
|
+
const versions: ModelVersion[] = [];
|
|
144
|
+
for await (const version of client.models.listAllVersions({
|
|
145
|
+
projectId: input.projectId,
|
|
146
|
+
modelId: model.id,
|
|
147
|
+
limit: 100,
|
|
148
|
+
})) {
|
|
149
|
+
versions.push(version);
|
|
150
|
+
}
|
|
151
|
+
writeOutput(
|
|
152
|
+
io,
|
|
153
|
+
input.json,
|
|
154
|
+
versions,
|
|
155
|
+
versions.length === 0
|
|
156
|
+
? "No Model Versions found."
|
|
157
|
+
: formatTable([
|
|
158
|
+
["VERSION", "ID", "CREATED AT"],
|
|
159
|
+
...versions.map((version) => [version.version, version.id, version.createdAt]),
|
|
160
|
+
]),
|
|
161
|
+
);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const createModel = async (
|
|
165
|
+
client: ModelApi,
|
|
166
|
+
input: {
|
|
167
|
+
projectId: string;
|
|
168
|
+
name: string;
|
|
169
|
+
description?: string;
|
|
170
|
+
baseModelId: BaseModelId;
|
|
171
|
+
json: boolean;
|
|
172
|
+
},
|
|
173
|
+
io: ModelIo,
|
|
174
|
+
): Promise<void> => {
|
|
175
|
+
const { json, ...body } = input;
|
|
176
|
+
const model = await client.models.create(body);
|
|
177
|
+
writeOutput(io, json, model, formatModel(model));
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export const archiveModel = async (
|
|
181
|
+
client: ModelApi,
|
|
182
|
+
input: { projectId: string; nameOrId: string; json: boolean },
|
|
183
|
+
io: ModelIo,
|
|
184
|
+
): Promise<void> => {
|
|
185
|
+
const model = await resolveModel(client, input.projectId, input.nameOrId);
|
|
186
|
+
const archived = await client.models.archive({ projectId: input.projectId, modelId: model.id });
|
|
187
|
+
writeOutput(io, input.json, archived, `Archived Model ${archived.id}.`);
|
|
188
|
+
};
|