@solgridai/cli 0.1.0 → 0.2.0-beta.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.
- package/README.md +41 -0
- package/dist/index.js +1244 -46
- package/package.json +1 -1
- package/skills/solgrid-cli/SKILL.md +81 -7
package/dist/index.js
CHANGED
|
@@ -2075,7 +2075,7 @@ function useColor() {
|
|
|
2075
2075
|
// node_modules/commander/index.js
|
|
2076
2076
|
var program = new Command;
|
|
2077
2077
|
// src/build-info.ts
|
|
2078
|
-
var buildInfo = { packageName: "@solgridai/cli", version: "0.
|
|
2078
|
+
var buildInfo = { packageName: "@solgridai/cli", version: "0.2.0-beta.0" };
|
|
2079
2079
|
|
|
2080
2080
|
// src/core/errors.ts
|
|
2081
2081
|
class CliError extends Error {
|
|
@@ -2091,16 +2091,43 @@ class CliError extends Error {
|
|
|
2091
2091
|
this.details = options.details ?? [];
|
|
2092
2092
|
this.data = options.data;
|
|
2093
2093
|
}
|
|
2094
|
+
copyWith(changes) {
|
|
2095
|
+
return new CliError(this.code, this.message, {
|
|
2096
|
+
hint: changes.hint ?? this.hint,
|
|
2097
|
+
details: this.details,
|
|
2098
|
+
data: changes.data ?? this.data,
|
|
2099
|
+
cause: this
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2094
2102
|
}
|
|
2095
2103
|
|
|
2096
2104
|
class ApiError extends CliError {
|
|
2097
2105
|
status;
|
|
2098
2106
|
key;
|
|
2107
|
+
routeMissing;
|
|
2108
|
+
apiKeySentTo;
|
|
2109
|
+
signedUrl;
|
|
2099
2110
|
constructor(code, message, options = {}) {
|
|
2100
2111
|
super(code, message, options);
|
|
2101
2112
|
this.name = "ApiError";
|
|
2102
2113
|
this.status = options.status;
|
|
2103
2114
|
this.key = options.key;
|
|
2115
|
+
this.routeMissing = options.routeMissing === true;
|
|
2116
|
+
this.apiKeySentTo = options.apiKeySentTo;
|
|
2117
|
+
this.signedUrl = options.signedUrl === true;
|
|
2118
|
+
}
|
|
2119
|
+
copyWith(changes) {
|
|
2120
|
+
return new ApiError(this.code, this.message, {
|
|
2121
|
+
...this.status !== undefined ? { status: this.status } : {},
|
|
2122
|
+
...this.key ? { key: this.key } : {},
|
|
2123
|
+
routeMissing: this.routeMissing,
|
|
2124
|
+
...this.apiKeySentTo ? { apiKeySentTo: this.apiKeySentTo } : {},
|
|
2125
|
+
signedUrl: this.signedUrl,
|
|
2126
|
+
hint: changes.hint ?? this.hint,
|
|
2127
|
+
details: this.details,
|
|
2128
|
+
data: changes.data ?? this.data,
|
|
2129
|
+
cause: this
|
|
2130
|
+
});
|
|
2104
2131
|
}
|
|
2105
2132
|
}
|
|
2106
2133
|
function errorCodeForStatus(status) {
|
|
@@ -2181,7 +2208,19 @@ var isPrice = objectWith({
|
|
|
2181
2208
|
selectors: optional(listOf(objectWith({ key: isString, label: optional(isText) }))),
|
|
2182
2209
|
variants: optional(listOf(objectWith({ when: isObject, price: isLeafPrice }))),
|
|
2183
2210
|
tiers: optional(listOf(objectWith({ label: isText, when: isObject, price: isLeafPrice }))),
|
|
2184
|
-
notes: optional(listOf(isText))
|
|
2211
|
+
notes: optional(listOf(isText)),
|
|
2212
|
+
unit: optional(isText),
|
|
2213
|
+
unitSize: optional(isNumber),
|
|
2214
|
+
description: optional(isText),
|
|
2215
|
+
rates: optional(listOf(objectWith({
|
|
2216
|
+
meter: isString,
|
|
2217
|
+
unitPriceUsd: isNumber,
|
|
2218
|
+
unitSize: optional(isNumber),
|
|
2219
|
+
freeUnits: optional(isNumber),
|
|
2220
|
+
maximumUnits: optional(isNumber),
|
|
2221
|
+
roundUpTo: optional(isNumber),
|
|
2222
|
+
conditions: optional(isObject)
|
|
2223
|
+
})))
|
|
2185
2224
|
});
|
|
2186
2225
|
var isMetrics = objectWith({
|
|
2187
2226
|
status: isString,
|
|
@@ -2225,12 +2264,12 @@ async function inspect(client, params) {
|
|
|
2225
2264
|
const data = await client.post("/v1/tools/inspect", params);
|
|
2226
2265
|
return expectShape("/v1/tools/inspect", data, TOOL_FIELDS);
|
|
2227
2266
|
}
|
|
2228
|
-
async function createRun(client, params, idempotencyKey) {
|
|
2267
|
+
async function createRun(client, params, idempotencyKey, request = {}) {
|
|
2229
2268
|
const data = await client.post("/v1/runs/create", {
|
|
2230
2269
|
provider: params.provider,
|
|
2231
2270
|
endpoint: params.endpoint,
|
|
2232
2271
|
...params.input ? { input: params.input } : {}
|
|
2233
|
-
}, { "Idempotency-Key": idempotencyKey });
|
|
2272
|
+
}, { "Idempotency-Key": idempotencyKey }, request);
|
|
2234
2273
|
return expectShape("/v1/runs/create", data, RUN_FIELDS);
|
|
2235
2274
|
}
|
|
2236
2275
|
async function stopRun(client, runId) {
|
|
@@ -2265,7 +2304,8 @@ var TOOL_FIELDS = {
|
|
|
2265
2304
|
provider: isString,
|
|
2266
2305
|
endpoint: isString,
|
|
2267
2306
|
description: isText,
|
|
2268
|
-
price: isPrice,
|
|
2307
|
+
price: optional(isPrice),
|
|
2308
|
+
externalPrice: optional(isText),
|
|
2269
2309
|
tags: optional(listOf(isText)),
|
|
2270
2310
|
notes: optional(listOf(isText)),
|
|
2271
2311
|
metrics: optional(isMetrics)
|
|
@@ -2540,10 +2580,36 @@ class ApiClient {
|
|
|
2540
2580
|
}
|
|
2541
2581
|
if (!response.ok) {
|
|
2542
2582
|
const text = await response.text().catch(() => "");
|
|
2543
|
-
throw envelopeError(response, parseEnvelope(text), text, url.split("?")[0] ?? url
|
|
2583
|
+
throw envelopeError(response, parseEnvelope(text), text, url.split("?")[0] ?? url, {
|
|
2584
|
+
signedUrl: true
|
|
2585
|
+
});
|
|
2544
2586
|
}
|
|
2545
2587
|
return response;
|
|
2546
2588
|
}
|
|
2589
|
+
async openDownload(path) {
|
|
2590
|
+
const url = `${this.baseUrl}${path}`;
|
|
2591
|
+
let response;
|
|
2592
|
+
try {
|
|
2593
|
+
response = await this.options.fetch(url, {
|
|
2594
|
+
method: "GET",
|
|
2595
|
+
headers: {
|
|
2596
|
+
"X-Solgrid-Client": this.options.clientId,
|
|
2597
|
+
...this.options.apiKey ? { Authorization: `Bearer ${this.options.apiKey}` } : {}
|
|
2598
|
+
},
|
|
2599
|
+
signal: AbortSignal.timeout(TRANSFER_TIMEOUT_MS)
|
|
2600
|
+
});
|
|
2601
|
+
} catch (cause) {
|
|
2602
|
+
throw transportError(cause, this.baseUrl, path, TRANSFER_TIMEOUT_MS);
|
|
2603
|
+
}
|
|
2604
|
+
if (!response.ok) {
|
|
2605
|
+
const text = await response.text().catch(() => "");
|
|
2606
|
+
throw envelopeError(response, parseEnvelope(text), text, url, this.sentWith());
|
|
2607
|
+
}
|
|
2608
|
+
return response;
|
|
2609
|
+
}
|
|
2610
|
+
sentWith() {
|
|
2611
|
+
return this.options.apiKey ? { apiKeySentTo: this.baseUrl } : {};
|
|
2612
|
+
}
|
|
2547
2613
|
async send(method, path, options = {}) {
|
|
2548
2614
|
const timeoutMs = options.timeoutMs ?? this.options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
2549
2615
|
let response;
|
|
@@ -2567,7 +2633,7 @@ class ApiClient {
|
|
|
2567
2633
|
const url = `${this.baseUrl}${path.split("?")[0] ?? path}`;
|
|
2568
2634
|
const envelope = parseEnvelope(text);
|
|
2569
2635
|
if (!response.ok || envelope?.success === false) {
|
|
2570
|
-
throw envelopeError(response, envelope, text, url);
|
|
2636
|
+
throw envelopeError(response, envelope, text, url, this.sentWith());
|
|
2571
2637
|
}
|
|
2572
2638
|
if (!envelope || !("data" in envelope)) {
|
|
2573
2639
|
throw new ApiError("INVALID_RESPONSE", `Unexpected response from ${path}: ${describeBody(response, text)}`, { status: response.status });
|
|
@@ -2575,6 +2641,9 @@ class ApiClient {
|
|
|
2575
2641
|
return envelope.data;
|
|
2576
2642
|
}
|
|
2577
2643
|
}
|
|
2644
|
+
function isResponseData(envelope) {
|
|
2645
|
+
return typeof envelope?.success === "boolean";
|
|
2646
|
+
}
|
|
2578
2647
|
function parseEnvelope(text) {
|
|
2579
2648
|
try {
|
|
2580
2649
|
const value = JSON.parse(text);
|
|
@@ -2583,25 +2652,28 @@ function parseEnvelope(text) {
|
|
|
2583
2652
|
return;
|
|
2584
2653
|
}
|
|
2585
2654
|
}
|
|
2586
|
-
function envelopeError(response, envelope, text, url) {
|
|
2587
|
-
if (
|
|
2655
|
+
function envelopeError(response, envelope, text, url, auth) {
|
|
2656
|
+
if (response.status === 404 && !isResponseData(envelope)) {
|
|
2588
2657
|
return new ApiError("NOT_FOUND", `${url} does not exist`, {
|
|
2589
2658
|
status: 404,
|
|
2659
|
+
routeMissing: true,
|
|
2590
2660
|
hint: "This Solgrid API may not support this command yet, or SOLGRID_API_BASE_URL points to the wrong server."
|
|
2591
2661
|
});
|
|
2592
2662
|
}
|
|
2593
|
-
const bodyCode = typeof envelope?.code === "number" && envelope.code >= 400 ? envelope.code : undefined;
|
|
2663
|
+
const bodyCode = typeof envelope?.code === "number" && envelope.code >= 400 && envelope.code <= 599 ? envelope.code : undefined;
|
|
2594
2664
|
const status = bodyCode ?? (response.ok ? 400 : response.status);
|
|
2595
|
-
const
|
|
2665
|
+
const serverMessage = typeof envelope?.message === "string" && envelope.message ? envelope.message : undefined;
|
|
2666
|
+
const key = errorKeyOf(envelope);
|
|
2596
2667
|
const details = [
|
|
2597
|
-
...validationDetails(envelope?.errorData),
|
|
2668
|
+
...validationDetails(envelope?.errorData, key),
|
|
2598
2669
|
...frameworkErrorDetails(envelope?.errors)
|
|
2599
2670
|
];
|
|
2600
2671
|
const serverJsonHint = hasSerializerErrors(envelope?.errors) ? "The server failed to read JSON while handling this request. This is most likely a problem on the Solgrid side, not in your input; try again later or report it." : undefined;
|
|
2601
|
-
const message =
|
|
2672
|
+
const message = serverMessage ?? (envelope ? `HTTP ${response.status}` : `HTTP ${response.status}: ${describeBody(response, text)}`);
|
|
2602
2673
|
return new ApiError(errorCodeForStatus(status), message, {
|
|
2603
2674
|
status,
|
|
2604
2675
|
...key ? { key } : {},
|
|
2676
|
+
...auth,
|
|
2605
2677
|
...serverJsonHint ? { hint: serverJsonHint } : {},
|
|
2606
2678
|
details
|
|
2607
2679
|
});
|
|
@@ -2616,10 +2688,21 @@ function hasSerializerErrors(errors) {
|
|
|
2616
2688
|
const value = errors?.serializerErrors;
|
|
2617
2689
|
return Array.isArray(value) ? value.length > 0 : typeof value === "string" && value.length > 0;
|
|
2618
2690
|
}
|
|
2619
|
-
|
|
2691
|
+
var ERROR_KEY = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)+$/;
|
|
2692
|
+
function errorKeyOf(envelope) {
|
|
2693
|
+
const items = Array.isArray(envelope?.errorData) ? envelope.errorData : [];
|
|
2694
|
+
const item = items.find((entry) => entry && !entry.propertyName && typeof entry.errorCode === "string" && ERROR_KEY.test(entry.errorCode));
|
|
2695
|
+
if (item?.errorCode)
|
|
2696
|
+
return item.errorCode;
|
|
2697
|
+
const message = envelope?.message;
|
|
2698
|
+
return typeof message === "string" && ERROR_KEY.test(message) ? message : undefined;
|
|
2699
|
+
}
|
|
2700
|
+
function validationDetails(items, key) {
|
|
2620
2701
|
if (!Array.isArray(items))
|
|
2621
2702
|
return [];
|
|
2622
2703
|
return items.flatMap((item) => {
|
|
2704
|
+
if (key && !item?.propertyName && item?.errorCode === key)
|
|
2705
|
+
return [];
|
|
2623
2706
|
const text = item?.errorMessage?.trim();
|
|
2624
2707
|
if (!text)
|
|
2625
2708
|
return [];
|
|
@@ -2677,6 +2760,62 @@ var KEY_PRESENTATIONS = {
|
|
|
2677
2760
|
message: "This tool is not available to run.",
|
|
2678
2761
|
hint: "Check the provider and endpoint with `solgrid inspect`, or pick another tool with `solgrid discover`."
|
|
2679
2762
|
},
|
|
2763
|
+
skill_archive_size_invalid: {
|
|
2764
|
+
message: "The Skill package is too large unpacked.",
|
|
2765
|
+
hint: "A Skill may hold at most 10,000,000 bytes of files."
|
|
2766
|
+
},
|
|
2767
|
+
skill_archive_entries_invalid: {
|
|
2768
|
+
message: "The Skill package is empty or has too many entries (at most 1,000).",
|
|
2769
|
+
hint: undefined
|
|
2770
|
+
},
|
|
2771
|
+
skill_archive_file_count_exceeded: {
|
|
2772
|
+
message: "The Skill package has more than 200 files.",
|
|
2773
|
+
hint: undefined
|
|
2774
|
+
},
|
|
2775
|
+
skill_archive_path_invalid: {
|
|
2776
|
+
message: "The Skill package has a path SkillHub does not accept.",
|
|
2777
|
+
hint: "Remove links, encrypted entries, absolute paths, `..`, backslashes, and names that differ only in case."
|
|
2778
|
+
},
|
|
2779
|
+
skill_archive_scripts_not_supported: {
|
|
2780
|
+
message: "This Solgrid server does not accept a scripts directory in a Skill.",
|
|
2781
|
+
hint: "Remove the scripts directories, or try again once the server allows them."
|
|
2782
|
+
},
|
|
2783
|
+
skill_archive_structure_invalid: {
|
|
2784
|
+
message: "The Skill package needs SKILL.md at its top, or inside a single top-level directory.",
|
|
2785
|
+
hint: "Pack the Skill directory with `--dir` and the CLI builds the right layout."
|
|
2786
|
+
},
|
|
2787
|
+
skill_archive_markdown_invalid: {
|
|
2788
|
+
message: "SKILL.md in the package is empty or not valid UTF-8.",
|
|
2789
|
+
hint: undefined
|
|
2790
|
+
},
|
|
2791
|
+
skill_archive_frontmatter_invalid: {
|
|
2792
|
+
message: "SKILL.md must start with YAML frontmatter between --- lines, followed by the instructions.",
|
|
2793
|
+
hint: "The frontmatter needs `name: <the Skill's slug>` and `description: <what it does>`."
|
|
2794
|
+
},
|
|
2795
|
+
skill_archive_name_invalid: {
|
|
2796
|
+
message: "The name in SKILL.md's frontmatter is missing or not valid.",
|
|
2797
|
+
hint: 'Use 1–64 lowercase letters, digits or hyphens, not starting or ending with a hyphen and without "--".'
|
|
2798
|
+
},
|
|
2799
|
+
skill_archive_description_invalid: {
|
|
2800
|
+
message: "The description in SKILL.md's frontmatter is missing or longer than 1,024 characters.",
|
|
2801
|
+
hint: undefined
|
|
2802
|
+
},
|
|
2803
|
+
creative_case_skill_name_mismatch: {
|
|
2804
|
+
message: "The name in SKILL.md must equal the Skill's slug.",
|
|
2805
|
+
hint: "Set `name: <slug>` in SKILL.md's frontmatter, or pass the matching --slug."
|
|
2806
|
+
},
|
|
2807
|
+
creative_case_slug_invalid: {
|
|
2808
|
+
message: "The slug is not valid.",
|
|
2809
|
+
hint: 'Use 1–64 lowercase letters, digits or hyphens, not starting or ending with a hyphen and without "--".'
|
|
2810
|
+
},
|
|
2811
|
+
skill_archive_content_invalid: {
|
|
2812
|
+
message: "The Skill package is damaged or could not be unpacked.",
|
|
2813
|
+
hint: "Pack it again, e.g. with `--dir`."
|
|
2814
|
+
},
|
|
2815
|
+
file_content_type_not_allowed: {
|
|
2816
|
+
message: "Solgrid does not accept this kind of file.",
|
|
2817
|
+
hint: undefined
|
|
2818
|
+
},
|
|
2680
2819
|
file_size_invalid: {
|
|
2681
2820
|
message: "The file is larger than Solgrid accepts for its type.",
|
|
2682
2821
|
hint: "Limits: images and audio 20 MiB, video 200 MiB, Skill ZIP 10 MB."
|
|
@@ -2703,7 +2842,10 @@ function presentError(error) {
|
|
|
2703
2842
|
}
|
|
2704
2843
|
function presentApiError(error) {
|
|
2705
2844
|
const base = explain(error);
|
|
2706
|
-
const
|
|
2845
|
+
const steps = [base.hint, error.hint];
|
|
2846
|
+
if (error.code === "AUTH_FAILED")
|
|
2847
|
+
steps.reverse();
|
|
2848
|
+
const hint = steps.filter(Boolean).join(`
|
|
2707
2849
|
`) || undefined;
|
|
2708
2850
|
return { message: base.message, hint };
|
|
2709
2851
|
}
|
|
@@ -2713,6 +2855,12 @@ function explain(error) {
|
|
|
2713
2855
|
return known;
|
|
2714
2856
|
switch (error.code) {
|
|
2715
2857
|
case "AUTH_FAILED":
|
|
2858
|
+
if (error.signedUrl) {
|
|
2859
|
+
return {
|
|
2860
|
+
message: "The download link was refused.",
|
|
2861
|
+
hint: "Signed links expire after 24 hours. Get a fresh one (e.g. `solgrid files url -i <fileId>`) and try again."
|
|
2862
|
+
};
|
|
2863
|
+
}
|
|
2716
2864
|
return {
|
|
2717
2865
|
message: "The API key is missing, invalid or revoked.",
|
|
2718
2866
|
hint: `Create a key at ${CONSOLE_URLS.apiKeys}, then run: solgrid keys add -k <key> -l <label>`
|
|
@@ -2720,7 +2868,10 @@ function explain(error) {
|
|
|
2720
2868
|
case "INVALID_INPUT":
|
|
2721
2869
|
return { message: `Invalid request: ${error.message}`, hint: undefined };
|
|
2722
2870
|
case "NOT_FOUND":
|
|
2723
|
-
return {
|
|
2871
|
+
return {
|
|
2872
|
+
message: error.message === error.key ? `Not found: ${error.message}` : error.message,
|
|
2873
|
+
hint: undefined
|
|
2874
|
+
};
|
|
2724
2875
|
case "TOO_LARGE":
|
|
2725
2876
|
return {
|
|
2726
2877
|
message: "The upload was rejected as too large.",
|
|
@@ -2864,13 +3015,14 @@ async function runAction(ctx, json, body) {
|
|
|
2864
3015
|
try {
|
|
2865
3016
|
await body(out);
|
|
2866
3017
|
} catch (error) {
|
|
2867
|
-
out.error(error);
|
|
3018
|
+
out.error(withAuthContext(ctx, error));
|
|
2868
3019
|
ctx.exitCode = 1;
|
|
2869
3020
|
}
|
|
2870
3021
|
}
|
|
2871
3022
|
function configStore(ctx) {
|
|
2872
3023
|
return new ConfigStore(resolveConfigDir(ctx.env, ctx.homeDir));
|
|
2873
3024
|
}
|
|
3025
|
+
var sentKeys = new WeakMap;
|
|
2874
3026
|
function apiClient(ctx) {
|
|
2875
3027
|
const resolved = resolveApiKey(configStore(ctx), ctx.env);
|
|
2876
3028
|
if (!resolved) {
|
|
@@ -2878,6 +3030,7 @@ function apiClient(ctx) {
|
|
|
2878
3030
|
hint: `Create a key at ${CONSOLE_URLS.apiKeys}, then run: solgrid keys add -k <key> -l <label>`
|
|
2879
3031
|
});
|
|
2880
3032
|
}
|
|
3033
|
+
sentKeys.set(ctx, resolved);
|
|
2881
3034
|
return new ApiClient({
|
|
2882
3035
|
baseUrl: ctx.env[ENV.apiBaseUrl]?.trim() || DEFAULT_API_BASE_URL,
|
|
2883
3036
|
apiKey: resolved.key,
|
|
@@ -2885,6 +3038,21 @@ function apiClient(ctx) {
|
|
|
2885
3038
|
fetch: ctx.fetch
|
|
2886
3039
|
});
|
|
2887
3040
|
}
|
|
3041
|
+
function withAuthContext(ctx, error) {
|
|
3042
|
+
const key = sentKeys.get(ctx);
|
|
3043
|
+
if (!(error instanceof ApiError) || error.code !== "AUTH_FAILED" || !key)
|
|
3044
|
+
return error;
|
|
3045
|
+
const baseUrl = error.apiKeySentTo;
|
|
3046
|
+
if (!baseUrl)
|
|
3047
|
+
return error;
|
|
3048
|
+
const which = key.source === "env" ? `the key from ${ENV.apiKey} (${maskApiKey(key.key)})` : `key "${key.label}" (${maskApiKey(key.key)})`;
|
|
3049
|
+
const where = baseUrl === DEFAULT_API_BASE_URL ? `${baseUrl}, the production API` : `${baseUrl}, set by ${ENV.apiBaseUrl}`;
|
|
3050
|
+
const advice = baseUrl === DEFAULT_API_BASE_URL ? `If this key is for another environment, set ${ENV.apiBaseUrl} to that environment's API.` : `If this key is for production, unset ${ENV.apiBaseUrl}.`;
|
|
3051
|
+
return error.copyWith({
|
|
3052
|
+
hint: [`Sent ${which} to ${where}. ${advice}`, error.hint].filter(Boolean).join(`
|
|
3053
|
+
`)
|
|
3054
|
+
});
|
|
3055
|
+
}
|
|
2888
3056
|
|
|
2889
3057
|
// src/commands/account.ts
|
|
2890
3058
|
function registerAccountCommands(program, ctx) {
|
|
@@ -3417,14 +3585,19 @@ var TRANSIENT = new Set([
|
|
|
3417
3585
|
"RATE_LIMITED",
|
|
3418
3586
|
"INVALID_RESPONSE"
|
|
3419
3587
|
]);
|
|
3588
|
+
var SUBMIT_TIMEOUT_MS = 10 * 60000;
|
|
3589
|
+
var LOOKUP_TIMEOUT_MS = 30000;
|
|
3420
3590
|
var RESUBMIT_BACKOFF_MS = [1000, 2000, 4000];
|
|
3421
3591
|
var LOOKUP_BACKOFF_MS = [1000, 2000];
|
|
3422
3592
|
async function submitRun(client, request, options) {
|
|
3423
3593
|
const { idempotencyKey } = request;
|
|
3594
|
+
const budget = submitBudget(idempotencyKey, options);
|
|
3424
3595
|
for (let resubmissions = 0;; resubmissions++) {
|
|
3425
3596
|
let failure;
|
|
3426
3597
|
try {
|
|
3427
|
-
const run = await createRun(client, request, idempotencyKey
|
|
3598
|
+
const run = await createRun(client, request, idempotencyKey, {
|
|
3599
|
+
timeoutMs: budget.timeoutFor(SUBMIT_TIMEOUT_MS, { wholeBudget: true })
|
|
3600
|
+
});
|
|
3428
3601
|
return { run, recovered: false };
|
|
3429
3602
|
} catch (error) {
|
|
3430
3603
|
if (!(error instanceof ApiError) || REJECTED.has(error.code)) {
|
|
@@ -3432,31 +3605,60 @@ async function submitRun(client, request, options) {
|
|
|
3432
3605
|
}
|
|
3433
3606
|
failure = error;
|
|
3434
3607
|
}
|
|
3435
|
-
const existing = await lookupByKey(client, idempotencyKey,
|
|
3608
|
+
const existing = await lookupByKey(client, idempotencyKey, budget);
|
|
3436
3609
|
if (existing)
|
|
3437
3610
|
return { run: verifyRecovered(existing, request), recovered: true };
|
|
3438
3611
|
if (resubmissions >= RESUBMIT_BACKOFF_MS.length) {
|
|
3439
3612
|
throw withContext(failure, { idempotencyKey });
|
|
3440
3613
|
}
|
|
3441
3614
|
options.onRetry?.(resubmissions + 1, failure);
|
|
3442
|
-
await
|
|
3615
|
+
await budget.pause(RESUBMIT_BACKOFF_MS[resubmissions] ?? 4000);
|
|
3443
3616
|
}
|
|
3444
3617
|
}
|
|
3445
|
-
|
|
3618
|
+
function isNoRunForKey(error) {
|
|
3619
|
+
if (!(error instanceof ApiError) || error.code !== "NOT_FOUND" || error.routeMissing)
|
|
3620
|
+
return false;
|
|
3621
|
+
return error.key === undefined || error.key === "tool_run_not_found";
|
|
3622
|
+
}
|
|
3623
|
+
async function lookupByKey(client, idempotencyKey, budget) {
|
|
3446
3624
|
for (let attempt = 0;; attempt++) {
|
|
3447
3625
|
try {
|
|
3448
|
-
return await getRunByIdempotencyKey(client, idempotencyKey
|
|
3626
|
+
return await getRunByIdempotencyKey(client, idempotencyKey, {
|
|
3627
|
+
timeoutMs: budget.timeoutFor(LOOKUP_TIMEOUT_MS)
|
|
3628
|
+
});
|
|
3449
3629
|
} catch (error) {
|
|
3450
|
-
if (error
|
|
3630
|
+
if (isNoRunForKey(error))
|
|
3451
3631
|
return;
|
|
3632
|
+
budget.check();
|
|
3452
3633
|
const transient = error instanceof ApiError && TRANSIENT.has(error.code);
|
|
3453
3634
|
if (!transient || attempt >= LOOKUP_BACKOFF_MS.length) {
|
|
3454
3635
|
throw withContext(error, { idempotencyKey }, { lead: "Could not confirm whether the run was accepted." });
|
|
3455
3636
|
}
|
|
3456
|
-
await
|
|
3637
|
+
await budget.pause(LOOKUP_BACKOFF_MS[attempt] ?? 2000);
|
|
3457
3638
|
}
|
|
3458
3639
|
}
|
|
3459
3640
|
}
|
|
3641
|
+
function submitBudget(idempotencyKey, options) {
|
|
3642
|
+
const { deadline } = options;
|
|
3643
|
+
const left = () => deadline === undefined ? Infinity : deadline - options.now().getTime();
|
|
3644
|
+
const check = () => {
|
|
3645
|
+
if (left() <= 0)
|
|
3646
|
+
throw withContext(submitTimeout(), { idempotencyKey });
|
|
3647
|
+
};
|
|
3648
|
+
return {
|
|
3649
|
+
check,
|
|
3650
|
+
timeoutFor(cap, { wholeBudget = false } = {}) {
|
|
3651
|
+
if (deadline === undefined)
|
|
3652
|
+
return cap;
|
|
3653
|
+
check();
|
|
3654
|
+
return wholeBudget ? left() : Math.min(cap, left());
|
|
3655
|
+
},
|
|
3656
|
+
async pause(ms) {
|
|
3657
|
+
check();
|
|
3658
|
+
await options.sleep(Math.min(ms, left()));
|
|
3659
|
+
}
|
|
3660
|
+
};
|
|
3661
|
+
}
|
|
3460
3662
|
function verifyRecovered(run, request) {
|
|
3461
3663
|
const data = { idempotencyKey: request.idempotencyKey, runId: run.runId };
|
|
3462
3664
|
const check = `Inspect it with: solgrid runs get -r ${run.runId}`;
|
|
@@ -3490,18 +3692,11 @@ function withContext(error, data, options = {}) {
|
|
|
3490
3692
|
} else if (data.idempotencyKey && options.retryable !== false) {
|
|
3491
3693
|
steps.push(`Retry safely with --idempotency-key ${data.idempotencyKey}, or check: solgrid runs get --idempotency-key ${data.idempotencyKey}`);
|
|
3492
3694
|
}
|
|
3493
|
-
|
|
3695
|
+
return error.copyWith({
|
|
3494
3696
|
hint: [error.hint, ...steps].filter(Boolean).join(`
|
|
3495
3697
|
`),
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
cause: error
|
|
3499
|
-
};
|
|
3500
|
-
return error instanceof ApiError ? new ApiError(error.code, error.message, {
|
|
3501
|
-
...rebuilt,
|
|
3502
|
-
...error.status !== undefined ? { status: error.status } : {},
|
|
3503
|
-
...error.key ? { key: error.key } : {}
|
|
3504
|
-
}) : new CliError(error.code, error.message, rebuilt);
|
|
3698
|
+
data: { ...error.data, ...data }
|
|
3699
|
+
});
|
|
3505
3700
|
}
|
|
3506
3701
|
function sameInput(stored, submitted) {
|
|
3507
3702
|
return canonical(normalize(stored)) === canonical(normalize(submitted));
|
|
@@ -3534,7 +3729,7 @@ var MAX_POLL_MS = 1e4;
|
|
|
3534
3729
|
async function waitFor(fetchRun, options) {
|
|
3535
3730
|
const deadline = options.now().getTime() + options.timeoutMs;
|
|
3536
3731
|
const remaining = () => deadline - options.now().getTime();
|
|
3537
|
-
const timedOut = () => waitTimeout(options.description, last, options.timeoutMs, options.checkCommand);
|
|
3732
|
+
const timedOut = () => waitTimeout(options.description, last, options.totalMs ?? options.timeoutMs, options.checkCommand);
|
|
3538
3733
|
let delay = FIRST_POLL_MS;
|
|
3539
3734
|
let last;
|
|
3540
3735
|
for (;; ) {
|
|
@@ -3566,6 +3761,11 @@ function waitForRun(client, runId, options) {
|
|
|
3566
3761
|
checkCommand: `solgrid runs get -r ${runId} --wait`
|
|
3567
3762
|
});
|
|
3568
3763
|
}
|
|
3764
|
+
function submitTimeout() {
|
|
3765
|
+
return new CliError("WAIT_TIMEOUT", "The wait ended before the run could be confirmed.", {
|
|
3766
|
+
hint: "It may still be running on Solgrid, or may not have started."
|
|
3767
|
+
});
|
|
3768
|
+
}
|
|
3569
3769
|
function waitTimeout(description, last, timeoutMs, checkCommand) {
|
|
3570
3770
|
const runId = last?.runId;
|
|
3571
3771
|
const status = last?.status;
|
|
@@ -3721,6 +3921,8 @@ function registerRunCommand(program, ctx) {
|
|
|
3721
3921
|
const idempotencyKey = opts.idempotencyKey ?? ctx.randomUUID();
|
|
3722
3922
|
validateIdempotencyKey(idempotencyKey);
|
|
3723
3923
|
const waitSeconds = opts.wait === true ? DEFAULT_WAIT_SECONDS : opts.wait;
|
|
3924
|
+
const budgetMs = waitSeconds !== undefined || opts.output !== undefined ? (waitSeconds ?? DEFAULT_WAIT_SECONDS) * 1000 : undefined;
|
|
3925
|
+
const deadline = budgetMs !== undefined ? ctx.now().getTime() + budgetMs : undefined;
|
|
3724
3926
|
const client = apiClient(ctx);
|
|
3725
3927
|
out.progress.start(`Submitting ${opts.provider} ${opts.endpoint}…`);
|
|
3726
3928
|
const { run: submitted, recovered } = await submitRun(client, {
|
|
@@ -3729,15 +3931,17 @@ function registerRunCommand(program, ctx) {
|
|
|
3729
3931
|
...input ? { input } : {},
|
|
3730
3932
|
idempotencyKey
|
|
3731
3933
|
}, {
|
|
3934
|
+
now: () => ctx.now(),
|
|
3732
3935
|
sleep: (ms) => ctx.sleep(ms),
|
|
3733
|
-
onRetry: (attempt) => out.progress.update(`Submission interrupted, retrying (${attempt})…`)
|
|
3936
|
+
onRetry: (attempt) => out.progress.update(`Submission interrupted, retrying (${attempt})…`),
|
|
3937
|
+
...deadline !== undefined ? { deadline } : {}
|
|
3734
3938
|
});
|
|
3735
3939
|
try {
|
|
3736
3940
|
await finishRun(ctx, client, out, {
|
|
3737
3941
|
submitted,
|
|
3738
3942
|
recovered,
|
|
3739
3943
|
idempotencyKey,
|
|
3740
|
-
...
|
|
3944
|
+
...budgetMs !== undefined && deadline !== undefined ? { budgetMs, deadline } : {},
|
|
3741
3945
|
...opts.output !== undefined ? { outputPath: opts.output } : {}
|
|
3742
3946
|
});
|
|
3743
3947
|
} catch (error) {
|
|
@@ -3747,13 +3951,14 @@ function registerRunCommand(program, ctx) {
|
|
|
3747
3951
|
}
|
|
3748
3952
|
async function finishRun(ctx, client, out, params) {
|
|
3749
3953
|
const { submitted, recovered, idempotencyKey, outputPath } = params;
|
|
3750
|
-
const wait = params.
|
|
3954
|
+
const wait = params.deadline !== undefined;
|
|
3751
3955
|
if (!wait && !isTerminal(submitted.status)) {
|
|
3752
3956
|
out.result({ ...submitted, idempotencyKey, recovered }, () => renderSubmitted(submitted.runId, submitted.status, idempotencyKey, recovered, out));
|
|
3753
3957
|
return;
|
|
3754
3958
|
}
|
|
3755
3959
|
const finished = isTerminal(submitted.status) ? submitted : await waitForRun(client, submitted.runId, {
|
|
3756
|
-
timeoutMs:
|
|
3960
|
+
timeoutMs: params.deadline - ctx.now().getTime(),
|
|
3961
|
+
totalMs: params.budgetMs,
|
|
3757
3962
|
now: () => ctx.now(),
|
|
3758
3963
|
sleep: (ms) => ctx.sleep(ms),
|
|
3759
3964
|
onPoll: (run) => out.progress.update(`Waiting for ${run.runId} (${run.status})…`)
|
|
@@ -3863,6 +4068,893 @@ function registerSessionIdCommand(program, ctx) {
|
|
|
3863
4068
|
}));
|
|
3864
4069
|
}
|
|
3865
4070
|
|
|
4071
|
+
// src/commands/skills.ts
|
|
4072
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
|
|
4073
|
+
|
|
4074
|
+
// src/core/skill-markdown.ts
|
|
4075
|
+
var MAX_SKILL_NAME_LENGTH = 64;
|
|
4076
|
+
var MAX_SKILL_DESCRIPTION_LENGTH = 1024;
|
|
4077
|
+
var SKILL_NAME = /^[a-z0-9](?:[a-z0-9]|-(?!-))*$/;
|
|
4078
|
+
function isSkillName(value) {
|
|
4079
|
+
return value.length <= MAX_SKILL_NAME_LENGTH && SKILL_NAME.test(value) && !value.endsWith("-");
|
|
4080
|
+
}
|
|
4081
|
+
function readFrontmatter(text) {
|
|
4082
|
+
const lines = text.replace(/^\uFEFF/, "").split(/\r\n|\r|\n/);
|
|
4083
|
+
if (lines[0] !== "---")
|
|
4084
|
+
return;
|
|
4085
|
+
const end = lines.indexOf("---", 1);
|
|
4086
|
+
if (end === -1)
|
|
4087
|
+
return;
|
|
4088
|
+
if (!lines.slice(end + 1).join(`
|
|
4089
|
+
`).trim())
|
|
4090
|
+
return;
|
|
4091
|
+
const fields = topLevelScalars(lines.slice(1, end));
|
|
4092
|
+
return { name: fields.get("name"), description: fields.get("description") };
|
|
4093
|
+
}
|
|
4094
|
+
function frontmatterProblem(frontmatter) {
|
|
4095
|
+
if (!frontmatter)
|
|
4096
|
+
return "missing";
|
|
4097
|
+
const { name, description } = frontmatter;
|
|
4098
|
+
if (name === undefined || name !== null && !isSkillName(name))
|
|
4099
|
+
return "name";
|
|
4100
|
+
if (description === undefined || description !== null && (!description.trim() || description.length > MAX_SKILL_DESCRIPTION_LENGTH)) {
|
|
4101
|
+
return "description";
|
|
4102
|
+
}
|
|
4103
|
+
return;
|
|
4104
|
+
}
|
|
4105
|
+
function topLevelScalars(lines) {
|
|
4106
|
+
const fields = new Map;
|
|
4107
|
+
for (let i = 0;i < lines.length; i++) {
|
|
4108
|
+
const match = /^(?:"([^"\\]*)"|'([^']*)'|([^\s#:'"][^:]*?))[ \t]*:(?:[ \t]+(.*))?$/.exec(lines[i]);
|
|
4109
|
+
if (!match)
|
|
4110
|
+
continue;
|
|
4111
|
+
const key = (match[1] ?? match[2] ?? match[3]).trim();
|
|
4112
|
+
const rest = [];
|
|
4113
|
+
while (i + 1 < lines.length && /^(?:[ \t]|$)/.test(lines[i + 1])) {
|
|
4114
|
+
rest.push(lines[++i]);
|
|
4115
|
+
}
|
|
4116
|
+
let value;
|
|
4117
|
+
try {
|
|
4118
|
+
value = scalar((match[4] ?? "").trim(), rest);
|
|
4119
|
+
} catch {
|
|
4120
|
+
value = null;
|
|
4121
|
+
}
|
|
4122
|
+
if (value !== undefined)
|
|
4123
|
+
fields.set(key, value);
|
|
4124
|
+
}
|
|
4125
|
+
return fields;
|
|
4126
|
+
}
|
|
4127
|
+
var NESTED = /^(?:-(?:[ \t]|$)|(?:"[^"\\]*"|'[^']*'|[^\s#'"][^:]*?)[ \t]*:(?:[ \t]|$))/;
|
|
4128
|
+
function scalar(head, rest) {
|
|
4129
|
+
if (!head) {
|
|
4130
|
+
const index = rest.findIndex((line) => line.trim() && !line.trim().startsWith("#"));
|
|
4131
|
+
if (index === -1)
|
|
4132
|
+
return "";
|
|
4133
|
+
const first = rest[index].trim();
|
|
4134
|
+
if (NESTED.test(first))
|
|
4135
|
+
return;
|
|
4136
|
+
return scalar(first, rest.slice(index + 1));
|
|
4137
|
+
}
|
|
4138
|
+
if (head.startsWith('"') || head.startsWith("'"))
|
|
4139
|
+
return quoted([head, ...rest]);
|
|
4140
|
+
const more = rest.map((line) => line.trim()).filter(Boolean);
|
|
4141
|
+
if (/^[|>]/.test(head)) {
|
|
4142
|
+
return more.join(head.startsWith("|") ? `
|
|
4143
|
+
` : " ");
|
|
4144
|
+
}
|
|
4145
|
+
if (/^[[{]/.test(head))
|
|
4146
|
+
return;
|
|
4147
|
+
if (/^[&*!]/.test(head))
|
|
4148
|
+
return null;
|
|
4149
|
+
return [head, ...more].filter((line) => !line.startsWith("#")).join(" ").replace(/\s+#.*$/, "");
|
|
4150
|
+
}
|
|
4151
|
+
var ESCAPES = {
|
|
4152
|
+
"0": "\x00",
|
|
4153
|
+
a: "\x07",
|
|
4154
|
+
b: "\b",
|
|
4155
|
+
t: "\t",
|
|
4156
|
+
"\t": "\t",
|
|
4157
|
+
n: `
|
|
4158
|
+
`,
|
|
4159
|
+
v: "\v",
|
|
4160
|
+
f: "\f",
|
|
4161
|
+
r: "\r",
|
|
4162
|
+
e: "\x1B",
|
|
4163
|
+
" ": " ",
|
|
4164
|
+
'"': '"',
|
|
4165
|
+
"/": "/",
|
|
4166
|
+
"\\": "\\",
|
|
4167
|
+
N: "
",
|
|
4168
|
+
_: " ",
|
|
4169
|
+
L: "\u2028",
|
|
4170
|
+
P: "\u2029"
|
|
4171
|
+
};
|
|
4172
|
+
var HEX_ESCAPES = { x: 2, u: 4, U: 8 };
|
|
4173
|
+
function quoted(lines) {
|
|
4174
|
+
const text = lines.join(`
|
|
4175
|
+
`);
|
|
4176
|
+
const quote = text[0];
|
|
4177
|
+
let value = "";
|
|
4178
|
+
let i = 1;
|
|
4179
|
+
for (;; ) {
|
|
4180
|
+
if (i >= text.length)
|
|
4181
|
+
return null;
|
|
4182
|
+
const char = text[i];
|
|
4183
|
+
if (char === quote) {
|
|
4184
|
+
if (quote === "'" && text[i + 1] === "'") {
|
|
4185
|
+
value += "'";
|
|
4186
|
+
i += 2;
|
|
4187
|
+
continue;
|
|
4188
|
+
}
|
|
4189
|
+
break;
|
|
4190
|
+
}
|
|
4191
|
+
if (char === "\\" && quote === '"') {
|
|
4192
|
+
const next = text[i + 1] ?? "";
|
|
4193
|
+
if (next === `
|
|
4194
|
+
`) {
|
|
4195
|
+
i += 2;
|
|
4196
|
+
while (text[i] === " " || text[i] === "\t")
|
|
4197
|
+
i++;
|
|
4198
|
+
continue;
|
|
4199
|
+
}
|
|
4200
|
+
const digits = HEX_ESCAPES[next];
|
|
4201
|
+
if (digits !== undefined) {
|
|
4202
|
+
const hex = text.slice(i + 2, i + 2 + digits);
|
|
4203
|
+
if (!new RegExp(`^[0-9a-fA-F]{${digits}}$`).test(hex))
|
|
4204
|
+
return null;
|
|
4205
|
+
const codePoint = Number.parseInt(hex, 16);
|
|
4206
|
+
if (codePoint > 1114111)
|
|
4207
|
+
return null;
|
|
4208
|
+
value += String.fromCodePoint(codePoint);
|
|
4209
|
+
i += 2 + digits;
|
|
4210
|
+
continue;
|
|
4211
|
+
}
|
|
4212
|
+
const escaped = ESCAPES[next];
|
|
4213
|
+
if (escaped === undefined)
|
|
4214
|
+
return null;
|
|
4215
|
+
value += escaped;
|
|
4216
|
+
i += 2;
|
|
4217
|
+
continue;
|
|
4218
|
+
}
|
|
4219
|
+
if (char === `
|
|
4220
|
+
`) {
|
|
4221
|
+
value = value.replace(/[ \t]+$/, "");
|
|
4222
|
+
let breaks = 0;
|
|
4223
|
+
while (text[i] === `
|
|
4224
|
+
`) {
|
|
4225
|
+
breaks++;
|
|
4226
|
+
i++;
|
|
4227
|
+
while (text[i] === " " || text[i] === "\t")
|
|
4228
|
+
i++;
|
|
4229
|
+
}
|
|
4230
|
+
value += breaks === 1 ? " " : `
|
|
4231
|
+
`.repeat(breaks - 1);
|
|
4232
|
+
continue;
|
|
4233
|
+
}
|
|
4234
|
+
value += char;
|
|
4235
|
+
i++;
|
|
4236
|
+
}
|
|
4237
|
+
return /^[ \t]*(?:#[^\n]*)?(?:\n[ \t]*(?:#[^\n]*)?)*$/.test(text.slice(i + 1)) ? value : null;
|
|
4238
|
+
}
|
|
4239
|
+
|
|
4240
|
+
// src/core/skill-package.ts
|
|
4241
|
+
import { lstatSync, mkdtempSync, readdirSync, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4242
|
+
import { tmpdir } from "node:os";
|
|
4243
|
+
import { basename as basename2, join as join3, relative, sep } from "node:path";
|
|
4244
|
+
import { deflateRawSync } from "node:zlib";
|
|
4245
|
+
var MAX_SKILL_BYTES = 1e7;
|
|
4246
|
+
var MAX_SKILL_FILES = 200;
|
|
4247
|
+
var MAX_DEPTH = 16;
|
|
4248
|
+
var MAX_PATH_LENGTH = 1024;
|
|
4249
|
+
var SKIPPED = new Set([".git", ".DS_Store"]);
|
|
4250
|
+
function invalid(dir, reason, hint) {
|
|
4251
|
+
return new CliError("USAGE", `${dir} is not a valid Skill: ${reason}.`, hint ? { hint } : {});
|
|
4252
|
+
}
|
|
4253
|
+
function collect(dir) {
|
|
4254
|
+
const entries = [];
|
|
4255
|
+
const seen = new Map;
|
|
4256
|
+
let total = 0;
|
|
4257
|
+
const walk = (current, depth) => {
|
|
4258
|
+
for (const name of readdirSync(current).sort()) {
|
|
4259
|
+
if (SKIPPED.has(name))
|
|
4260
|
+
continue;
|
|
4261
|
+
const full = join3(current, name);
|
|
4262
|
+
const zipName = relative(dir, full).split(sep).join("/");
|
|
4263
|
+
if (/[:\\\u0000-\u001f\u007f]/.test(name)) {
|
|
4264
|
+
throw invalid(dir, `"${zipName}" has a ":", a backslash or a control character in its name`);
|
|
4265
|
+
}
|
|
4266
|
+
if (zipName.length > MAX_PATH_LENGTH || depth > MAX_DEPTH) {
|
|
4267
|
+
throw invalid(dir, `"${zipName}" is nested too deeply or its path is too long`);
|
|
4268
|
+
}
|
|
4269
|
+
const lower = zipName.toLowerCase();
|
|
4270
|
+
const clash = seen.get(lower);
|
|
4271
|
+
if (clash)
|
|
4272
|
+
throw invalid(dir, `"${clash}" and "${zipName}" differ only in case`);
|
|
4273
|
+
seen.set(lower, zipName);
|
|
4274
|
+
const stat = lstatSync(full);
|
|
4275
|
+
if (stat.isSymbolicLink()) {
|
|
4276
|
+
throw invalid(dir, `"${zipName}" is a symbolic link`, "Replace links with the files themselves.");
|
|
4277
|
+
}
|
|
4278
|
+
if (stat.isDirectory()) {
|
|
4279
|
+
walk(full, depth + 1);
|
|
4280
|
+
} else if (stat.isFile()) {
|
|
4281
|
+
if (entries.length >= MAX_SKILL_FILES) {
|
|
4282
|
+
throw invalid(dir, `it has more than ${MAX_SKILL_FILES} files`);
|
|
4283
|
+
}
|
|
4284
|
+
total += stat.size;
|
|
4285
|
+
if (total > MAX_SKILL_BYTES) {
|
|
4286
|
+
throw invalid(dir, `its files add up to more than ${MAX_SKILL_BYTES.toLocaleString("en-US")} bytes`);
|
|
4287
|
+
}
|
|
4288
|
+
entries.push({ name: zipName, data: readFileSync3(full) });
|
|
4289
|
+
} else {
|
|
4290
|
+
throw invalid(dir, `"${zipName}" is not a regular file or directory`);
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
};
|
|
4294
|
+
walk(dir, 1);
|
|
4295
|
+
return entries;
|
|
4296
|
+
}
|
|
4297
|
+
var FRONTMATTER_EXAMPLE = `Start SKILL.md with:
|
|
4298
|
+
---
|
|
4299
|
+
name: my-skill
|
|
4300
|
+
description: What the Skill does and when to use it.
|
|
4301
|
+
---
|
|
4302
|
+
followed by the instructions.`;
|
|
4303
|
+
function checkSkillMarkdown(dir, entries) {
|
|
4304
|
+
const markdown = entries.find((entry) => entry.name === "SKILL.md");
|
|
4305
|
+
if (!markdown) {
|
|
4306
|
+
const wrongCase = entries.find((entry) => entry.name.toLowerCase() === "skill.md");
|
|
4307
|
+
throw invalid(dir, wrongCase ? `the file must be named SKILL.md, not ${wrongCase.name}` : "it has no SKILL.md at the top", "Put SKILL.md directly in the directory you pass to --dir.");
|
|
4308
|
+
}
|
|
4309
|
+
let text;
|
|
4310
|
+
try {
|
|
4311
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(markdown.data);
|
|
4312
|
+
} catch {
|
|
4313
|
+
throw invalid(dir, "SKILL.md is not valid UTF-8");
|
|
4314
|
+
}
|
|
4315
|
+
if (!text.trim())
|
|
4316
|
+
throw invalid(dir, "SKILL.md is empty");
|
|
4317
|
+
const frontmatter = readFrontmatter(text);
|
|
4318
|
+
switch (frontmatterProblem(frontmatter)) {
|
|
4319
|
+
case "missing":
|
|
4320
|
+
throw invalid(dir, "SKILL.md must start with frontmatter between --- lines, followed by the instructions", FRONTMATTER_EXAMPLE);
|
|
4321
|
+
case "name":
|
|
4322
|
+
throw invalid(dir, frontmatter?.name === undefined ? "SKILL.md's frontmatter has no name" : `the name "${frontmatter.name}" in SKILL.md is not valid`, `Use 1–64 lowercase letters, digits or hyphens, not starting or ending with a hyphen and without "--". It must equal the Skill's slug.`);
|
|
4323
|
+
case "description":
|
|
4324
|
+
throw invalid(dir, `SKILL.md's frontmatter needs a description of 1–${MAX_SKILL_DESCRIPTION_LENGTH.toLocaleString("en-US")} characters`, "Say what the Skill does and when to use it.");
|
|
4325
|
+
}
|
|
4326
|
+
return frontmatter?.name ?? undefined;
|
|
4327
|
+
}
|
|
4328
|
+
function packSkillDirectory(dir) {
|
|
4329
|
+
try {
|
|
4330
|
+
if (!lstatSync(dir).isDirectory())
|
|
4331
|
+
throw new Error("not a directory");
|
|
4332
|
+
} catch (cause) {
|
|
4333
|
+
throw new CliError("USAGE", `Cannot read the directory ${dir}.`, { cause });
|
|
4334
|
+
}
|
|
4335
|
+
const entries = collect(dir);
|
|
4336
|
+
const name = checkSkillMarkdown(dir, entries);
|
|
4337
|
+
const zip = buildZip(entries);
|
|
4338
|
+
if (zip.length > MAX_SKILL_BYTES) {
|
|
4339
|
+
throw invalid(dir, `the package would be ${zip.length.toLocaleString("en-US")} bytes, over the limit`);
|
|
4340
|
+
}
|
|
4341
|
+
const temp = mkdtempSync(join3(tmpdir(), "solgrid-skill-"));
|
|
4342
|
+
const path = join3(temp, `${basename2(dir) || "skill"}.zip`);
|
|
4343
|
+
writeFileSync3(path, zip);
|
|
4344
|
+
return {
|
|
4345
|
+
path,
|
|
4346
|
+
files: entries.length,
|
|
4347
|
+
bytes: zip.length,
|
|
4348
|
+
name,
|
|
4349
|
+
cleanup: () => rmSync(temp, { recursive: true, force: true })
|
|
4350
|
+
};
|
|
4351
|
+
}
|
|
4352
|
+
var CRC_TABLE = (() => {
|
|
4353
|
+
const table = new Uint32Array(256);
|
|
4354
|
+
for (let n = 0;n < 256; n++) {
|
|
4355
|
+
let c = n;
|
|
4356
|
+
for (let k = 0;k < 8; k++)
|
|
4357
|
+
c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
4358
|
+
table[n] = c >>> 0;
|
|
4359
|
+
}
|
|
4360
|
+
return table;
|
|
4361
|
+
})();
|
|
4362
|
+
function crc32(data) {
|
|
4363
|
+
let crc = 4294967295;
|
|
4364
|
+
for (const byte of data)
|
|
4365
|
+
crc = CRC_TABLE[(crc ^ byte) & 255] ^ crc >>> 8;
|
|
4366
|
+
return (crc ^ 4294967295) >>> 0;
|
|
4367
|
+
}
|
|
4368
|
+
var DOS_TIME = 0;
|
|
4369
|
+
var DOS_DATE = 1 << 5 | 1;
|
|
4370
|
+
var UTF8_NAMES = 2048;
|
|
4371
|
+
function buildZip(entries) {
|
|
4372
|
+
const parts = [];
|
|
4373
|
+
const central = [];
|
|
4374
|
+
let offset = 0;
|
|
4375
|
+
for (const entry of entries) {
|
|
4376
|
+
const name = Buffer.from(entry.name, "utf8");
|
|
4377
|
+
const deflated = deflateRawSync(entry.data);
|
|
4378
|
+
const stored = deflated.length >= entry.data.length;
|
|
4379
|
+
const body = stored ? Buffer.from(entry.data) : deflated;
|
|
4380
|
+
const method = stored ? 0 : 8;
|
|
4381
|
+
const crc = crc32(entry.data);
|
|
4382
|
+
const local = Buffer.alloc(30);
|
|
4383
|
+
local.writeUInt32LE(67324752, 0);
|
|
4384
|
+
local.writeUInt16LE(20, 4);
|
|
4385
|
+
local.writeUInt16LE(UTF8_NAMES, 6);
|
|
4386
|
+
local.writeUInt16LE(method, 8);
|
|
4387
|
+
local.writeUInt16LE(DOS_TIME, 10);
|
|
4388
|
+
local.writeUInt16LE(DOS_DATE, 12);
|
|
4389
|
+
local.writeUInt32LE(crc, 14);
|
|
4390
|
+
local.writeUInt32LE(body.length, 18);
|
|
4391
|
+
local.writeUInt32LE(entry.data.length, 22);
|
|
4392
|
+
local.writeUInt16LE(name.length, 26);
|
|
4393
|
+
local.writeUInt16LE(0, 28);
|
|
4394
|
+
parts.push(local, name, body);
|
|
4395
|
+
const header = Buffer.alloc(46);
|
|
4396
|
+
header.writeUInt32LE(33639248, 0);
|
|
4397
|
+
header.writeUInt16LE(3 << 8 | 20, 4);
|
|
4398
|
+
header.writeUInt16LE(20, 6);
|
|
4399
|
+
header.writeUInt16LE(UTF8_NAMES, 8);
|
|
4400
|
+
header.writeUInt16LE(method, 10);
|
|
4401
|
+
header.writeUInt16LE(DOS_TIME, 12);
|
|
4402
|
+
header.writeUInt16LE(DOS_DATE, 14);
|
|
4403
|
+
header.writeUInt32LE(crc, 16);
|
|
4404
|
+
header.writeUInt32LE(body.length, 20);
|
|
4405
|
+
header.writeUInt32LE(entry.data.length, 24);
|
|
4406
|
+
header.writeUInt16LE(name.length, 28);
|
|
4407
|
+
header.writeUInt32LE(33188 << 16 >>> 0, 38);
|
|
4408
|
+
header.writeUInt32LE(offset, 42);
|
|
4409
|
+
central.push(header, name);
|
|
4410
|
+
offset += local.length + name.length + body.length;
|
|
4411
|
+
}
|
|
4412
|
+
const directory = Buffer.concat(central);
|
|
4413
|
+
const end = Buffer.alloc(22);
|
|
4414
|
+
end.writeUInt32LE(101010256, 0);
|
|
4415
|
+
end.writeUInt16LE(entries.length, 8);
|
|
4416
|
+
end.writeUInt16LE(entries.length, 10);
|
|
4417
|
+
end.writeUInt32LE(directory.length, 12);
|
|
4418
|
+
end.writeUInt32LE(offset, 16);
|
|
4419
|
+
return Buffer.concat([...parts, directory, end]);
|
|
4420
|
+
}
|
|
4421
|
+
|
|
4422
|
+
// src/core/skills.ts
|
|
4423
|
+
import { openAsBlob as openAsBlob2, statSync as statSync2 } from "node:fs";
|
|
4424
|
+
import { basename as basename3, extname as extname2 } from "node:path";
|
|
4425
|
+
var SKILL_KINDS = ["image", "video", "web", "audio"];
|
|
4426
|
+
var KIND_NAME = {
|
|
4427
|
+
image: "Image",
|
|
4428
|
+
video: "Video",
|
|
4429
|
+
web: "Web",
|
|
4430
|
+
audio: "Audio"
|
|
4431
|
+
};
|
|
4432
|
+
var KIND_BY_NUMBER2 = { 1: "image", 2: "video", 3: "web", 4: "audio" };
|
|
4433
|
+
function kindName(value) {
|
|
4434
|
+
if (typeof value === "number")
|
|
4435
|
+
return KIND_BY_NUMBER2[value];
|
|
4436
|
+
if (typeof value === "string") {
|
|
4437
|
+
const name = value.toLowerCase();
|
|
4438
|
+
return SKILL_KINDS.find((kind) => kind === name);
|
|
4439
|
+
}
|
|
4440
|
+
return;
|
|
4441
|
+
}
|
|
4442
|
+
var CARD_FIELDS = {
|
|
4443
|
+
id: isString,
|
|
4444
|
+
title: isText,
|
|
4445
|
+
tags: optional(listOf(isText)),
|
|
4446
|
+
author: optional(objectWith({ nickname: optional(isText) }))
|
|
4447
|
+
};
|
|
4448
|
+
async function listSkills(client, params) {
|
|
4449
|
+
const path = params.mine ? "/v1/creative-cases/list-mine" : "/v1/creative-cases/list";
|
|
4450
|
+
const data = await client.post(path, {
|
|
4451
|
+
...params.page !== undefined ? { pageIndex: params.page } : {},
|
|
4452
|
+
...params.pageSize !== undefined ? { pageSize: params.pageSize } : {},
|
|
4453
|
+
...params.kind ? { kind: KIND_NAME[params.kind] } : {},
|
|
4454
|
+
...params.keyword ? { keyword: params.keyword } : {},
|
|
4455
|
+
...params.slug ? { slug: params.slug } : {}
|
|
4456
|
+
});
|
|
4457
|
+
return expectShape(path, data, {
|
|
4458
|
+
items: listOf(objectWith(CARD_FIELDS)),
|
|
4459
|
+
total: isNumber,
|
|
4460
|
+
pageIndex: isNumber,
|
|
4461
|
+
pageSize: isNumber
|
|
4462
|
+
});
|
|
4463
|
+
}
|
|
4464
|
+
async function getSkill(client, target) {
|
|
4465
|
+
const path = "/v1/creative-cases/get";
|
|
4466
|
+
const data = await client.post(path, target);
|
|
4467
|
+
return expectShape(path, data, {
|
|
4468
|
+
...CARD_FIELDS,
|
|
4469
|
+
slug: isString,
|
|
4470
|
+
description: isText,
|
|
4471
|
+
skillFileAssetId: isString
|
|
4472
|
+
});
|
|
4473
|
+
}
|
|
4474
|
+
function contentBody(content) {
|
|
4475
|
+
return {
|
|
4476
|
+
title: content.title,
|
|
4477
|
+
kind: KIND_NAME[content.kind],
|
|
4478
|
+
description: content.description,
|
|
4479
|
+
tags: content.tags,
|
|
4480
|
+
coverFileAssetId: content.coverFileAssetId ?? null,
|
|
4481
|
+
previewFileAssetId: content.previewFileAssetId ?? null,
|
|
4482
|
+
skillFileAssetId: content.skillFileAssetId
|
|
4483
|
+
};
|
|
4484
|
+
}
|
|
4485
|
+
var MUTATION_FIELDS = { id: isString };
|
|
4486
|
+
async function createSkill(client, slug, content) {
|
|
4487
|
+
const path = "/v1/creative-cases/create";
|
|
4488
|
+
const data = await client.post(path, { ...contentBody(content), slug });
|
|
4489
|
+
return expectShape(path, data, MUTATION_FIELDS);
|
|
4490
|
+
}
|
|
4491
|
+
async function updateSkill(client, id, content) {
|
|
4492
|
+
const path = "/v1/creative-cases/update";
|
|
4493
|
+
const data = await client.post(path, { id, ...contentBody(content) });
|
|
4494
|
+
return expectShape(path, data, MUTATION_FIELDS);
|
|
4495
|
+
}
|
|
4496
|
+
async function deleteSkill(client, id) {
|
|
4497
|
+
await client.post("/v1/creative-cases/delete", { id });
|
|
4498
|
+
}
|
|
4499
|
+
function contentOf(skill) {
|
|
4500
|
+
const kind = kindName(skill.kind);
|
|
4501
|
+
if (!kind) {
|
|
4502
|
+
throw new CliError("INVALID_RESPONSE", `Unexpected kind ${JSON.stringify(skill.kind)} for this Skill.`);
|
|
4503
|
+
}
|
|
4504
|
+
return {
|
|
4505
|
+
title: skill.title,
|
|
4506
|
+
kind,
|
|
4507
|
+
description: skill.description,
|
|
4508
|
+
tags: skill.tags ?? [],
|
|
4509
|
+
coverFileAssetId: skill.cover?.fileAssetId ?? null,
|
|
4510
|
+
previewFileAssetId: skill.previewFileAssetId ?? null,
|
|
4511
|
+
skillFileAssetId: skill.skillFileAssetId
|
|
4512
|
+
};
|
|
4513
|
+
}
|
|
4514
|
+
var MAX_TAGS = 3;
|
|
4515
|
+
async function listSkillTags(client) {
|
|
4516
|
+
const path = "/v1/creative-cases/list-tags";
|
|
4517
|
+
const data = await client.post(path);
|
|
4518
|
+
const checked = expectShape(path, { tags: data }, {
|
|
4519
|
+
tags: listOf(objectWith({ id: isString, name: isText }))
|
|
4520
|
+
});
|
|
4521
|
+
return checked.tags;
|
|
4522
|
+
}
|
|
4523
|
+
function checkTags(tags, available) {
|
|
4524
|
+
if (tags.length > MAX_TAGS) {
|
|
4525
|
+
throw new CliError("USAGE", `A Skill takes at most ${MAX_TAGS} tags; ${tags.length} were given.`);
|
|
4526
|
+
}
|
|
4527
|
+
const known = new Set(available.map((tag) => tag.id));
|
|
4528
|
+
const unknown = tags.filter((tag) => !known.has(tag));
|
|
4529
|
+
if (unknown.length > 0) {
|
|
4530
|
+
throw new CliError("USAGE", `Unknown tag${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}.`, {
|
|
4531
|
+
hint: `Pick from: ${available.map((tag) => tag.id).join(", ")} (see \`solgrid skills tags\`).`
|
|
4532
|
+
});
|
|
4533
|
+
}
|
|
4534
|
+
}
|
|
4535
|
+
var PUBLIC_DISPLAY = "1";
|
|
4536
|
+
function prepareSkillFile(path, role) {
|
|
4537
|
+
let size;
|
|
4538
|
+
try {
|
|
4539
|
+
const stat = statSync2(path);
|
|
4540
|
+
if (!stat.isFile())
|
|
4541
|
+
throw new Error("not a file");
|
|
4542
|
+
size = stat.size;
|
|
4543
|
+
} catch (cause) {
|
|
4544
|
+
throw new CliError("USAGE", `Cannot read ${path}.`, { cause });
|
|
4545
|
+
}
|
|
4546
|
+
if (size === 0)
|
|
4547
|
+
throw new CliError("USAGE", `${path} is empty.`);
|
|
4548
|
+
const kind = guessKind(path);
|
|
4549
|
+
const allowed = {
|
|
4550
|
+
cover: ["image"],
|
|
4551
|
+
preview: ["image", "video", "audio"],
|
|
4552
|
+
skill: ["skill"]
|
|
4553
|
+
};
|
|
4554
|
+
if (!kind || !allowed[role].includes(kind)) {
|
|
4555
|
+
const expected = role === "skill" ? "a .zip file" : `an ${allowed[role].join(", ")} file`;
|
|
4556
|
+
throw new CliError("USAGE", `The ${role === "skill" ? "Skill package" : role} must be ${expected}: ${basename3(path)}.`);
|
|
4557
|
+
}
|
|
4558
|
+
if (size > SIZE_LIMIT[kind]) {
|
|
4559
|
+
throw new CliError("USAGE", `${basename3(path)} is ${formatBytes(size)}, over the ${formatBytes(SIZE_LIMIT[kind])} limit for ${kind} files.`);
|
|
4560
|
+
}
|
|
4561
|
+
return { kind, size };
|
|
4562
|
+
}
|
|
4563
|
+
var CONTENT_TYPE = {
|
|
4564
|
+
image: {
|
|
4565
|
+
".png": "image/png",
|
|
4566
|
+
".jpg": "image/jpeg",
|
|
4567
|
+
".jpeg": "image/jpeg",
|
|
4568
|
+
".webp": "image/webp",
|
|
4569
|
+
".gif": "image/gif"
|
|
4570
|
+
},
|
|
4571
|
+
video: { ".mp4": "video/mp4", ".mov": "video/quicktime", ".webm": "video/webm" },
|
|
4572
|
+
audio: { ".mp3": "audio/mpeg", ".wav": "audio/wav", ".m4a": "audio/mp4", ".ogg": "audio/ogg" },
|
|
4573
|
+
skill: { ".zip": "application/zip" }
|
|
4574
|
+
};
|
|
4575
|
+
async function uploadSkillFile(client, path, role) {
|
|
4576
|
+
const { kind } = prepareSkillFile(path, role);
|
|
4577
|
+
const form = new FormData;
|
|
4578
|
+
form.append("fileType", String(FILE_TYPE_CODE[kind]));
|
|
4579
|
+
form.append("purpose", PUBLIC_DISPLAY);
|
|
4580
|
+
const type = CONTENT_TYPE[kind][extname2(path).toLowerCase()] ?? "application/octet-stream";
|
|
4581
|
+
form.append("file", await openAsBlob2(path, { type }), basename3(path));
|
|
4582
|
+
const data = await client.upload("/v1/files/upload", form);
|
|
4583
|
+
return expectShape("/v1/files/upload", data, { fileAssetId: isString });
|
|
4584
|
+
}
|
|
4585
|
+
function openSkillPackage(client, slug) {
|
|
4586
|
+
return client.openDownload(`/v1/skills/${encodeURIComponent(slug)}/content`);
|
|
4587
|
+
}
|
|
4588
|
+
|
|
4589
|
+
// src/commands/skills.ts
|
|
4590
|
+
var MAX_INLINE_DESCRIPTION_CHARS = 1500;
|
|
4591
|
+
function parseKind2(value) {
|
|
4592
|
+
const kind = kindName(value);
|
|
4593
|
+
if (!kind)
|
|
4594
|
+
throw new InvalidArgumentError(`must be one of ${SKILL_KINDS.join(", ")}.`);
|
|
4595
|
+
return kind;
|
|
4596
|
+
}
|
|
4597
|
+
function parseSlug(value) {
|
|
4598
|
+
if (!isSkillName(value)) {
|
|
4599
|
+
throw new InvalidArgumentError('use 1–64 lowercase letters, digits or hyphens, not starting or ending with a hyphen and without "--".');
|
|
4600
|
+
}
|
|
4601
|
+
return value;
|
|
4602
|
+
}
|
|
4603
|
+
function collect2(value, previous = []) {
|
|
4604
|
+
return [...previous, value];
|
|
4605
|
+
}
|
|
4606
|
+
function addContentOptions(command, required) {
|
|
4607
|
+
const note = required ? "" : " (replaces the current one)";
|
|
4608
|
+
command.option("-t, --title <title>", `title${note}`).option("--kind <kind>", `what the Skill makes: ${SKILL_KINDS.join(", ")}${note}`, parseKind2).option("-d, --description <text>", `a short description shown under the title${note}`).option("--description-file <path>", "read the description from a file instead").option("--tag <id>", `a tag ID from \`solgrid skills tags\`; up to ${MAX_TAGS}, repeat for several${required ? "" : " (replaces all current tags)"}`, collect2).option("--cover <path>", `cover image; required unless the kind is video${note}`).option("--preview <path>", `preview image, video or audio; required for video and audio${note}`).option("--dir <path>", `a Skill directory with SKILL.md at its top, packed for you${note}`).option("--zip <path>", `or an already packed Skill (.zip with SKILL.md)${note}`);
|
|
4609
|
+
return command;
|
|
4610
|
+
}
|
|
4611
|
+
function readDescription(opts) {
|
|
4612
|
+
if (opts.description !== undefined && opts.descriptionFile !== undefined) {
|
|
4613
|
+
throw new CliError("USAGE", "Use either --description or --description-file, not both.");
|
|
4614
|
+
}
|
|
4615
|
+
if (opts.descriptionFile === undefined)
|
|
4616
|
+
return opts.description;
|
|
4617
|
+
try {
|
|
4618
|
+
return readFileSync4(opts.descriptionFile, "utf8");
|
|
4619
|
+
} catch (cause) {
|
|
4620
|
+
throw new CliError("USAGE", `Cannot read ${opts.descriptionFile}.`, { cause });
|
|
4621
|
+
}
|
|
4622
|
+
}
|
|
4623
|
+
function aSkill(kind) {
|
|
4624
|
+
return `${kind === "image" || kind === "audio" ? "An" : "A"} ${kind} Skill`;
|
|
4625
|
+
}
|
|
4626
|
+
function checkMedia(kind, hasCover, hasPreview) {
|
|
4627
|
+
if (kind !== "video" && !hasCover) {
|
|
4628
|
+
throw new CliError("USAGE", `${aSkill(kind)} needs a cover image.`, {
|
|
4629
|
+
hint: "Pass --cover <image>."
|
|
4630
|
+
});
|
|
4631
|
+
}
|
|
4632
|
+
if ((kind === "video" || kind === "audio") && !hasPreview) {
|
|
4633
|
+
throw new CliError("USAGE", `${aSkill(kind)} needs a preview.`, {
|
|
4634
|
+
hint: `Pass --preview <${kind} or image file>.`
|
|
4635
|
+
});
|
|
4636
|
+
}
|
|
4637
|
+
}
|
|
4638
|
+
function pickTarget(opts) {
|
|
4639
|
+
if (opts.slug === undefined === (opts.id === undefined)) {
|
|
4640
|
+
throw new CliError("USAGE", "Pass exactly one of --slug or --id.");
|
|
4641
|
+
}
|
|
4642
|
+
return opts.slug !== undefined ? { slug: opts.slug } : { id: opts.id };
|
|
4643
|
+
}
|
|
4644
|
+
function targetName(target) {
|
|
4645
|
+
return "slug" in target ? target.slug : target.id;
|
|
4646
|
+
}
|
|
4647
|
+
function isApiNotFound(error) {
|
|
4648
|
+
return error instanceof ApiError && error.code === "NOT_FOUND" && !error.routeMissing;
|
|
4649
|
+
}
|
|
4650
|
+
async function findSkill(client, target) {
|
|
4651
|
+
try {
|
|
4652
|
+
return await getSkill(client, target);
|
|
4653
|
+
} catch (error) {
|
|
4654
|
+
if (!isApiNotFound(error))
|
|
4655
|
+
throw error;
|
|
4656
|
+
throw new CliError("NOT_FOUND", `No Skill with ${"slug" in target ? "slug" : "ID"} "${targetName(target)}".`, {
|
|
4657
|
+
hint: "Search with `solgrid skills list -q <words>`, or list yours with `solgrid skills list --mine`.",
|
|
4658
|
+
cause: error
|
|
4659
|
+
});
|
|
4660
|
+
}
|
|
4661
|
+
}
|
|
4662
|
+
function notYours(error, slug) {
|
|
4663
|
+
if (!isApiNotFound(error))
|
|
4664
|
+
return error;
|
|
4665
|
+
return new CliError("NOT_FOUND", `Skill ${slug} is not one of yours.`, {
|
|
4666
|
+
hint: "Only your own Skills can be changed. List them with `solgrid skills list --mine`.",
|
|
4667
|
+
cause: error
|
|
4668
|
+
});
|
|
4669
|
+
}
|
|
4670
|
+
function skillPackage(opts) {
|
|
4671
|
+
if (opts.dir !== undefined && opts.zip !== undefined) {
|
|
4672
|
+
throw new CliError("USAGE", "Use either --dir or --zip, not both.");
|
|
4673
|
+
}
|
|
4674
|
+
if (opts.dir !== undefined)
|
|
4675
|
+
return packSkillDirectory(opts.dir);
|
|
4676
|
+
if (opts.zip !== undefined) {
|
|
4677
|
+
prepareSkillFile(opts.zip, "skill");
|
|
4678
|
+
return { path: opts.zip, cleanup: () => {} };
|
|
4679
|
+
}
|
|
4680
|
+
return;
|
|
4681
|
+
}
|
|
4682
|
+
function slugFor(pkg, opts) {
|
|
4683
|
+
const { slug } = opts;
|
|
4684
|
+
if (pkg.name === undefined) {
|
|
4685
|
+
if (slug === undefined) {
|
|
4686
|
+
throw new CliError("USAGE", opts.zip !== undefined ? "Pass --slug with --zip." : "Could not read the name in SKILL.md; pass --slug.", {
|
|
4687
|
+
hint: opts.zip !== undefined ? "It must equal the name in the package's SKILL.md. With --dir the CLI reads it for you." : "It must equal the name in SKILL.md's frontmatter."
|
|
4688
|
+
});
|
|
4689
|
+
}
|
|
4690
|
+
return slug;
|
|
4691
|
+
}
|
|
4692
|
+
if (slug !== undefined && slug !== pkg.name)
|
|
4693
|
+
throw nameMismatch(pkg.name, slug);
|
|
4694
|
+
return pkg.name;
|
|
4695
|
+
}
|
|
4696
|
+
function nameMismatch(name, slug) {
|
|
4697
|
+
return new CliError("USAGE", `The name in SKILL.md is "${name}", but the Skill's slug is "${slug}". They must be the same.`, { hint: `Set \`name: ${slug}\` in SKILL.md's frontmatter.` });
|
|
4698
|
+
}
|
|
4699
|
+
async function checkTagOptions(client, out, tags) {
|
|
4700
|
+
if (tags === undefined)
|
|
4701
|
+
return;
|
|
4702
|
+
if (tags.length > MAX_TAGS)
|
|
4703
|
+
checkTags(tags, []);
|
|
4704
|
+
out.progress.update("Checking tags…");
|
|
4705
|
+
checkTags(tags, await listSkillTags(client));
|
|
4706
|
+
}
|
|
4707
|
+
async function confirmOrRefuse(ctx, out, opts, question, refusal) {
|
|
4708
|
+
if (opts.yes)
|
|
4709
|
+
return true;
|
|
4710
|
+
if (opts.json || !ctx.stdinIsTTY) {
|
|
4711
|
+
throw new CliError("USAGE", refusal, { hint: "Pass --yes to confirm in non-interactive use." });
|
|
4712
|
+
}
|
|
4713
|
+
out.progress.stop();
|
|
4714
|
+
return ctx.confirm(question);
|
|
4715
|
+
}
|
|
4716
|
+
async function uploadIfGiven(client, out, path, role) {
|
|
4717
|
+
if (path === undefined)
|
|
4718
|
+
return;
|
|
4719
|
+
out.progress.update(`Uploading ${path}…`);
|
|
4720
|
+
return (await uploadSkillFile(client, path, role)).fileAssetId;
|
|
4721
|
+
}
|
|
4722
|
+
function registerSkillsCommand(program, ctx) {
|
|
4723
|
+
const skills = program.command("skills").description("Find, publish and manage Skills on SkillHub.").addHelpText("after", `
|
|
4724
|
+
A Skill is public as soon as it is created. Its slug identifies it everywhere and cannot
|
|
4725
|
+
be changed later.`);
|
|
4726
|
+
skills.command("list").description("Search Skills, newest first.").option("-q, --query <words>", "match titles and tags").option("--kind <kind>", `only this kind: ${SKILL_KINDS.join(", ")}`, parseKind2).option("--mine", "only your own Skills").option("-l, --limit <n>", "Skills per page (default 20, up to 100)", parsePositiveInt).option("--page <n>", "page number (default 1)", parsePositiveInt).option(...JSON_FLAG).action((opts) => runAction(ctx, opts.json, async (out) => {
|
|
4727
|
+
const client = apiClient(ctx);
|
|
4728
|
+
out.progress.start("Searching Skills…");
|
|
4729
|
+
const data = await listSkills(client, {
|
|
4730
|
+
...opts.mine ? { mine: true } : {},
|
|
4731
|
+
...opts.query ? { keyword: opts.query } : {},
|
|
4732
|
+
...opts.kind ? { kind: opts.kind } : {},
|
|
4733
|
+
...opts.limit !== undefined ? { pageSize: opts.limit } : {},
|
|
4734
|
+
...opts.page !== undefined ? { page: opts.page } : {}
|
|
4735
|
+
});
|
|
4736
|
+
out.result(data, () => {
|
|
4737
|
+
if (data.items.length === 0) {
|
|
4738
|
+
return opts.query || opts.kind ? "No Skills match. Try other words, or drop --kind." : opts.mine ? "You have not published any Skills yet. Publish one with `solgrid skills create`." : "No Skills yet.";
|
|
4739
|
+
}
|
|
4740
|
+
const lines = [
|
|
4741
|
+
renderTable(["ID", "TITLE", "KIND", "AUTHOR", "UPDATED"], data.items.map((skill) => [
|
|
4742
|
+
skill.id,
|
|
4743
|
+
truncate(skill.title, 40),
|
|
4744
|
+
kindName(skill.kind) ?? "-",
|
|
4745
|
+
skill.author?.nickname || "-",
|
|
4746
|
+
formatTime(skill.updatedAt ?? skill.createdAt)
|
|
4747
|
+
]), { formatHeader: (text) => out.style("dim", text) })
|
|
4748
|
+
];
|
|
4749
|
+
const shown = (data.pageIndex - 1) * data.pageSize + data.items.length;
|
|
4750
|
+
if (shown < data.total) {
|
|
4751
|
+
lines.push("", out.style("dim", `${shown} of ${data.total}. Next page: add --page ${data.pageIndex + 1}`));
|
|
4752
|
+
}
|
|
4753
|
+
lines.push("", out.style("dim", "Details: solgrid skills get -i <id>"));
|
|
4754
|
+
return lines.join(`
|
|
4755
|
+
`);
|
|
4756
|
+
});
|
|
4757
|
+
}));
|
|
4758
|
+
skills.command("get").description("Show a Skill's details.").option("-s, --slug <slug>", "the Skill's slug").option("-i, --id <id>", "the Skill's ID").option(...JSON_FLAG).action((opts) => runAction(ctx, opts.json, async (out) => {
|
|
4759
|
+
const target = pickTarget(opts);
|
|
4760
|
+
const client = apiClient(ctx);
|
|
4761
|
+
out.progress.start("Fetching Skill…");
|
|
4762
|
+
const skill = await findSkill(client, target);
|
|
4763
|
+
out.result(skill, () => renderSkill(skill, out.style));
|
|
4764
|
+
}));
|
|
4765
|
+
skills.command("tags").description(`List the tags a Skill can carry (up to ${MAX_TAGS} each).`).option(...JSON_FLAG).action((opts) => runAction(ctx, opts.json, async (out) => {
|
|
4766
|
+
const client = apiClient(ctx);
|
|
4767
|
+
out.progress.start("Fetching tags…");
|
|
4768
|
+
const tags = await listSkillTags(client);
|
|
4769
|
+
out.result({ tags }, () => [
|
|
4770
|
+
renderTable(["ID", "NAME"], tags.map((tag) => [tag.id, tag.name]), { formatHeader: (text) => out.style("dim", text) }),
|
|
4771
|
+
"",
|
|
4772
|
+
out.style("dim", "Use the ID with --tag, e.g. --tag cat_anime.")
|
|
4773
|
+
].join(`
|
|
4774
|
+
`));
|
|
4775
|
+
}));
|
|
4776
|
+
addContentOptions(skills.command("create").description("Publish a new Skill. It is public as soon as it is created.").option("-s, --slug <slug>", "the Skill's unique ID; must equal the name in SKILL.md (default: that name)", parseSlug), true).option("-y, --yes", "publish without asking").option(...JSON_FLAG).action((opts) => runAction(ctx, opts.json, async (out) => {
|
|
4777
|
+
const description = readDescription(opts);
|
|
4778
|
+
const missing = [
|
|
4779
|
+
["--title", opts.title],
|
|
4780
|
+
["--kind", opts.kind],
|
|
4781
|
+
["--description or --description-file", description],
|
|
4782
|
+
["--dir or --zip", opts.dir ?? opts.zip]
|
|
4783
|
+
].filter(([, value]) => !value).map(([name]) => name);
|
|
4784
|
+
if (missing.length > 0)
|
|
4785
|
+
throw new CliError("USAGE", `Missing ${missing.join(", ")}.`);
|
|
4786
|
+
const kind = opts.kind;
|
|
4787
|
+
checkMedia(kind, opts.cover !== undefined, opts.preview !== undefined);
|
|
4788
|
+
if (opts.cover)
|
|
4789
|
+
prepareSkillFile(opts.cover, "cover");
|
|
4790
|
+
if (opts.preview)
|
|
4791
|
+
prepareSkillFile(opts.preview, "preview");
|
|
4792
|
+
const pkg = skillPackage(opts);
|
|
4793
|
+
try {
|
|
4794
|
+
const slug = slugFor(pkg, opts);
|
|
4795
|
+
const client = apiClient(ctx);
|
|
4796
|
+
out.progress.start("Checking…");
|
|
4797
|
+
await checkTagOptions(client, out, opts.tag);
|
|
4798
|
+
const confirmed = await confirmOrRefuse(ctx, out, opts, `Publish "${opts.title}" (${slug}) publicly on SkillHub?`, "Refusing to publish a Skill without confirmation.");
|
|
4799
|
+
if (!confirmed) {
|
|
4800
|
+
out.result({ slug, created: false }, () => "Cancelled.");
|
|
4801
|
+
return;
|
|
4802
|
+
}
|
|
4803
|
+
out.progress.start("Uploading…");
|
|
4804
|
+
const content = {
|
|
4805
|
+
title: opts.title,
|
|
4806
|
+
kind,
|
|
4807
|
+
description,
|
|
4808
|
+
tags: opts.tag ?? [],
|
|
4809
|
+
skillFileAssetId: await uploadIfGiven(client, out, pkg.path, "skill"),
|
|
4810
|
+
coverFileAssetId: await uploadIfGiven(client, out, opts.cover, "cover") ?? null,
|
|
4811
|
+
previewFileAssetId: await uploadIfGiven(client, out, opts.preview, "preview") ?? null
|
|
4812
|
+
};
|
|
4813
|
+
out.progress.update("Publishing…");
|
|
4814
|
+
const { id } = await createSkill(client, slug, content).catch((error) => {
|
|
4815
|
+
if (error instanceof ApiError && error.code === "CONFLICT") {
|
|
4816
|
+
throw new CliError("CONFLICT", `The slug "${slug}" is already taken.`, {
|
|
4817
|
+
hint: "Choose another name in SKILL.md (and --slug).",
|
|
4818
|
+
cause: error
|
|
4819
|
+
});
|
|
4820
|
+
}
|
|
4821
|
+
throw error;
|
|
4822
|
+
});
|
|
4823
|
+
out.result({ id, slug, created: true }, () => [
|
|
4824
|
+
`Published "${content.title}" as ${slug}.`,
|
|
4825
|
+
...renderFields([["ID", id]], out.style),
|
|
4826
|
+
"",
|
|
4827
|
+
out.style("dim", `See it with: solgrid skills get -s ${slug}`)
|
|
4828
|
+
].join(`
|
|
4829
|
+
`));
|
|
4830
|
+
} finally {
|
|
4831
|
+
pkg.cleanup();
|
|
4832
|
+
}
|
|
4833
|
+
}));
|
|
4834
|
+
addContentOptions(skills.command("update").description("Change one of your Skills. Only the options you pass are changed; the slug never is.").option("-s, --slug <slug>", "the Skill's slug").option("-i, --id <id>", "the Skill's ID"), false).option("--clear-tags", "remove all tags").option("--clear-preview", "remove the preview (image and web Skills only)").option(...JSON_FLAG).action((opts) => runAction(ctx, opts.json, async (out) => {
|
|
4835
|
+
const target = pickTarget(opts);
|
|
4836
|
+
const description = readDescription(opts);
|
|
4837
|
+
if (opts.clearTags && opts.tag !== undefined) {
|
|
4838
|
+
throw new CliError("USAGE", "Use either --tag or --clear-tags, not both.");
|
|
4839
|
+
}
|
|
4840
|
+
const tags = opts.clearTags ? [] : opts.tag;
|
|
4841
|
+
if (opts.clearPreview && opts.preview !== undefined) {
|
|
4842
|
+
throw new CliError("USAGE", "Use either --preview or --clear-preview, not both.");
|
|
4843
|
+
}
|
|
4844
|
+
const changes = [
|
|
4845
|
+
opts.title,
|
|
4846
|
+
opts.kind,
|
|
4847
|
+
description,
|
|
4848
|
+
tags,
|
|
4849
|
+
opts.cover,
|
|
4850
|
+
opts.preview,
|
|
4851
|
+
opts.clearPreview,
|
|
4852
|
+
opts.dir,
|
|
4853
|
+
opts.zip
|
|
4854
|
+
];
|
|
4855
|
+
if (changes.every((value) => value === undefined)) {
|
|
4856
|
+
throw new CliError("USAGE", "Nothing to change.", {
|
|
4857
|
+
hint: "Pass at least one of --title, --kind, --description, --tag, --clear-tags, --cover, --preview, --clear-preview, --dir or --zip."
|
|
4858
|
+
});
|
|
4859
|
+
}
|
|
4860
|
+
if (opts.cover)
|
|
4861
|
+
prepareSkillFile(opts.cover, "cover");
|
|
4862
|
+
if (opts.preview)
|
|
4863
|
+
prepareSkillFile(opts.preview, "preview");
|
|
4864
|
+
const pkg = skillPackage(opts);
|
|
4865
|
+
try {
|
|
4866
|
+
const client = apiClient(ctx);
|
|
4867
|
+
out.progress.start("Fetching Skill…");
|
|
4868
|
+
const skill = await findSkill(client, target);
|
|
4869
|
+
if (pkg?.name !== undefined && pkg.name !== skill.slug) {
|
|
4870
|
+
throw nameMismatch(pkg.name, skill.slug);
|
|
4871
|
+
}
|
|
4872
|
+
const next = {
|
|
4873
|
+
...contentOf(skill),
|
|
4874
|
+
...opts.title !== undefined ? { title: opts.title } : {},
|
|
4875
|
+
...opts.kind !== undefined ? { kind: opts.kind } : {},
|
|
4876
|
+
...description !== undefined ? { description } : {},
|
|
4877
|
+
...tags !== undefined ? { tags } : {},
|
|
4878
|
+
...opts.clearPreview ? { previewFileAssetId: null } : {}
|
|
4879
|
+
};
|
|
4880
|
+
if (opts.clearPreview && (next.kind === "video" || next.kind === "audio")) {
|
|
4881
|
+
throw new CliError("USAGE", `${aSkill(next.kind)} needs a preview; it cannot be removed.`, {
|
|
4882
|
+
hint: "Replace it with --preview <file> instead."
|
|
4883
|
+
});
|
|
4884
|
+
}
|
|
4885
|
+
checkMedia(next.kind, Boolean(opts.cover ?? next.coverFileAssetId), Boolean(opts.preview ?? next.previewFileAssetId));
|
|
4886
|
+
await checkTagOptions(client, out, opts.tag);
|
|
4887
|
+
next.skillFileAssetId = await uploadIfGiven(client, out, pkg?.path, "skill") ?? next.skillFileAssetId;
|
|
4888
|
+
next.coverFileAssetId = await uploadIfGiven(client, out, opts.cover, "cover") ?? next.coverFileAssetId;
|
|
4889
|
+
next.previewFileAssetId = await uploadIfGiven(client, out, opts.preview, "preview") ?? next.previewFileAssetId;
|
|
4890
|
+
out.progress.update("Saving…");
|
|
4891
|
+
await updateSkill(client, skill.id, next).catch((error) => {
|
|
4892
|
+
throw notYours(error, skill.slug);
|
|
4893
|
+
});
|
|
4894
|
+
out.result({ id: skill.id, slug: skill.slug, updated: true }, () => `Updated Skill ${skill.slug}.`);
|
|
4895
|
+
} finally {
|
|
4896
|
+
pkg?.cleanup();
|
|
4897
|
+
}
|
|
4898
|
+
}));
|
|
4899
|
+
skills.command("delete").description("Delete one of your Skills. It disappears from SkillHub.").option("-s, --slug <slug>", "the Skill's slug").option("-i, --id <id>", "the Skill's ID").option("-y, --yes", "delete without asking").option(...JSON_FLAG).action((opts) => runAction(ctx, opts.json, async (out) => {
|
|
4900
|
+
const target = pickTarget(opts);
|
|
4901
|
+
const client = apiClient(ctx);
|
|
4902
|
+
out.progress.start("Finding Skill…");
|
|
4903
|
+
const { id, slug } = await findSkill(client, target);
|
|
4904
|
+
const confirmed = await confirmOrRefuse(ctx, out, opts, `Delete Skill ${slug}? It will no longer be listed on SkillHub.`, "Refusing to delete a Skill without confirmation.");
|
|
4905
|
+
if (!confirmed) {
|
|
4906
|
+
out.result({ id, slug, deleted: false }, () => "Cancelled.");
|
|
4907
|
+
return;
|
|
4908
|
+
}
|
|
4909
|
+
out.progress.start("Deleting…");
|
|
4910
|
+
await deleteSkill(client, id).catch((error) => {
|
|
4911
|
+
throw notYours(error, slug);
|
|
4912
|
+
});
|
|
4913
|
+
out.result({ id, slug, deleted: true }, () => `Deleted Skill ${slug}.`);
|
|
4914
|
+
}));
|
|
4915
|
+
skills.command("download").description("Download a Skill's package (.zip).").requiredOption("-s, --slug <slug>", "the Skill's slug").option("-o, --output <path>", "where to save it (default: <slug>.zip in the current directory)").option("--force", "overwrite the output file if it exists").option(...JSON_FLAG).action((opts) => runAction(ctx, opts.json, async (out) => {
|
|
4916
|
+
const client = apiClient(ctx);
|
|
4917
|
+
out.progress.start("Downloading…");
|
|
4918
|
+
const response = await openSkillPackage(client, opts.slug);
|
|
4919
|
+
const destination = opts.output ?? filenameFromDisposition(response.headers.get("content-disposition")) ?? `${opts.slug}.zip`;
|
|
4920
|
+
if (existsSync3(destination) && !opts.force) {
|
|
4921
|
+
await response.body?.cancel();
|
|
4922
|
+
throw new CliError("USAGE", `${destination} already exists.`, {
|
|
4923
|
+
hint: "Pass --force to overwrite it, or choose another path with -o."
|
|
4924
|
+
});
|
|
4925
|
+
}
|
|
4926
|
+
if (!response.body)
|
|
4927
|
+
throw new CliError("INVALID_RESPONSE", "The download has no content.");
|
|
4928
|
+
const bytes = await saveDownload(response.body, destination, {
|
|
4929
|
+
overwrite: opts.force === true
|
|
4930
|
+
});
|
|
4931
|
+
out.result({ slug: opts.slug, path: destination, sizeBytes: bytes }, () => `Saved ${destination} (${formatBytes(bytes)}).`);
|
|
4932
|
+
}));
|
|
4933
|
+
}
|
|
4934
|
+
function renderSkill(skill, style) {
|
|
4935
|
+
const lines = [
|
|
4936
|
+
style("bold", skill.title),
|
|
4937
|
+
"",
|
|
4938
|
+
...renderFields([
|
|
4939
|
+
["Slug", skill.slug],
|
|
4940
|
+
["ID", skill.id],
|
|
4941
|
+
["Kind", kindName(skill.kind)],
|
|
4942
|
+
["Author", skill.author?.nickname || undefined],
|
|
4943
|
+
["Tags", skill.tags?.length ? skill.tags.join(", ") : undefined],
|
|
4944
|
+
["Created", skill.createdAt ? formatTime(skill.createdAt) : undefined],
|
|
4945
|
+
["Updated", skill.updatedAt ? formatTime(skill.updatedAt) : undefined]
|
|
4946
|
+
], style)
|
|
4947
|
+
];
|
|
4948
|
+
if (skill.description) {
|
|
4949
|
+
const text = skill.description.trim();
|
|
4950
|
+
lines.push("", text.length > MAX_INLINE_DESCRIPTION_CHARS ? `${text.slice(0, MAX_INLINE_DESCRIPTION_CHARS)}
|
|
4951
|
+
${style("dim", `… ${text.length - MAX_INLINE_DESCRIPTION_CHARS} more characters; use --json for the full description.`)}` : text);
|
|
4952
|
+
}
|
|
4953
|
+
lines.push("", style("dim", `Download it with: solgrid skills download -s ${skill.slug}`));
|
|
4954
|
+
return lines.join(`
|
|
4955
|
+
`);
|
|
4956
|
+
}
|
|
4957
|
+
|
|
3866
4958
|
// src/output/price.ts
|
|
3867
4959
|
function periodText(period) {
|
|
3868
4960
|
const unit = period.unit.toLowerCase();
|
|
@@ -3899,7 +4991,80 @@ function describeLeaf(leaf) {
|
|
|
3899
4991
|
const text = withUnit(formatAmount(leaf.amount), unitText(leaf));
|
|
3900
4992
|
return leaf.flatFee ? `${text} + ${formatAmount(leaf.flatFee)} flat` : text;
|
|
3901
4993
|
}
|
|
3902
|
-
|
|
4994
|
+
var METER_UNITS = {
|
|
4995
|
+
Calls: ["call", "calls"],
|
|
4996
|
+
WebSearchCalls: ["web search", "web searches"],
|
|
4997
|
+
InputImages: ["input image", "input images"],
|
|
4998
|
+
OutputImages: ["output image", "output images"],
|
|
4999
|
+
InputVideoSeconds: ["second of input video", "seconds of input video"],
|
|
5000
|
+
OutputVideoSeconds: ["second of output video", "seconds of output video"],
|
|
5001
|
+
TotalVideoSeconds: ["second of video", "seconds of video"],
|
|
5002
|
+
BilledTokens: ["token", "tokens"],
|
|
5003
|
+
Token: ["token", "tokens"],
|
|
5004
|
+
BilledCharacters: ["character", "characters"],
|
|
5005
|
+
Characters: ["character", "characters"],
|
|
5006
|
+
Credits: ["credit", "credits"],
|
|
5007
|
+
ProviderUnits: ["Unit", "Units"],
|
|
5008
|
+
PROVIDER_UNIT: ["Unit", "Units"],
|
|
5009
|
+
Unit: ["Unit", "Units"]
|
|
5010
|
+
};
|
|
5011
|
+
function meterText(meter, size = 1) {
|
|
5012
|
+
const [one, many] = METER_UNITS[meter] ?? [meter, meter];
|
|
5013
|
+
return size === 1 ? `per ${one}` : `per ${countText(size)} ${many}`;
|
|
5014
|
+
}
|
|
5015
|
+
function rateAmount(rate) {
|
|
5016
|
+
const micros = Math.ceil(Number((rate.unitPriceUsd * 1e6).toPrecision(12)));
|
|
5017
|
+
return formatAmount({ value: (micros / 1e6).toFixed(6), currency: "USD" });
|
|
5018
|
+
}
|
|
5019
|
+
function describeRate(rate) {
|
|
5020
|
+
return `${rateAmount(rate)} ${meterText(rate.meter, rate.unitSize)}`;
|
|
5021
|
+
}
|
|
5022
|
+
function ratesByMeter(rates) {
|
|
5023
|
+
const groups = new Map;
|
|
5024
|
+
for (const rate of rates)
|
|
5025
|
+
groups.set(rate.meter, [...groups.get(rate.meter) ?? [], rate]);
|
|
5026
|
+
return [...groups.values()];
|
|
5027
|
+
}
|
|
5028
|
+
function perUnit(rate) {
|
|
5029
|
+
return rate.unitPriceUsd / (rate.unitSize || 1);
|
|
5030
|
+
}
|
|
5031
|
+
function hasConditions(rate) {
|
|
5032
|
+
return Boolean(rate.conditions && Object.keys(rate.conditions).length > 0);
|
|
5033
|
+
}
|
|
5034
|
+
function summarizeRates(price, rates) {
|
|
5035
|
+
const only = rates.length === 1 ? rates[0] : undefined;
|
|
5036
|
+
if (only) {
|
|
5037
|
+
const amount = price.amount ? formatAmount(price.amount) : rateAmount(only);
|
|
5038
|
+
const text = `${amount} ${meterText(only.meter, only.unitSize)}`;
|
|
5039
|
+
return hasConditions(only) ? `${text} (${conditionsText(only)})` : text;
|
|
5040
|
+
}
|
|
5041
|
+
return ratesByMeter(rates).map((group) => {
|
|
5042
|
+
const cheapest = group.reduce((a, b) => perUnit(b) < perUnit(a) ? b : a);
|
|
5043
|
+
const varies = new Set(group.map(describeRate)).size > 1;
|
|
5044
|
+
return `${varies ? "from " : ""}${describeRate(cheapest)}`;
|
|
5045
|
+
}).join(" + ");
|
|
5046
|
+
}
|
|
5047
|
+
function rateNotes(rate) {
|
|
5048
|
+
const notes = [];
|
|
5049
|
+
if (rate.freeUnits)
|
|
5050
|
+
notes.push(`first ${rate.freeUnits} free`);
|
|
5051
|
+
if (typeof rate.maximumUnits === "number")
|
|
5052
|
+
notes.push(`billed up to ${rate.maximumUnits}`);
|
|
5053
|
+
if (rate.roundUpTo)
|
|
5054
|
+
notes.push(`rounded up to ${rate.roundUpTo}`);
|
|
5055
|
+
return notes.join("; ");
|
|
5056
|
+
}
|
|
5057
|
+
function conditionsText(rate) {
|
|
5058
|
+
const conditions = rate.conditions && typeof rate.conditions === "object" ? rate.conditions : {};
|
|
5059
|
+
return whenText(conditions) || "-";
|
|
5060
|
+
}
|
|
5061
|
+
function summarizePrice(price, fallback) {
|
|
5062
|
+
if (!price)
|
|
5063
|
+
return fallback || "-";
|
|
5064
|
+
if (price.rates?.length)
|
|
5065
|
+
return summarizeRates(price, price.rates);
|
|
5066
|
+
if (!price.amount)
|
|
5067
|
+
return fallback || "-";
|
|
3903
5068
|
let text;
|
|
3904
5069
|
switch (price.type) {
|
|
3905
5070
|
case "PER_UNIT_MATRIX": {
|
|
@@ -3925,9 +5090,41 @@ function summarizePrice(price) {
|
|
|
3925
5090
|
function whenText(when) {
|
|
3926
5091
|
return Object.entries(when).map(([key, value]) => `${key}=${String(value)}`).join(", ");
|
|
3927
5092
|
}
|
|
3928
|
-
function priceDetail(price) {
|
|
5093
|
+
function priceDetail(price, fallback) {
|
|
3929
5094
|
const rows = [];
|
|
3930
5095
|
const lines = [];
|
|
5096
|
+
const summary = summarizePrice(price, fallback);
|
|
5097
|
+
if (!price)
|
|
5098
|
+
return { summary, rows, lines };
|
|
5099
|
+
const rates = price.rates ?? [];
|
|
5100
|
+
if (rates.length > 1 || rates.some(hasConditions)) {
|
|
5101
|
+
const notes = rates.map(rateNotes);
|
|
5102
|
+
const withNotes = notes.some(Boolean);
|
|
5103
|
+
rows.push(["CHARGE", "CONDITIONS", "PRICE", ...withNotes ? ["NOTES"] : []]);
|
|
5104
|
+
rates.forEach((rate, index) => {
|
|
5105
|
+
const [one] = METER_UNITS[rate.meter] ?? [rate.meter];
|
|
5106
|
+
rows.push([
|
|
5107
|
+
one,
|
|
5108
|
+
conditionsText(rate),
|
|
5109
|
+
describeRate(rate),
|
|
5110
|
+
...withNotes ? [notes[index] || "-"] : []
|
|
5111
|
+
]);
|
|
5112
|
+
});
|
|
5113
|
+
if (ratesByMeter(rates).length > 1) {
|
|
5114
|
+
lines.push("Different charges add up; for each charge, the row matching the request applies.");
|
|
5115
|
+
} else if (rates.length > 1) {
|
|
5116
|
+
lines.push("The row matching the request applies.");
|
|
5117
|
+
} else {
|
|
5118
|
+
lines.push("This price applies when the request matches its conditions.");
|
|
5119
|
+
}
|
|
5120
|
+
} else if (rates[0] && rateNotes(rates[0])) {
|
|
5121
|
+
lines.push(rateNotes(rates[0]));
|
|
5122
|
+
}
|
|
5123
|
+
if (price.description)
|
|
5124
|
+
lines.push(price.description);
|
|
5125
|
+
if (rates.length > 0) {
|
|
5126
|
+
lines.push("The final cost depends on actual usage: see `cost` in `solgrid runs get`.");
|
|
5127
|
+
}
|
|
3931
5128
|
if (price.type === "PER_UNIT_MATRIX" && price.variants?.length) {
|
|
3932
5129
|
const keys = price.selectors?.map((s) => s.key) ?? Object.keys(price.variants[0]?.when ?? {});
|
|
3933
5130
|
const labels = keys.map((key) => price.selectors?.find((s) => s.key === key)?.label ?? key);
|
|
@@ -3944,11 +5141,11 @@ function priceDetail(price) {
|
|
|
3944
5141
|
}
|
|
3945
5142
|
for (const note of price.notes ?? [])
|
|
3946
5143
|
lines.push(note);
|
|
3947
|
-
return { summary
|
|
5144
|
+
return { summary, rows, lines };
|
|
3948
5145
|
}
|
|
3949
5146
|
|
|
3950
5147
|
// src/output/schema.ts
|
|
3951
|
-
var
|
|
5148
|
+
var MAX_DEPTH2 = 2;
|
|
3952
5149
|
var MAX_ENUM_VALUES = 6;
|
|
3953
5150
|
function isSchema(value) {
|
|
3954
5151
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -3993,7 +5190,7 @@ function schemaFields(schema, prefix = "", depth = 0) {
|
|
|
3993
5190
|
required: required.has(name),
|
|
3994
5191
|
description: describe(property)
|
|
3995
5192
|
});
|
|
3996
|
-
if (property.type === "object" && depth + 1 <
|
|
5193
|
+
if (property.type === "object" && depth + 1 < MAX_DEPTH2) {
|
|
3997
5194
|
fields.push(...schemaFields(property, `${path}.`, depth + 1));
|
|
3998
5195
|
}
|
|
3999
5196
|
}
|
|
@@ -4021,7 +5218,7 @@ function registerToolCommands(program, ctx) {
|
|
|
4021
5218
|
const table = renderTable(["PROVIDER", "ENDPOINT", "PRICE", "HEALTH", "DESCRIPTION"], data.results.map((r) => [
|
|
4022
5219
|
r.provider,
|
|
4023
5220
|
r.endpoint,
|
|
4024
|
-
summarizePrice(r.price),
|
|
5221
|
+
truncate(summarizePrice(r.price, r.externalPrice), 60),
|
|
4025
5222
|
formatHealth(r.metrics, out.style),
|
|
4026
5223
|
truncate(r.description, 60)
|
|
4027
5224
|
]), { formatHeader: (text) => out.style("dim", text) });
|
|
@@ -4045,7 +5242,7 @@ function registerToolCommands(program, ctx) {
|
|
|
4045
5242
|
}));
|
|
4046
5243
|
}
|
|
4047
5244
|
function renderInspect(tool, style) {
|
|
4048
|
-
const price = priceDetail(tool.price);
|
|
5245
|
+
const price = priceDetail(tool.price, tool.externalPrice);
|
|
4049
5246
|
const runtime = tool.metrics?.runTimeMs;
|
|
4050
5247
|
const lines = [
|
|
4051
5248
|
style("bold", `${tool.providerName || tool.provider} · ${tool.endpoint}`),
|
|
@@ -4119,6 +5316,7 @@ function createProgram(ctx, options = {}) {
|
|
|
4119
5316
|
registerRunCommand(program, ctx);
|
|
4120
5317
|
registerRunsCommand(program, ctx);
|
|
4121
5318
|
registerFilesCommand(program, ctx);
|
|
5319
|
+
registerSkillsCommand(program, ctx);
|
|
4122
5320
|
registerAccountCommands(program, ctx);
|
|
4123
5321
|
registerKeysCommand(program, ctx);
|
|
4124
5322
|
registerSessionIdCommand(program, ctx);
|