@hasna/skills 0.3.0 → 0.5.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 +269 -14
- package/bin/index.js +7835 -5440
- package/bin/mcp.js +2040 -589
- package/bin/migrate.js +148 -40
- package/bin/server.js +66 -87
- package/bin/worker.js +42 -75
- package/dist/admin-contract.d.ts +37 -19
- package/dist/admin-contract.js +1 -1
- package/dist/cli/cli.test-utils.d.ts +10 -8
- package/dist/cli/commands/customer-profile.d.ts +2 -0
- package/dist/cli/commands/customer-verification.d.ts +5 -0
- package/dist/cli/commands/remote-account.d.ts +7 -0
- package/dist/cli/commands/tool-primitives.d.ts +1 -1
- package/dist/cli/commands/workspace-member-mutations.d.ts +2 -0
- package/dist/cli/commands/workspace-members.d.ts +2 -0
- package/dist/cli/env-assignment.d.ts +9 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +1369 -349
- package/dist/lib/agent-sync.d.ts +13 -8
- package/dist/lib/api-url.d.ts +4 -3
- package/dist/lib/app-home.d.ts +0 -1
- package/dist/lib/auth-store.d.ts +1 -1
- package/dist/lib/client-types.d.ts +75 -0
- package/dist/lib/credential-state.d.ts +12 -0
- package/dist/lib/fleet-credentials.d.ts +49 -17
- package/dist/lib/home-adoption.d.ts +2 -0
- package/dist/lib/home-census.d.ts +3 -1
- package/dist/lib/instance-credentials.d.ts +13 -0
- package/dist/lib/local-opt-in.d.ts +24 -0
- package/dist/lib/mcp-contracts.d.ts +4 -0
- package/dist/lib/portable-skills-files.d.ts +10 -2
- package/dist/lib/portable-skills-types.d.ts +2 -0
- package/dist/lib/read-access.d.ts +83 -0
- package/dist/lib/remote-account.d.ts +42 -0
- package/dist/lib/remote-auth.d.ts +46 -0
- package/dist/lib/remote-client.d.ts +90 -6
- package/dist/lib/remote-customer-operations.d.ts +106 -0
- package/dist/lib/remote-files.d.ts +21 -0
- package/dist/lib/remote-profile.d.ts +26 -0
- package/dist/lib/remote-registry.d.ts +7 -3
- package/dist/lib/remote-workspace.d.ts +76 -0
- package/dist/lib/run-routing.d.ts +1 -0
- package/dist/lib/run-state.d.ts +3 -0
- package/dist/lib/skillinfo.d.ts +1 -1
- package/dist/mcp/helpers.d.ts +22 -0
- package/dist/mcp/index.d.ts +16 -0
- package/dist/mcp/remote-customer-tools.d.ts +2 -0
- package/dist/sdk/governance-store.d.ts +1 -0
- package/dist/sdk/index.d.ts +8 -1
- package/dist/sdk/index.js +1994 -416
- package/dist/sdk/outputs.d.ts +0 -11
- package/dist/sdk/runs.d.ts +5 -5
- package/dist/storage.js +6 -40
- package/docs/skill-standard.md +30 -2
- package/package.json +7 -6
package/dist/index.js
CHANGED
|
@@ -111,12 +111,7 @@ import { homedir } from "os";
|
|
|
111
111
|
import { join, resolve } from "path";
|
|
112
112
|
import { homedir as pathsResolverHomedir } from "os";
|
|
113
113
|
import { join as pathsResolverJoin } from "path";
|
|
114
|
-
var
|
|
115
|
-
config: "HASNA_CONFIG_HOME",
|
|
116
|
-
data: "HASNA_DATA_HOME",
|
|
117
|
-
state: "HASNA_STATE_HOME",
|
|
118
|
-
cache: "HASNA_CACHE_HOME"
|
|
119
|
-
};
|
|
114
|
+
var PATHS_RESOLVER_DATA_ENV = "HASNA_DATA_HOME";
|
|
120
115
|
var PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
121
116
|
function pathsResolverAssertApp(app) {
|
|
122
117
|
if (typeof app !== "string" || app.length === 0) {
|
|
@@ -126,48 +121,19 @@ function pathsResolverAssertApp(app) {
|
|
|
126
121
|
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
127
122
|
}
|
|
128
123
|
}
|
|
129
|
-
function
|
|
130
|
-
if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
|
|
131
|
-
throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
function pathsResolverBaseDir(kind, options) {
|
|
135
|
-
pathsResolverAssertKind(kind);
|
|
124
|
+
function pathsResolverDataBaseDir(options) {
|
|
136
125
|
const env = options.env ?? process.env;
|
|
137
|
-
const override = env[
|
|
126
|
+
const override = env[PATHS_RESOLVER_DATA_ENV];
|
|
138
127
|
if (typeof override === "string" && override.length > 0)
|
|
139
128
|
return override;
|
|
140
129
|
const home = options.home ?? pathsResolverHomedir();
|
|
141
130
|
const platform = options.platform ?? process.platform;
|
|
142
|
-
|
|
143
|
-
switch (kind) {
|
|
144
|
-
case "config":
|
|
145
|
-
case "data":
|
|
146
|
-
return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
|
|
147
|
-
case "cache":
|
|
148
|
-
return pathsResolverJoin(home, "Library", "Caches", "Hasna");
|
|
149
|
-
case "state":
|
|
150
|
-
return pathsResolverJoin(home, "Library", "Logs", "Hasna");
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
switch (kind) {
|
|
154
|
-
case "config":
|
|
155
|
-
return pathsResolverJoin(home, ".config", "hasna");
|
|
156
|
-
case "data":
|
|
157
|
-
return pathsResolverJoin(home, ".local", "share", "hasna");
|
|
158
|
-
case "state":
|
|
159
|
-
return pathsResolverJoin(home, ".local", "state", "hasna");
|
|
160
|
-
case "cache":
|
|
161
|
-
return pathsResolverJoin(home, ".cache", "hasna");
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
function pathsResolverResolve(kind, options) {
|
|
165
|
-
pathsResolverAssertApp(options.app);
|
|
166
|
-
const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
|
|
167
|
-
return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
|
|
131
|
+
return platform === "darwin" ? pathsResolverJoin(home, "Library", "Application Support", "Hasna") : pathsResolverJoin(home, ".local", "share", "hasna");
|
|
168
132
|
}
|
|
169
133
|
function dataDir(options) {
|
|
170
|
-
|
|
134
|
+
pathsResolverAssertApp(options.app);
|
|
135
|
+
const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
|
|
136
|
+
return pathsResolverJoin(pathsResolverDataBaseDir(options), appSegment);
|
|
171
137
|
}
|
|
172
138
|
var DATA_DIR_ENV = "HASNA_SKILLS_DIR";
|
|
173
139
|
var HASNA_SKILLS_HOME_ENV = "HASNA_SKILLS_HOME";
|
|
@@ -1221,6 +1187,16 @@ function isHostedMetadataSkill(skillName, frontmatter, registryMeta, packageDecl
|
|
|
1221
1187
|
return true;
|
|
1222
1188
|
return false;
|
|
1223
1189
|
}
|
|
1190
|
+
function frontmatterString(raw) {
|
|
1191
|
+
if (raw.startsWith('"') && raw.endsWith('"')) {
|
|
1192
|
+
try {
|
|
1193
|
+
const decoded = JSON.parse(raw);
|
|
1194
|
+
if (typeof decoded === "string")
|
|
1195
|
+
return decoded;
|
|
1196
|
+
} catch {}
|
|
1197
|
+
}
|
|
1198
|
+
return raw.replace(/^["']|["']$/g, "");
|
|
1199
|
+
}
|
|
1224
1200
|
function parseSkillFrontmatter(content) {
|
|
1225
1201
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
1226
1202
|
if (!match)
|
|
@@ -1240,12 +1216,12 @@ function parseSkillFrontmatter(content) {
|
|
|
1240
1216
|
const tags = [];
|
|
1241
1217
|
while (i + 1 < lines.length && /^\s+-\s+/.test(lines[i + 1])) {
|
|
1242
1218
|
i++;
|
|
1243
|
-
tags.push(lines[i].replace(/^\s+-\s+/, "").trim());
|
|
1219
|
+
tags.push(frontmatterString(lines[i].replace(/^\s+-\s+/, "").trim()));
|
|
1244
1220
|
}
|
|
1245
1221
|
result.tags = tags;
|
|
1246
1222
|
continue;
|
|
1247
1223
|
}
|
|
1248
|
-
const value = rawValue
|
|
1224
|
+
const value = frontmatterString(rawValue);
|
|
1249
1225
|
if (!value)
|
|
1250
1226
|
continue;
|
|
1251
1227
|
if (key === "name")
|
|
@@ -1828,14 +1804,27 @@ function normalizePortableSkillName(name) {
|
|
|
1828
1804
|
}
|
|
1829
1805
|
return normalized;
|
|
1830
1806
|
}
|
|
1807
|
+
function normalizeNewPortableSkillName(name) {
|
|
1808
|
+
normalizePortableSkillName(name);
|
|
1809
|
+
const normalized = name.trim().replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1810
|
+
if (!normalized)
|
|
1811
|
+
throw new Error(`Invalid skill name '${name}'. Include letters or numbers.`);
|
|
1812
|
+
return normalized;
|
|
1813
|
+
}
|
|
1831
1814
|
function readPortableSkillManifest(skillPath, fallbackName = basename(skillPath)) {
|
|
1815
|
+
return readManifest(skillPath, fallbackName, normalizePortableSkillName);
|
|
1816
|
+
}
|
|
1817
|
+
function readPortableSkillManifestForImport(skillPath) {
|
|
1818
|
+
return readManifest(skillPath, basename(skillPath), normalizeNewPortableSkillName);
|
|
1819
|
+
}
|
|
1820
|
+
function readManifest(skillPath, fallbackName, normalizeName) {
|
|
1832
1821
|
const skillJsonPath = join6(skillPath, "skill.json");
|
|
1833
1822
|
const skillMdPath = join6(skillPath, "SKILL.md");
|
|
1834
1823
|
const pkgPath = join6(skillPath, "package.json");
|
|
1835
1824
|
const jsonManifest = existsSync6(skillJsonPath) ? readJsonObject(skillJsonPath) : undefined;
|
|
1836
1825
|
const frontmatter = existsSync6(skillMdPath) ? parseSkillFrontmatter(readFileSync5(skillMdPath, "utf-8")) ?? undefined : undefined;
|
|
1837
1826
|
const pkg = existsSync6(pkgPath) ? readJsonObject(pkgPath) : undefined;
|
|
1838
|
-
const name =
|
|
1827
|
+
const name = normalizeName(stringField(jsonManifest, "name") ?? frontmatter?.name ?? stringValue(pkg?.name) ?? fallbackName);
|
|
1839
1828
|
const description = stringField(jsonManifest, "description") ?? frontmatter?.description ?? stringValue(pkg?.description) ?? `${name} skill`;
|
|
1840
1829
|
const version = readDeclaredSkillVersion(skillPath) ?? PORTABLE_SKILL_DEFAULT_VERSION;
|
|
1841
1830
|
const kind = parseSkillKind(stringField(jsonManifest, "kind") ?? frontmatter?.kind);
|
|
@@ -1878,8 +1867,8 @@ function createInstructionManifest(name, options) {
|
|
|
1878
1867
|
description: options.description,
|
|
1879
1868
|
version: PORTABLE_SKILL_DEFAULT_VERSION,
|
|
1880
1869
|
displayName: displayName(name),
|
|
1881
|
-
category: "Development Tools",
|
|
1882
|
-
tags: ["custom", name],
|
|
1870
|
+
category: options.category ?? "Development Tools",
|
|
1871
|
+
tags: options.tags ?? ["custom", name],
|
|
1883
1872
|
kind: "instruction",
|
|
1884
1873
|
inputs: [],
|
|
1885
1874
|
commands: [],
|
|
@@ -1894,16 +1883,16 @@ function writeInstructionSkillTemplate(skillPath, manifest) {
|
|
|
1894
1883
|
}
|
|
1895
1884
|
function renderInstructionSkillMd(manifest) {
|
|
1896
1885
|
const tags = manifest.tags?.length ? `tags:
|
|
1897
|
-
${manifest.tags.map((tag) => ` - ${tag}`).join(`
|
|
1886
|
+
${manifest.tags.map((tag) => ` - ${yamlString(tag)}`).join(`
|
|
1898
1887
|
`)}
|
|
1899
1888
|
` : "";
|
|
1900
1889
|
return `---
|
|
1901
1890
|
name: ${manifest.name}
|
|
1902
|
-
description: ${manifest.description}
|
|
1891
|
+
description: ${yamlString(manifest.description)}
|
|
1903
1892
|
kind: instruction
|
|
1904
1893
|
version: ${manifest.version}
|
|
1905
1894
|
source: custom
|
|
1906
|
-
category: ${manifest.category ?? "Development Tools"}
|
|
1895
|
+
category: ${yamlString(manifest.category ?? "Development Tools")}
|
|
1907
1896
|
${tags}---
|
|
1908
1897
|
|
|
1909
1898
|
# ${manifest.displayName ?? displayName(manifest.name)}
|
|
@@ -1924,8 +1913,8 @@ function createPortableManifest(name, options) {
|
|
|
1924
1913
|
description: options.description,
|
|
1925
1914
|
version: PORTABLE_SKILL_DEFAULT_VERSION,
|
|
1926
1915
|
displayName: displayName(name),
|
|
1927
|
-
category: "Development Tools",
|
|
1928
|
-
tags: ["custom", name],
|
|
1916
|
+
category: options.category ?? "Development Tools",
|
|
1917
|
+
tags: options.tags ?? ["custom", name],
|
|
1929
1918
|
inputs: DEFAULT_INPUTS,
|
|
1930
1919
|
commands: [{
|
|
1931
1920
|
name,
|
|
@@ -2083,10 +2072,45 @@ function ensureInstructionSkillFiles(skillPath, manifest) {
|
|
|
2083
2072
|
};
|
|
2084
2073
|
if (!existsSync6(join6(skillPath, "SKILL.md"))) {
|
|
2085
2074
|
writeFileSync2(join6(skillPath, "SKILL.md"), renderSkillMd(next));
|
|
2075
|
+
} else {
|
|
2076
|
+
const path = join6(skillPath, "SKILL.md");
|
|
2077
|
+
const content = readFileSync5(path, "utf8");
|
|
2078
|
+
const declaredName = parseSkillFrontmatter(content)?.name;
|
|
2079
|
+
if (declaredName && declaredName !== next.name) {
|
|
2080
|
+
writeFileSync2(path, renameInstructionFrontmatter(content, next.name));
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
const packagePath = join6(skillPath, "package.json");
|
|
2084
|
+
if (existsSync6(packagePath)) {
|
|
2085
|
+
const pkg = readJsonObject(packagePath);
|
|
2086
|
+
if (typeof pkg.name === "string" && pkg.name !== next.name) {
|
|
2087
|
+
writeFileSync2(packagePath, `${JSON.stringify({ ...pkg, name: next.name }, null, 2)}
|
|
2088
|
+
`);
|
|
2089
|
+
}
|
|
2086
2090
|
}
|
|
2087
2091
|
writeSkillJsonWithHash(skillPath, next);
|
|
2088
2092
|
return readPortableSkillManifest(skillPath, next.name);
|
|
2089
2093
|
}
|
|
2094
|
+
function renameInstructionFrontmatter(content, name) {
|
|
2095
|
+
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?=\r?\n|$)/);
|
|
2096
|
+
const names = frontmatter?.[1]?.match(/^[ \t]*name[ \t]*:[^\r\n]*/gm) ?? [];
|
|
2097
|
+
const declaration = names.length === 1 ? names[0].match(/^(name[ \t]*:[ \t]*)(.*?)([ \t]*)$/) : null;
|
|
2098
|
+
const scalar = declaration?.[2] ?? "";
|
|
2099
|
+
let simple = /^[a-zA-Z0-9_.@/ -]+$/.test(scalar);
|
|
2100
|
+
if (scalar.startsWith('"')) {
|
|
2101
|
+
try {
|
|
2102
|
+
simple = typeof JSON.parse(scalar) === "string";
|
|
2103
|
+
} catch {
|
|
2104
|
+
simple = false;
|
|
2105
|
+
}
|
|
2106
|
+
} else if (scalar.startsWith("'"))
|
|
2107
|
+
simple = /^'[^'\r\n]*'$/.test(scalar);
|
|
2108
|
+
if (!frontmatter || !declaration || !simple) {
|
|
2109
|
+
throw new Error("Cannot rename instruction SKILL.md: use one unambiguous top-level name scalar in frontmatter.");
|
|
2110
|
+
}
|
|
2111
|
+
const renamed = frontmatter[0].replace(/^name[ \t]*:[^\r\n]*/m, () => `${declaration[1]}${name}${declaration[3]}`);
|
|
2112
|
+
return renamed + content.slice(frontmatter[0].length);
|
|
2113
|
+
}
|
|
2090
2114
|
function copySkillDirectory(source, destination) {
|
|
2091
2115
|
const resolvedSource = lstatSync2(source).isSymbolicLink() ? realpathSync(source) : source;
|
|
2092
2116
|
mkdirSync2(destination, { recursive: true });
|
|
@@ -2116,10 +2140,13 @@ function isExcludedCopyEntry(name, isFirstSegment) {
|
|
|
2116
2140
|
return true;
|
|
2117
2141
|
return false;
|
|
2118
2142
|
}
|
|
2143
|
+
function yamlString(value) {
|
|
2144
|
+
return JSON.stringify(value);
|
|
2145
|
+
}
|
|
2119
2146
|
function renderSkillMd(manifest) {
|
|
2120
2147
|
return `---
|
|
2121
2148
|
name: ${manifest.name}
|
|
2122
|
-
description: ${manifest.description}
|
|
2149
|
+
description: ${yamlString(manifest.description)}
|
|
2123
2150
|
---
|
|
2124
2151
|
|
|
2125
2152
|
# ${manifest.displayName ?? displayName(manifest.name)}
|
|
@@ -2491,7 +2518,7 @@ function isOfficialSkillName(name) {
|
|
|
2491
2518
|
return OFFICIAL_SKILL_NAMES.has(name);
|
|
2492
2519
|
}
|
|
2493
2520
|
function scaffoldPortableSkill(name, options = {}) {
|
|
2494
|
-
const skillName =
|
|
2521
|
+
const skillName = normalizeNewPortableSkillName(name);
|
|
2495
2522
|
const root = getPortableSkillsRoot(options);
|
|
2496
2523
|
const skillPath = join7(root, skillName);
|
|
2497
2524
|
if (existsSync7(skillPath)) {
|
|
@@ -2502,11 +2529,11 @@ function scaffoldPortableSkill(name, options = {}) {
|
|
|
2502
2529
|
const kind = options.kind ?? "executable";
|
|
2503
2530
|
const description = options.description ?? `${displayName(skillName)} skill`;
|
|
2504
2531
|
if (kind === "instruction") {
|
|
2505
|
-
const manifest2 = createInstructionManifest(skillName, { description });
|
|
2532
|
+
const manifest2 = createInstructionManifest(skillName, { description, category: options.category, tags: options.tags });
|
|
2506
2533
|
writeInstructionSkillTemplate(skillPath, manifest2);
|
|
2507
2534
|
return { name: skillName, path: skillPath, manifest: manifest2, created: true };
|
|
2508
2535
|
}
|
|
2509
|
-
const manifest = createPortableManifest(skillName, { description });
|
|
2536
|
+
const manifest = createPortableManifest(skillName, { description, category: options.category, tags: options.tags });
|
|
2510
2537
|
writePortableSkillTemplate(skillPath, manifest);
|
|
2511
2538
|
return { name: skillName, path: skillPath, manifest, created: true };
|
|
2512
2539
|
}
|
|
@@ -2561,9 +2588,9 @@ function portPortableSkill(sourcePath, options = {}) {
|
|
|
2561
2588
|
if (!existsSync7(absoluteSource) || !statSync6(absoluteSource).isDirectory()) {
|
|
2562
2589
|
throw new Error(`Skill source directory not found: ${sourcePath}`);
|
|
2563
2590
|
}
|
|
2564
|
-
const inferred =
|
|
2591
|
+
const inferred = readPortableSkillManifestForImport(absoluteSource);
|
|
2565
2592
|
const explicitName = options.name != null;
|
|
2566
|
-
const skillName =
|
|
2593
|
+
const skillName = normalizeNewPortableSkillName(options.name ?? inferred.name);
|
|
2567
2594
|
if (isOfficialSkillName(skillName) && !options.allowShadow) {
|
|
2568
2595
|
const sourceSlug = safeNormalizeName(basename2(absoluteSource));
|
|
2569
2596
|
const via = explicitName ? `Name '${skillName}' matches a bundled official skill.` : `Inferred name '${skillName}'${sourceSlug && sourceSlug !== skillName ? ` (from source folder '${basename2(absoluteSource)}')` : ""} matches a bundled official skill.`;
|
|
@@ -3178,6 +3205,7 @@ import { fileURLToPath } from "url";
|
|
|
3178
3205
|
import {
|
|
3179
3206
|
cpSync as cpSync3,
|
|
3180
3207
|
existsSync as existsSync9,
|
|
3208
|
+
lstatSync as lstatSync3,
|
|
3181
3209
|
mkdirSync as mkdirSync4,
|
|
3182
3210
|
mkdtempSync as mkdtempSync2,
|
|
3183
3211
|
readFileSync as readFileSync7,
|
|
@@ -3199,6 +3227,20 @@ var SYNC_AGENTS = ["claude", "codewith", "codex", "opencode", "cursor"];
|
|
|
3199
3227
|
var SKILLS_SOURCE_ENV = "SKILLS_SOURCE";
|
|
3200
3228
|
var SYNC_MARKER_FILE = ".hasna-skills.json";
|
|
3201
3229
|
var SYNC_MARKER_MANAGED_BY = "@hasna/skills";
|
|
3230
|
+
function isSkillsOwnershipMarker(marker) {
|
|
3231
|
+
return typeof marker === "object" && marker !== null && Object.hasOwn(marker, "managedBy") && marker.managedBy === SYNC_MARKER_MANAGED_BY;
|
|
3232
|
+
}
|
|
3233
|
+
function hasSkillsOwnershipMarker(dir) {
|
|
3234
|
+
const path = join9(dir, SYNC_MARKER_FILE);
|
|
3235
|
+
try {
|
|
3236
|
+
if (!lstatSync3(path).isFile())
|
|
3237
|
+
return false;
|
|
3238
|
+
const marker = JSON.parse(readFileSync7(path, "utf8"));
|
|
3239
|
+
return isSkillsOwnershipMarker(marker);
|
|
3240
|
+
} catch {
|
|
3241
|
+
return false;
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3202
3244
|
function isSyncAgent(value) {
|
|
3203
3245
|
return SYNC_AGENTS.includes(value);
|
|
3204
3246
|
}
|
|
@@ -3382,7 +3424,7 @@ function writeManagedSkillDir(dir, skillMd, options) {
|
|
|
3382
3424
|
const skillMdPath = join9(dir, "SKILL.md");
|
|
3383
3425
|
const markerPath = join9(dir, SYNC_MARKER_FILE);
|
|
3384
3426
|
const dirExists = existsSync9(dir);
|
|
3385
|
-
const managed =
|
|
3427
|
+
const managed = hasSkillsOwnershipMarker(dir);
|
|
3386
3428
|
const hasSkillMd = existsSync9(skillMdPath);
|
|
3387
3429
|
if (dirExists && !managed && !hasSkillMd) {
|
|
3388
3430
|
return {
|
|
@@ -3395,7 +3437,7 @@ function writeManagedSkillDir(dir, skillMd, options) {
|
|
|
3395
3437
|
return {
|
|
3396
3438
|
action: "skip",
|
|
3397
3439
|
path: skillMdPath,
|
|
3398
|
-
reason: "an unmanaged SKILL.md already exists here (hand-authored); pass --force to overwrite"
|
|
3440
|
+
reason: existsSync9(markerPath) ? "an unmanaged SKILL.md already exists here (invalid or foreign ownership marker); pass --force to overwrite" : "an unmanaged SKILL.md already exists here (hand-authored); pass --force to overwrite"
|
|
3399
3441
|
};
|
|
3400
3442
|
}
|
|
3401
3443
|
if (dirExists && managed && hasSkillMd && !options.force && isPointerSkillMd(skillMd)) {
|
|
@@ -3472,7 +3514,7 @@ function writeManagedSkillDir(dir, skillMd, options) {
|
|
|
3472
3514
|
}
|
|
3473
3515
|
function removeManagedAgentSkill(skill, agent, homeDir = homedir2()) {
|
|
3474
3516
|
const dir = join9(agentGlobalSkillsDir(agent, homeDir), skill);
|
|
3475
|
-
if (!
|
|
3517
|
+
if (!hasSkillsOwnershipMarker(dir))
|
|
3476
3518
|
return false;
|
|
3477
3519
|
rmSync2(dir, { recursive: true, force: true });
|
|
3478
3520
|
return true;
|
|
@@ -3848,7 +3890,7 @@ function removeSkillForAgent(name, options) {
|
|
|
3848
3890
|
const canonicalName = getCanonicalSkillName(name);
|
|
3849
3891
|
const scope = options.scope ?? "global";
|
|
3850
3892
|
const dir = getAgentSkillPath(canonicalName, options.agent, scope, options.projectDir);
|
|
3851
|
-
if (!
|
|
3893
|
+
if (!hasSkillsOwnershipMarker(dir))
|
|
3852
3894
|
return false;
|
|
3853
3895
|
rmSync3(dir, { recursive: true, force: true });
|
|
3854
3896
|
return true;
|
|
@@ -3950,6 +3992,7 @@ function createSkillRun(params, targetDir = process.cwd()) {
|
|
|
3950
3992
|
startedAt: now.toISOString(),
|
|
3951
3993
|
remote: params.remote ?? false,
|
|
3952
3994
|
...params.remoteRunId ? { remoteRunId: params.remoteRunId } : {},
|
|
3995
|
+
...params.remoteApiOrigin ? { remoteApiOrigin: params.remoteApiOrigin } : {},
|
|
3953
3996
|
...params.costCents !== undefined ? { costCents: params.costCents } : {},
|
|
3954
3997
|
artifacts: [],
|
|
3955
3998
|
paths: {
|
|
@@ -4274,7 +4317,7 @@ async function runSkill(name, args, options = {}) {
|
|
|
4274
4317
|
}
|
|
4275
4318
|
const proc = Bun.spawn(["bun", "run", entryPath, ...args], {
|
|
4276
4319
|
cwd: skillPath,
|
|
4277
|
-
stdout: options.stdio === "pipe" ? "pipe" : "inherit",
|
|
4320
|
+
stdout: options.stdio === "pipe" ? "pipe" : options.stdio === "stderr" ? 2 : "inherit",
|
|
4278
4321
|
stderr: options.stdio === "pipe" ? "pipe" : "inherit",
|
|
4279
4322
|
stdin: "inherit",
|
|
4280
4323
|
env: { ...process.env, ...options.env }
|
|
@@ -8964,7 +9007,6 @@ async function completePointerCredential(name, pointerResolution, env = process.
|
|
|
8964
9007
|
});
|
|
8965
9008
|
}
|
|
8966
9009
|
var DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com";
|
|
8967
|
-
var DEFAULT_AUTHORITY_SOURCE = "default";
|
|
8968
9010
|
function defaultFleetGatewayBaseUrl(name) {
|
|
8969
9011
|
return `${DEFAULT_FLEET_GATEWAY_ORIGIN}/${validateAppSlug(name)}`;
|
|
8970
9012
|
}
|
|
@@ -9083,110 +9125,124 @@ function toV1BaseUrl(apiUrl) {
|
|
|
9083
9125
|
url.pathname = `${path}/v1`;
|
|
9084
9126
|
return url.toString().replace(/\/+$/, "");
|
|
9085
9127
|
}
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9128
|
+
var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
9129
|
+
var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
9130
|
+
"host",
|
|
9131
|
+
":authority",
|
|
9132
|
+
"forwarded",
|
|
9133
|
+
"x-forwarded-host",
|
|
9134
|
+
"x-original-host"
|
|
9135
|
+
]);
|
|
9136
|
+
|
|
9137
|
+
// src/lib/instance-credentials.ts
|
|
9138
|
+
import { closeSync as closeSync2, constants, fstatSync as fstatSync2, lstatSync as lstatSync4, openSync as openSync2, readSync } from "fs";
|
|
9139
|
+
var SKILLS_BOUND_API_URL = "HASNA_SKILLS_BOUND_API_URL";
|
|
9140
|
+
function selectedSkillsProfile(env, explicit) {
|
|
9141
|
+
const selected = explicit ?? env.HASNA_PROFILE;
|
|
9142
|
+
if (selected === undefined)
|
|
9143
|
+
return null;
|
|
9144
|
+
const profile = selected.trim();
|
|
9145
|
+
if (!/^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/.test(profile))
|
|
9146
|
+
throw new Error("Invalid Skills credential profile");
|
|
9147
|
+
return profile;
|
|
9095
9148
|
}
|
|
9096
|
-
function
|
|
9097
|
-
env
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
9102
|
-
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
throw new ClientTransportConfigurationError(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
|
|
9107
|
-
}
|
|
9108
|
-
const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
|
|
9109
|
-
if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map((entry) => entry.value)).size > 1) {
|
|
9110
|
-
throw new ClientTransportConfigurationError(name, `${usableUrlEntries.map((entry) => entry.key).join(" and ")} disagree; client authority aliases must be identical or only one may be set.`, usableUrlEntries.map((entry) => entry.key));
|
|
9111
|
-
}
|
|
9112
|
-
const envUrlHit = usableUrlEntries[0] ?? null;
|
|
9113
|
-
const keychainUrlHit = keychainConfigValue(name, env, options.credentials?.keychain);
|
|
9114
|
-
const diskConfigUrlHit = appConfigDiskValue(name, env, keys.apiUrlKeys);
|
|
9115
|
-
if (diskConfigUrlHit?.unusable) {
|
|
9116
|
-
throw new ClientTransportConfigurationError(name, `${diskConfigUrlHit.key} in ${diskConfigUrlHit.path} is declared but blank or malformed; public clients require a valid HTTPS service authority.`, [diskConfigUrlHit.path]);
|
|
9117
|
-
}
|
|
9118
|
-
const urlCandidates = [
|
|
9119
|
-
...envUrlHit ? [envUrlHit] : [],
|
|
9120
|
-
...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
|
|
9121
|
-
...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
|
|
9122
|
-
];
|
|
9123
|
-
const configuredUrl = urlCandidates[0] ?? null;
|
|
9124
|
-
const divergentUrls = urlCandidates.filter((candidate) => candidate.value !== configuredUrl?.value);
|
|
9125
|
-
if (configuredUrl && divergentUrls.length > 0) {
|
|
9126
|
-
throw new ClientTransportConfigurationError(name, `${configuredUrl.key} and ${divergentUrls.map((candidate) => candidate.key).join(" and ")} select different service authorities; refusing to send a credential written for one authority to the other.`, urlCandidates.map((candidate) => candidate.key));
|
|
9127
|
-
}
|
|
9128
|
-
const warnings = [];
|
|
9129
|
-
if (configuredUrl && !envUrlHit) {
|
|
9130
|
-
warnings.push(`No ${keys.apiUrlKeys[0]} in the environment; the server URL in ${configuredUrl.key} was used, so this client connects to the server. ` + `Keep that entry aligned with the intended service authority.`);
|
|
9149
|
+
function skillsProfileCredentialFiles(env, explicit) {
|
|
9150
|
+
return credentialDiskSourceList("skills", env, selectedSkillsProfile(env, explicit)).map((source) => source.path);
|
|
9151
|
+
}
|
|
9152
|
+
function fileIdentity(file) {
|
|
9153
|
+
try {
|
|
9154
|
+
return lstatSync4(file);
|
|
9155
|
+
} catch (error) {
|
|
9156
|
+
if (["ENOENT", "ENOTDIR"].includes(error.code ?? ""))
|
|
9157
|
+
return null;
|
|
9158
|
+
throw new Error("Cannot inspect Skills instance configuration");
|
|
9131
9159
|
}
|
|
9132
|
-
|
|
9133
|
-
|
|
9134
|
-
|
|
9135
|
-
|
|
9136
|
-
|
|
9137
|
-
|
|
9138
|
-
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
let urlHit;
|
|
9142
|
-
if (configuredUrl) {
|
|
9143
|
-
urlHit = configuredUrl;
|
|
9144
|
-
} else {
|
|
9145
|
-
try {
|
|
9146
|
-
urlHit = { key: DEFAULT_AUTHORITY_SOURCE, value: defaultFleetGatewayBaseUrl(name) };
|
|
9147
|
-
} catch (error) {
|
|
9148
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
9149
|
-
throw new ClientTransportConfigurationError(name, `No ${keys.apiUrlKeys[0]} is configured and the default fleet gateway authority cannot be composed for '${name}': ${message}`, [keys.apiUrlKeys[0]]);
|
|
9160
|
+
}
|
|
9161
|
+
function unchanged(before, after) {
|
|
9162
|
+
return before === null || after === null ? before === after : ["dev", "ino", "size", "mtimeMs", "ctimeMs", "mode", "uid"].every((key) => before[key] === after[key]);
|
|
9163
|
+
}
|
|
9164
|
+
function captureSkillsCredentialFiles(files) {
|
|
9165
|
+
const identities = files.map((file) => [file, fileIdentity(file)]);
|
|
9166
|
+
return () => {
|
|
9167
|
+
if (identities.some(([file, before]) => !unchanged(before, fileIdentity(file)))) {
|
|
9168
|
+
throw new Error("Skills instance configuration changed while resolving credentials; retry without sending a credential");
|
|
9150
9169
|
}
|
|
9151
|
-
}
|
|
9152
|
-
|
|
9153
|
-
|
|
9170
|
+
};
|
|
9171
|
+
}
|
|
9172
|
+
function readMetadataText(file) {
|
|
9173
|
+
let fd;
|
|
9154
9174
|
try {
|
|
9155
|
-
|
|
9175
|
+
fd = openSync2(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
9156
9176
|
} catch (error) {
|
|
9157
|
-
|
|
9158
|
-
|
|
9177
|
+
if (["ENOENT", "ENOTDIR"].includes(error.code ?? ""))
|
|
9178
|
+
return null;
|
|
9179
|
+
throw new Error("Cannot safely read Skills instance configuration");
|
|
9180
|
+
}
|
|
9181
|
+
try {
|
|
9182
|
+
const before = fstatSync2(fd);
|
|
9183
|
+
const uid = process.getuid?.() ?? process.geteuid?.();
|
|
9184
|
+
if (!before.isFile() || ![256, 384].includes(before.mode & 4095) || uid !== undefined && before.uid !== uid || before.size > 64 * 1024) {
|
|
9185
|
+
throw new Error("Unsafe Skills instance configuration; expected a bounded owner-only regular file");
|
|
9186
|
+
}
|
|
9187
|
+
const bytes = Buffer.alloc(64 * 1024 + 1);
|
|
9188
|
+
let length = 0;
|
|
9189
|
+
while (length < bytes.length) {
|
|
9190
|
+
const count = readSync(fd, bytes, length, bytes.length - length, null);
|
|
9191
|
+
if (!count)
|
|
9192
|
+
break;
|
|
9193
|
+
length += count;
|
|
9194
|
+
}
|
|
9195
|
+
if (length > 64 * 1024 || !unchanged(before, fstatSync2(fd)) || !unchanged(before, fileIdentity(file))) {
|
|
9196
|
+
throw new Error("Skills instance configuration changed while reading");
|
|
9197
|
+
}
|
|
9198
|
+
return bytes.subarray(0, length).toString("utf8");
|
|
9199
|
+
} finally {
|
|
9200
|
+
closeSync2(fd);
|
|
9159
9201
|
}
|
|
9160
|
-
return {
|
|
9161
|
-
resolution: {
|
|
9162
|
-
transport: "http",
|
|
9163
|
-
transportSource: urlHit.key,
|
|
9164
|
-
baseUrl,
|
|
9165
|
-
apiUrlSource,
|
|
9166
|
-
apiKeyPresent: true,
|
|
9167
|
-
apiKeySource: credential.source,
|
|
9168
|
-
apiKeyTier: credential.tier,
|
|
9169
|
-
misconfigured: false,
|
|
9170
|
-
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
9171
|
-
},
|
|
9172
|
-
credential
|
|
9173
|
-
};
|
|
9174
9202
|
}
|
|
9175
|
-
function
|
|
9176
|
-
|
|
9203
|
+
function readSkillsInstanceMetadata(file) {
|
|
9204
|
+
const text = readMetadataText(file);
|
|
9205
|
+
if (text === null)
|
|
9206
|
+
return {};
|
|
9207
|
+
const values = new Map;
|
|
9208
|
+
for (const line of text.split(/\r?\n/)) {
|
|
9209
|
+
const match = /^\s*(?:export\s+)?(HASNA_SKILLS_API_URL|SKILLS_API_URL|HASNA_SKILLS_BOUND_API_URL)\s*=\s*(.*)$/.exec(line);
|
|
9210
|
+
if (!match)
|
|
9211
|
+
continue;
|
|
9212
|
+
let value = match[2].trim();
|
|
9213
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))
|
|
9214
|
+
value = value.slice(1, -1);
|
|
9215
|
+
if (!value || /[\x00-\x20\x7f]/.test(value) || values.has(match[1]))
|
|
9216
|
+
throw new Error("Invalid Skills instance configuration");
|
|
9217
|
+
values.set(match[1], value);
|
|
9218
|
+
}
|
|
9219
|
+
const urls = [values.get("HASNA_SKILLS_API_URL"), values.get("SKILLS_API_URL")].filter(Boolean);
|
|
9220
|
+
if (new Set(urls).size > 1)
|
|
9221
|
+
throw new Error("Skills API URL aliases disagree");
|
|
9222
|
+
return { apiUrl: urls[0], binding: values.get(SKILLS_BOUND_API_URL) };
|
|
9177
9223
|
}
|
|
9178
|
-
|
|
9179
|
-
|
|
9180
|
-
|
|
9224
|
+
|
|
9225
|
+
// src/lib/local-opt-in.ts
|
|
9226
|
+
var SKILLS_LOCAL_OPT_IN_ENV_KEYS = ["HASNA_SKILLS_LOCAL", "SKILLS_LOCAL"];
|
|
9227
|
+
function isSkillsLocalOptIn(env = process.env) {
|
|
9228
|
+
return SKILLS_LOCAL_OPT_IN_ENV_KEYS.some((key) => (env[key] ?? "").trim() !== "");
|
|
9229
|
+
}
|
|
9230
|
+
function skillsAuthorityEnvKeys() {
|
|
9231
|
+
const keys = clientTransportEnvKeys("skills");
|
|
9232
|
+
return [
|
|
9233
|
+
...keys.apiUrlKeys,
|
|
9234
|
+
...keys.apiKeyKeys,
|
|
9235
|
+
credentialOverrideEnvKey("skills"),
|
|
9236
|
+
credentialPointerEnvKey("skills"),
|
|
9237
|
+
CREDENTIAL_PROFILE_ENV_KEY
|
|
9238
|
+
];
|
|
9239
|
+
}
|
|
9240
|
+
function hasSkillsEnvAuthorityIntent(env = process.env) {
|
|
9241
|
+
return skillsAuthorityEnvKeys().some((key) => (env[key] ?? "").trim() !== "");
|
|
9242
|
+
}
|
|
9243
|
+
function selectsSkillsLocalMode(env = process.env) {
|
|
9244
|
+
return !hasSkillsEnvAuthorityIntent(env) && isSkillsLocalOptIn(env);
|
|
9181
9245
|
}
|
|
9182
|
-
var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
9183
|
-
var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
9184
|
-
"host",
|
|
9185
|
-
":authority",
|
|
9186
|
-
"forwarded",
|
|
9187
|
-
"x-forwarded-host",
|
|
9188
|
-
"x-original-host"
|
|
9189
|
-
]);
|
|
9190
9246
|
|
|
9191
9247
|
// src/lib/fleet-credentials.ts
|
|
9192
9248
|
var SKILLS_APP = "skills";
|
|
@@ -9204,12 +9260,12 @@ class SkillsFleetCredentialError extends Error {
|
|
|
9204
9260
|
this.code = code;
|
|
9205
9261
|
}
|
|
9206
9262
|
}
|
|
9207
|
-
function isClientTransportConfigurationError(error) {
|
|
9208
|
-
return error instanceof ClientTransportConfigurationError || typeof error === "object" && error !== null && error.name === "ClientTransportConfigurationError";
|
|
9209
|
-
}
|
|
9210
9263
|
function isCredentialResolutionError(error) {
|
|
9211
9264
|
return error instanceof CredentialResolutionError || typeof error === "object" && error !== null && error.name === "CredentialResolutionError";
|
|
9212
9265
|
}
|
|
9266
|
+
function isSkillsFleetCredentialError(error) {
|
|
9267
|
+
return error instanceof SkillsFleetCredentialError || typeof error === "object" && error !== null && error.name === "SkillsFleetCredentialError";
|
|
9268
|
+
}
|
|
9213
9269
|
function asSkillsFleetCredentialError(error) {
|
|
9214
9270
|
if (!isCredentialResolutionError(error))
|
|
9215
9271
|
return null;
|
|
@@ -9217,6 +9273,9 @@ function asSkillsFleetCredentialError(error) {
|
|
|
9217
9273
|
}
|
|
9218
9274
|
function normalizeSkillsApiOrigin(apiUrl) {
|
|
9219
9275
|
const url = new URL(apiUrl);
|
|
9276
|
+
if (url.username || url.password || url.search || url.hash || url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))) {
|
|
9277
|
+
throw new SkillsFleetCredentialError("A Skills API URL must use HTTPS (or loopback HTTP), without credentials, query or fragment", "INVALID_API_URL");
|
|
9278
|
+
}
|
|
9220
9279
|
const pathname = url.pathname.replace(/\/+$/, "");
|
|
9221
9280
|
if (pathname === "/api" || pathname === "/api/v1") {
|
|
9222
9281
|
url.pathname = "/";
|
|
@@ -9227,11 +9286,24 @@ function normalizeSkillsApiOrigin(apiUrl) {
|
|
|
9227
9286
|
}
|
|
9228
9287
|
return url.toString().replace(/\/+$/, "");
|
|
9229
9288
|
}
|
|
9230
|
-
function configuredSkillsApiUrl(env = process.env, keychain) {
|
|
9231
|
-
|
|
9232
|
-
|
|
9233
|
-
if (value)
|
|
9234
|
-
|
|
9289
|
+
function configuredSkillsApiUrl(env = process.env, keychain, profile) {
|
|
9290
|
+
const declared = SKILLS_API_URL_ENV_KEYS.filter((key) => env[key] !== undefined).map((key) => ({ key, value: env[key] }));
|
|
9291
|
+
for (const entry of declared) {
|
|
9292
|
+
if (!entry.value.trim() || /[\x00-\x1f\x7f]/.test(entry.value))
|
|
9293
|
+
throw new SkillsFleetCredentialError(`${entry.key} is blank or contains control characters`, "INVALID_API_URL");
|
|
9294
|
+
}
|
|
9295
|
+
const normalized = declared.map((entry) => ({ value: normalizeSkillsApiOrigin(entry.value), source: entry.key }));
|
|
9296
|
+
if (new Set(normalized.map((entry) => entry.value)).size > 1)
|
|
9297
|
+
throw new SkillsFleetCredentialError("Skills API URL aliases disagree", "INVALID_API_URL");
|
|
9298
|
+
if (normalized[0])
|
|
9299
|
+
return normalized[0];
|
|
9300
|
+
if (selectedSkillsProfile(env, profile)) {
|
|
9301
|
+
for (const file of skillsProfileCredentialFiles(env, profile)) {
|
|
9302
|
+
const metadata = readSkillsInstanceMetadata(file);
|
|
9303
|
+
if (metadata.apiUrl || metadata.binding)
|
|
9304
|
+
return { value: metadata.apiUrl ?? metadata.binding, source: file };
|
|
9305
|
+
}
|
|
9306
|
+
return { value: defaultFleetGatewayBaseUrl(SKILLS_APP), source: "default" };
|
|
9235
9307
|
}
|
|
9236
9308
|
const fromKeychain = keychainConfigValue(SKILLS_APP, env, keychain);
|
|
9237
9309
|
if (fromKeychain)
|
|
@@ -9245,7 +9317,7 @@ function configuredSkillsApiUrl(env = process.env, keychain) {
|
|
|
9245
9317
|
return null;
|
|
9246
9318
|
}
|
|
9247
9319
|
function skillsCredentialFiles(env = process.env) {
|
|
9248
|
-
return
|
|
9320
|
+
return skillsProfileCredentialFiles(env);
|
|
9249
9321
|
}
|
|
9250
9322
|
function skillsCredentialFilePath(env = process.env) {
|
|
9251
9323
|
const paths = skillsCredentialFiles(env);
|
|
@@ -9260,11 +9332,15 @@ function noticeLocalSkillsMode(write = (line) => console.error(line)) {
|
|
|
9260
9332
|
if (localNoticePrinted)
|
|
9261
9333
|
return;
|
|
9262
9334
|
localNoticePrinted = true;
|
|
9263
|
-
write(`skills: local mode
|
|
9335
|
+
write(`skills: local mode (${SKILLS_LOCAL_OPT_IN_ENV_KEYS[0]}=1) \u2014 running on this machine against the bundled corpus.`);
|
|
9264
9336
|
}
|
|
9265
9337
|
function resolveSkillsFleet(env = process.env, options = {}) {
|
|
9266
9338
|
try {
|
|
9267
|
-
|
|
9339
|
+
const snapshot = snapshotSkillsEnvironment(env);
|
|
9340
|
+
const resolved = resolveSkillsFleetOrThrow(snapshot, snapshotSkillsOptions(env, options));
|
|
9341
|
+
if (resolved.mode === "local" && env === process.env)
|
|
9342
|
+
noticeLocalSkillsMode();
|
|
9343
|
+
return resolved;
|
|
9268
9344
|
} catch (error) {
|
|
9269
9345
|
const translated = asSkillsFleetCredentialError(error);
|
|
9270
9346
|
if (translated)
|
|
@@ -9272,38 +9348,49 @@ function resolveSkillsFleet(env = process.env, options = {}) {
|
|
|
9272
9348
|
throw error;
|
|
9273
9349
|
}
|
|
9274
9350
|
}
|
|
9275
|
-
function
|
|
9276
|
-
|
|
9277
|
-
|
|
9278
|
-
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
const configured2 = configuredSkillsApiUrl(env, options.credentials?.keychain);
|
|
9283
|
-
const credential2 = resolveCredential(SKILLS_APP, env, options.credentials);
|
|
9284
|
-
if (!configured2 && !credential2) {
|
|
9285
|
-
if (env === process.env)
|
|
9286
|
-
noticeLocalSkillsMode();
|
|
9287
|
-
return { mode: "local", apiOrigin: null, apiKey: null };
|
|
9288
|
-
}
|
|
9289
|
-
if (configured2 && !credential2) {
|
|
9290
|
-
throw new SkillsFleetCredentialError(`${configured2.source} points this CLI at a Skills service but no API key resolved \u2014 ` + `refusing to run locally instead. Looked in the Keychain item ` + `hasna.credentials.${SKILLS_APP}.api-key, then ${skillsCredentialFiles(env).join(" or ") || "no credentials file (no HOME)"}, ` + `then ${SKILLS_API_KEY_ENV}. Sign in with: skills auth login`);
|
|
9351
|
+
function snapshotSkillsEnvironment(env) {
|
|
9352
|
+
const snapshot = {};
|
|
9353
|
+
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(env))) {
|
|
9354
|
+
if (!("value" in descriptor)) {
|
|
9355
|
+
if (/^(?:HASNA_|SKILLS_|HOME$|USER$)/.test(key))
|
|
9356
|
+
throw new SkillsFleetCredentialError("Accessor-backed Skills configuration is unsupported");
|
|
9357
|
+
continue;
|
|
9291
9358
|
}
|
|
9292
|
-
|
|
9359
|
+
snapshot[key] = descriptor.value;
|
|
9293
9360
|
}
|
|
9294
|
-
|
|
9295
|
-
|
|
9361
|
+
return Object.freeze(snapshot);
|
|
9362
|
+
}
|
|
9363
|
+
function snapshotSkillsOptions(env, options) {
|
|
9364
|
+
if (env !== process.env)
|
|
9365
|
+
return options;
|
|
9366
|
+
return { ...options, credentials: { ...options.credentials, keychain: {
|
|
9367
|
+
...options.credentials?.keychain,
|
|
9368
|
+
enabled: options.credentials?.keychain?.enabled ?? true
|
|
9369
|
+
} } };
|
|
9370
|
+
}
|
|
9371
|
+
function resolveSkillsFleetOrThrow(env, options) {
|
|
9372
|
+
if (selectsSkillsLocalMode(env))
|
|
9373
|
+
return { mode: "local", apiOrigin: null, apiKey: null };
|
|
9374
|
+
const assertFilesUnchanged = captureSkillsCredentialFiles(skillsProfileCredentialFiles(env, options.credentials?.profile));
|
|
9375
|
+
const configured = configuredSkillsApiUrl(env, options.credentials?.keychain, options.credentials?.profile);
|
|
9296
9376
|
const credential = resolveCredential(SKILLS_APP, env, options.credentials);
|
|
9297
9377
|
if (!credential) {
|
|
9298
|
-
|
|
9378
|
+
if (!configured) {
|
|
9379
|
+
throw new SkillsFleetCredentialError(`No API key resolved and no Skills API URL is configured \u2014 failing closed ` + `(local mode is opt-in only: set ${SKILLS_LOCAL_OPT_IN_ENV_KEYS[0]}=1 to run on this machine). ` + `Looked in ${credentialLocations(env)}. Sign in with: skills auth login`);
|
|
9380
|
+
}
|
|
9381
|
+
throw new SkillsFleetCredentialError(`${configured.source} points this CLI at a Skills service but no API key resolved \u2014 refusing to run locally instead. ` + `Looked in ${credentialLocations(env)}. Sign in with: skills auth login`);
|
|
9299
9382
|
}
|
|
9383
|
+
const apiOrigin = normalizeSkillsApiOrigin(configured?.value ?? defaultFleetGatewayBaseUrl(SKILLS_APP));
|
|
9384
|
+
toV1BaseUrl(apiOrigin);
|
|
9385
|
+
assertCredentialInstance(credential, apiOrigin, env, options);
|
|
9386
|
+
assertFilesUnchanged();
|
|
9300
9387
|
const base = {
|
|
9301
9388
|
mode: "hosted",
|
|
9302
9389
|
apiOrigin,
|
|
9303
|
-
apiUrlSource:
|
|
9304
|
-
apiKeySource:
|
|
9305
|
-
apiKeyTier:
|
|
9306
|
-
warning:
|
|
9390
|
+
apiUrlSource: configured?.source ?? "default",
|
|
9391
|
+
apiKeySource: credential.source,
|
|
9392
|
+
apiKeyTier: credential.tier,
|
|
9393
|
+
warning: credential.warning
|
|
9307
9394
|
};
|
|
9308
9395
|
if (credential.tier === "pointer") {
|
|
9309
9396
|
return { ...base, apiKey: null, apiKeyPointer: credential };
|
|
@@ -9313,19 +9400,41 @@ function resolveSkillsFleetOrThrow(env, options) {
|
|
|
9313
9400
|
}
|
|
9314
9401
|
return { ...base, apiKey: credential.apiKey, apiKeyPointer: null };
|
|
9315
9402
|
}
|
|
9403
|
+
function credentialLocations(env) {
|
|
9404
|
+
const files = skillsCredentialFiles(env).join(" or ") || "no credentials file (HOME is unset)";
|
|
9405
|
+
return `hasna.credentials.${SKILLS_APP}.api-key (macOS Keychain, account HASNA_STATION or the host name), ${files}, and ${SKILLS_API_KEY_ENV}`;
|
|
9406
|
+
}
|
|
9407
|
+
function assertCredentialInstance(credential, apiOrigin, env, options) {
|
|
9408
|
+
let bound;
|
|
9409
|
+
if (credential.tier === "disk" || credential.tier === "profile") {
|
|
9410
|
+
const metadata = readSkillsInstanceMetadata(credential.source);
|
|
9411
|
+
bound = metadata.binding ?? metadata.apiUrl ?? defaultFleetGatewayBaseUrl(SKILLS_APP);
|
|
9412
|
+
} else if (credential.tier === "keychain") {
|
|
9413
|
+
bound = keychainConfigValue(SKILLS_APP, env, options.credentials?.keychain)?.value ?? defaultFleetGatewayBaseUrl(SKILLS_APP);
|
|
9414
|
+
}
|
|
9415
|
+
if (bound && normalizeSkillsApiOrigin(bound) !== apiOrigin) {
|
|
9416
|
+
throw new SkillsFleetCredentialError("The selected Skills API does not match this credential's instance. Select its profile or sign in to the new instance; no credential was sent.", "INSTANCE_CREDENTIAL_MISMATCH");
|
|
9417
|
+
}
|
|
9418
|
+
}
|
|
9316
9419
|
async function resolveSkillsApiKey(env = process.env, options = {}) {
|
|
9317
|
-
|
|
9420
|
+
return (await resolveSkillsConnection(env, options))?.apiKey ?? null;
|
|
9421
|
+
}
|
|
9422
|
+
async function resolveSkillsConnection(env = process.env, options = {}) {
|
|
9423
|
+
const snapshotEnv = snapshotSkillsEnvironment(env);
|
|
9424
|
+
const fleet = resolveSkillsFleet(snapshotEnv, snapshotSkillsOptions(env, options));
|
|
9425
|
+
if (fleet.mode === "local" && env === process.env)
|
|
9426
|
+
noticeLocalSkillsMode();
|
|
9318
9427
|
if (fleet.mode !== "hosted")
|
|
9319
9428
|
return null;
|
|
9320
9429
|
if (fleet.apiKey)
|
|
9321
|
-
return fleet.apiKey;
|
|
9430
|
+
return { ...fleet, apiKey: fleet.apiKey };
|
|
9322
9431
|
const pointer = fleet.apiKeyPointer;
|
|
9323
9432
|
if (!pointer) {
|
|
9324
9433
|
throw new SkillsFleetCredentialError(`A Skills authority resolved but no API key did. Sign in with: skills auth login`);
|
|
9325
9434
|
}
|
|
9326
9435
|
let completed;
|
|
9327
9436
|
try {
|
|
9328
|
-
completed = await completePointerCredential(SKILLS_APP, pointer,
|
|
9437
|
+
completed = await completePointerCredential(SKILLS_APP, pointer, snapshotEnv);
|
|
9329
9438
|
} catch (error) {
|
|
9330
9439
|
const translated = asSkillsFleetCredentialError(error);
|
|
9331
9440
|
if (translated)
|
|
@@ -9335,7 +9444,7 @@ async function resolveSkillsApiKey(env = process.env, options = {}) {
|
|
|
9335
9444
|
if (!completed.apiKey?.trim()) {
|
|
9336
9445
|
throw new SkillsFleetCredentialError(`${credentialPointerEnvKey(SKILLS_APP)} names a vault item that produced an empty Skills API key \u2014 ` + `refusing to send an unauthenticated request.`);
|
|
9337
9446
|
}
|
|
9338
|
-
return completed.apiKey;
|
|
9447
|
+
return { ...fleet, apiKey: completed.apiKey, apiKeyPointer: null };
|
|
9339
9448
|
}
|
|
9340
9449
|
async function requireSkillsApiKey(action = "This command", env = process.env, options = {}) {
|
|
9341
9450
|
const apiKey = await resolveSkillsApiKey(env, options);
|
|
@@ -9343,27 +9452,31 @@ async function requireSkillsApiKey(action = "This command", env = process.env, o
|
|
|
9343
9452
|
throw new MissingSkillsFleetError(action);
|
|
9344
9453
|
return apiKey;
|
|
9345
9454
|
}
|
|
9346
|
-
function stripV1(baseUrl) {
|
|
9347
|
-
return baseUrl.replace(/\/v1$/, "").replace(/\/+$/, "");
|
|
9348
|
-
}
|
|
9349
9455
|
async function skillsCredentialOrReason(env = process.env, options = {}) {
|
|
9350
9456
|
try {
|
|
9351
|
-
const
|
|
9352
|
-
return
|
|
9457
|
+
const connection = await resolveSkillsConnection(env, options);
|
|
9458
|
+
return connection ? { apiKey: connection.apiKey, apiOrigin: connection.apiOrigin, reason: null } : { apiKey: null, apiOrigin: null, reason: null };
|
|
9353
9459
|
} catch (error) {
|
|
9354
|
-
if (error
|
|
9355
|
-
return { apiKey: null, reason: error.message };
|
|
9460
|
+
if (isSkillsFleetCredentialError(error)) {
|
|
9461
|
+
return { apiKey: null, apiOrigin: null, reason: error.message };
|
|
9356
9462
|
}
|
|
9357
9463
|
throw error;
|
|
9358
9464
|
}
|
|
9359
9465
|
}
|
|
9360
9466
|
function resolveSkillsApiOrigin(env = process.env, options = {}) {
|
|
9361
|
-
const configured = configuredSkillsApiUrl(env, options.credentials?.keychain);
|
|
9467
|
+
const configured = configuredSkillsApiUrl(env, options.credentials?.keychain, options.credentials?.profile);
|
|
9362
9468
|
if (configured) {
|
|
9363
9469
|
toV1BaseUrl(configured.value);
|
|
9364
9470
|
return { origin: normalizeSkillsApiOrigin(configured.value), source: configured.source };
|
|
9365
9471
|
}
|
|
9366
|
-
|
|
9472
|
+
let fleet;
|
|
9473
|
+
try {
|
|
9474
|
+
fleet = resolveSkillsFleet(env, options);
|
|
9475
|
+
} catch (error) {
|
|
9476
|
+
if (isSkillsFleetCredentialError(error))
|
|
9477
|
+
return null;
|
|
9478
|
+
throw error;
|
|
9479
|
+
}
|
|
9367
9480
|
return fleet.mode === "hosted" ? { origin: fleet.apiOrigin, source: fleet.apiUrlSource } : null;
|
|
9368
9481
|
}
|
|
9369
9482
|
function requireSkillsApiOrigin(action = "This command", env = process.env, options = {}) {
|
|
@@ -9595,12 +9708,21 @@ function parseRemoteContract(schema, payload, message) {
|
|
|
9595
9708
|
}
|
|
9596
9709
|
async function remoteRequestHeaders(options) {
|
|
9597
9710
|
const headers = new Headers({ Accept: "application/json" });
|
|
9598
|
-
const token = options.authToken !== undefined ? options.authToken : await
|
|
9711
|
+
const token = options.authToken !== undefined ? options.authToken : await ambientTokenFor(options.apiUrl);
|
|
9599
9712
|
const trimmed = token?.trim();
|
|
9600
9713
|
if (trimmed)
|
|
9601
9714
|
headers.set("Authorization", `Bearer ${trimmed}`);
|
|
9602
9715
|
return headers;
|
|
9603
9716
|
}
|
|
9717
|
+
async function ambientTokenFor(callerApiUrl) {
|
|
9718
|
+
const connection = await resolveSkillsConnection();
|
|
9719
|
+
if (!connection)
|
|
9720
|
+
return null;
|
|
9721
|
+
if (callerApiUrl !== undefined && normalizeSkillsApiOrigin(callerApiUrl) !== connection.apiOrigin) {
|
|
9722
|
+
throw new SkillsFleetCredentialError(`The Skills credential resolved for ${connection.apiOrigin} is never sent to a caller-supplied apiUrl ` + `(${normalizeSkillsApiOrigin(callerApiUrl)}). Pass an explicit authToken for that instance, or authToken: null ` + `for an unauthenticated read; no credential was sent.`, "INSTANCE_CREDENTIAL_MISMATCH");
|
|
9723
|
+
}
|
|
9724
|
+
return connection.apiKey;
|
|
9725
|
+
}
|
|
9604
9726
|
async function fetchRemoteJson(url, options) {
|
|
9605
9727
|
const fetchImpl = options.fetchImpl || fetch;
|
|
9606
9728
|
const headers = await remoteRequestHeaders(options);
|
|
@@ -10132,6 +10254,108 @@ function primitiveHaystack(primitive) {
|
|
|
10132
10254
|
function clone(value) {
|
|
10133
10255
|
return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
|
|
10134
10256
|
}
|
|
10257
|
+
// src/lib/remote-workspace.ts
|
|
10258
|
+
var record = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
10259
|
+
var cursor = (value) => typeof value === "string" && /^[A-Za-z0-9_-]{1,512}$/.test(value);
|
|
10260
|
+
var uuid = (value) => typeof value === "string" && /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(value);
|
|
10261
|
+
function workspaceMembersQuery(options = {}) {
|
|
10262
|
+
if (!record(options) || Object.keys(options).some((key) => key !== "limit" && key !== "cursor") || options.limit !== undefined && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 100) || options.cursor !== undefined && !cursor(options.cursor))
|
|
10263
|
+
throw new Error("Use a roster limit from 1 to 100 and an unchanged continuation cursor.");
|
|
10264
|
+
const query = new URLSearchParams;
|
|
10265
|
+
if (options.limit !== undefined)
|
|
10266
|
+
query.set("limit", String(options.limit));
|
|
10267
|
+
if (options.cursor !== undefined)
|
|
10268
|
+
query.set("cursor", options.cursor);
|
|
10269
|
+
return query.size ? `?${query}` : "";
|
|
10270
|
+
}
|
|
10271
|
+
function timestamp(value) {
|
|
10272
|
+
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z$/.test(value))
|
|
10273
|
+
return false;
|
|
10274
|
+
const time = Date.parse(value);
|
|
10275
|
+
return Number.isFinite(time) && new Date(time).toISOString().slice(0, 23) === value.slice(0, 23);
|
|
10276
|
+
}
|
|
10277
|
+
function parseMember(row, fail) {
|
|
10278
|
+
if (!record(row) || !uuid(row.membershipId) || !uuid(row.userId) || typeof row.email !== "string" || !row.email || !(row.displayName === null || typeof row.displayName === "string") || !isRole(row.role) || !timestamp(row.createdAt))
|
|
10279
|
+
return fail();
|
|
10280
|
+
return {
|
|
10281
|
+
membershipId: row.membershipId,
|
|
10282
|
+
userId: row.userId,
|
|
10283
|
+
email: row.email,
|
|
10284
|
+
displayName: row.displayName,
|
|
10285
|
+
role: row.role,
|
|
10286
|
+
createdAt: row.createdAt
|
|
10287
|
+
};
|
|
10288
|
+
}
|
|
10289
|
+
var isRole = (value) => typeof value === "string" && ["owner", "admin", "member", "viewer"].includes(value);
|
|
10290
|
+
|
|
10291
|
+
class WorkspaceMemberInputError extends Error {
|
|
10292
|
+
constructor() {
|
|
10293
|
+
super("Use an unchanged lowercase membership ID and the exact role and expected-role parameters from the roster.");
|
|
10294
|
+
this.name = "WorkspaceMemberInputError";
|
|
10295
|
+
}
|
|
10296
|
+
}
|
|
10297
|
+
function mutationInput(membershipId, input, roleChange) {
|
|
10298
|
+
if (typeof membershipId !== "string" || !uuid(membershipId) || membershipId !== membershipId.toLowerCase() || !record(input) || Object.keys(input).sort().join(",") !== (roleChange ? "expectedRole,role" : "expectedRole"))
|
|
10299
|
+
throw new WorkspaceMemberInputError;
|
|
10300
|
+
const expectedRole = input.expectedRole, role = roleChange ? input.role : undefined;
|
|
10301
|
+
if (!isRole(expectedRole) || roleChange && !isRole(role))
|
|
10302
|
+
throw new WorkspaceMemberInputError;
|
|
10303
|
+
return { membershipId, role, expectedRole };
|
|
10304
|
+
}
|
|
10305
|
+
function workspaceMemberRoleInput(membershipId, input) {
|
|
10306
|
+
const value = mutationInput(membershipId, input, true);
|
|
10307
|
+
return { membershipId: value.membershipId, body: { role: value.role, expectedRole: value.expectedRole } };
|
|
10308
|
+
}
|
|
10309
|
+
function workspaceMemberRemovalInput(membershipId, input) {
|
|
10310
|
+
const value = mutationInput(membershipId, input, false);
|
|
10311
|
+
return { membershipId: value.membershipId, body: { expectedRole: value.expectedRole } };
|
|
10312
|
+
}
|
|
10313
|
+
var invalidMemberResult = "The server returned an invalid workspace member result. Refresh the roster before another action.";
|
|
10314
|
+
function parseWorkspaceMemberRoleResult(value, membershipId, role) {
|
|
10315
|
+
const fail = () => {
|
|
10316
|
+
throw new Error(invalidMemberResult);
|
|
10317
|
+
};
|
|
10318
|
+
if (!record(value) || !uuid(value.organizationId) || typeof value.changed !== "boolean")
|
|
10319
|
+
return fail();
|
|
10320
|
+
const member = parseMember(value.member, fail);
|
|
10321
|
+
if (member.membershipId !== membershipId || member.role !== role)
|
|
10322
|
+
return fail();
|
|
10323
|
+
return { organizationId: value.organizationId, member, changed: value.changed };
|
|
10324
|
+
}
|
|
10325
|
+
function parseWorkspaceMemberRemovalResult(value, membershipId) {
|
|
10326
|
+
if (!record(value) || !uuid(value.organizationId) || value.membershipId !== membershipId || value.removed !== true || typeof value.alreadyRemoved !== "boolean")
|
|
10327
|
+
throw new Error(invalidMemberResult);
|
|
10328
|
+
return { organizationId: value.organizationId, membershipId, removed: true, alreadyRemoved: value.alreadyRemoved };
|
|
10329
|
+
}
|
|
10330
|
+
var workspaceMemberFailures = {
|
|
10331
|
+
INVALID_REQUEST: [400, "Provide the exact membership role parameters."],
|
|
10332
|
+
ACCOUNT_UNAVAILABLE: [403, "Account is unavailable."],
|
|
10333
|
+
INTERACTIVE_SESSION_REQUIRED: [403, "Fresh interactive sign-in is required."],
|
|
10334
|
+
WORKSPACE_ADMIN_REQUIRED: [403, "A current workspace owner or admin is required."],
|
|
10335
|
+
MEMBERSHIP_ACTION_FORBIDDEN: [403, "Your current workspace role cannot perform this membership action."],
|
|
10336
|
+
MEMBERSHIP_NOT_FOUND: [404, "Membership was not found in the current workspace."],
|
|
10337
|
+
SELF_REMOVAL_UNAVAILABLE: [409, "Leaving your own workspace is not available through member removal."],
|
|
10338
|
+
MEMBERSHIP_ROLE_CHANGED: [409, "The member role changed. Refresh the roster before another action."],
|
|
10339
|
+
LAST_OWNER_REQUIRED: [409, "The workspace must retain at least one active owner."],
|
|
10340
|
+
MEMBERSHIP_BUSY: [503, "Membership is busy. Refresh the roster before another action."]
|
|
10341
|
+
};
|
|
10342
|
+
function workspaceMemberFailure(value, status) {
|
|
10343
|
+
if (!record(value) || typeof value.code !== "string" || !Object.hasOwn(workspaceMemberFailures, value.code))
|
|
10344
|
+
return null;
|
|
10345
|
+
const code = value.code;
|
|
10346
|
+
return workspaceMemberFailures[code][0] === status ? code : null;
|
|
10347
|
+
}
|
|
10348
|
+
function parseWorkspaceMembersPage(value) {
|
|
10349
|
+
const fail = () => {
|
|
10350
|
+
throw new Error("The server returned an invalid workspace roster.");
|
|
10351
|
+
};
|
|
10352
|
+
if (!record(value) || !uuid(value.organizationId) || !Array.isArray(value.members) || value.members.length > 100 || !(value.nextCursor === null || cursor(value.nextCursor)) || !value.members.length && value.nextCursor !== null)
|
|
10353
|
+
return fail();
|
|
10354
|
+
const members = value.members.map((row) => parseMember(row, fail));
|
|
10355
|
+
if (new Set(members.map((row) => row.membershipId)).size !== members.length)
|
|
10356
|
+
return fail();
|
|
10357
|
+
return { organizationId: value.organizationId, members, nextCursor: value.nextCursor };
|
|
10358
|
+
}
|
|
10135
10359
|
// src/lib/auth-store.ts
|
|
10136
10360
|
function getApiUrl(action, env = process.env, options = {}) {
|
|
10137
10361
|
return requireSkillsApiOrigin(action, env, options);
|
|
@@ -10140,47 +10364,213 @@ function getApiUrl(action, env = process.env, options = {}) {
|
|
|
10140
10364
|
// src/lib/remote-run-contract.ts
|
|
10141
10365
|
var REMOTE_SKILL_RUN_CONTRACT_VERSION = 1;
|
|
10142
10366
|
function normalizeRemoteSkillRunContract(payload, fallbackSkill) {
|
|
10143
|
-
const
|
|
10367
|
+
const record2 = isRecord3(payload) ? payload : {};
|
|
10144
10368
|
return {
|
|
10145
10369
|
contractVersion: REMOTE_SKILL_RUN_CONTRACT_VERSION,
|
|
10146
|
-
...pickString(
|
|
10147
|
-
skill: pickStringValue(
|
|
10148
|
-
...pickString(
|
|
10149
|
-
...pickString(
|
|
10150
|
-
...pickNumber(
|
|
10151
|
-
...pickString(
|
|
10152
|
-
...pickString(
|
|
10153
|
-
...pickString(
|
|
10154
|
-
...pickString(
|
|
10155
|
-
...pickNumber(
|
|
10156
|
-
...pickString(
|
|
10157
|
-
...hasOwn(
|
|
10158
|
-
...pickString(
|
|
10159
|
-
...pickString(
|
|
10160
|
-
...pickString(
|
|
10161
|
-
...pickString(
|
|
10162
|
-
...hasOwn(
|
|
10370
|
+
...pickString(record2, "id"),
|
|
10371
|
+
skill: pickStringValue(record2, "skill") ?? fallbackSkill,
|
|
10372
|
+
...pickString(record2, "requestedSlug"),
|
|
10373
|
+
...pickString(record2, "status"),
|
|
10374
|
+
...pickNumber(record2, "exitCode"),
|
|
10375
|
+
...pickString(record2, "correlationId"),
|
|
10376
|
+
...pickString(record2, "createdAt"),
|
|
10377
|
+
...pickString(record2, "startedAt"),
|
|
10378
|
+
...pickString(record2, "completedAt"),
|
|
10379
|
+
...pickNumber(record2, "durationMs"),
|
|
10380
|
+
...pickString(record2, "outputType"),
|
|
10381
|
+
...hasOwn(record2, "outputPreview") ? { outputPreview: record2.outputPreview } : {},
|
|
10382
|
+
...pickString(record2, "errorCode"),
|
|
10383
|
+
...pickString(record2, "errorMessage"),
|
|
10384
|
+
...pickString(record2, "error"),
|
|
10385
|
+
...pickString(record2, "code"),
|
|
10386
|
+
...hasOwn(record2, "details") ? { details: record2.details } : {}
|
|
10163
10387
|
};
|
|
10164
10388
|
}
|
|
10165
10389
|
function isRecord3(value) {
|
|
10166
10390
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
10167
10391
|
}
|
|
10168
|
-
function hasOwn(
|
|
10169
|
-
return Object.prototype.hasOwnProperty.call(
|
|
10392
|
+
function hasOwn(record2, key) {
|
|
10393
|
+
return Object.prototype.hasOwnProperty.call(record2, key);
|
|
10170
10394
|
}
|
|
10171
|
-
function pickString(
|
|
10172
|
-
const value = pickStringValue(
|
|
10395
|
+
function pickString(record2, key) {
|
|
10396
|
+
const value = pickStringValue(record2, key);
|
|
10173
10397
|
return value === undefined ? {} : { [key]: value };
|
|
10174
10398
|
}
|
|
10175
|
-
function pickStringValue(
|
|
10176
|
-
const value =
|
|
10399
|
+
function pickStringValue(record2, key) {
|
|
10400
|
+
const value = record2[key];
|
|
10177
10401
|
return typeof value === "string" ? value : undefined;
|
|
10178
10402
|
}
|
|
10179
|
-
function pickNumber(
|
|
10180
|
-
const value =
|
|
10403
|
+
function pickNumber(record2, key) {
|
|
10404
|
+
const value = record2[key];
|
|
10181
10405
|
return typeof value === "number" && Number.isFinite(value) ? { [key]: value } : {};
|
|
10182
10406
|
}
|
|
10183
10407
|
|
|
10408
|
+
// src/lib/remote-account.ts
|
|
10409
|
+
class RemoteCreditApprovalError extends Error {
|
|
10410
|
+
requiredCredits;
|
|
10411
|
+
maximumCredits;
|
|
10412
|
+
code = "CREDIT_APPROVAL_REQUIRED";
|
|
10413
|
+
constructor(requiredCredits, maximumCredits) {
|
|
10414
|
+
super(`This run requires ${requiredCredits} credits; the approved maximum is ${maximumCredits}. Quote the run and explicitly approve its cost.`);
|
|
10415
|
+
this.requiredCredits = requiredCredits;
|
|
10416
|
+
this.maximumCredits = maximumCredits;
|
|
10417
|
+
this.name = "RemoteCreditApprovalError";
|
|
10418
|
+
}
|
|
10419
|
+
}
|
|
10420
|
+
function creditCount(value) {
|
|
10421
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 || value > 2147483647) {
|
|
10422
|
+
throw new Error("The Skills server returned an invalid credit count");
|
|
10423
|
+
}
|
|
10424
|
+
return value;
|
|
10425
|
+
}
|
|
10426
|
+
function parseRemoteRunQuote(value) {
|
|
10427
|
+
const quote = object(value);
|
|
10428
|
+
const pricing = object(quote.pricing);
|
|
10429
|
+
if (typeof quote.skill !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(quote.skill))
|
|
10430
|
+
throw new Error("Invalid quoted skill");
|
|
10431
|
+
const costCents = creditCount(pricing.costCredits ?? pricing.costCents);
|
|
10432
|
+
if (pricing.costCredits !== undefined && pricing.costCents !== undefined && pricing.costCents !== costCents)
|
|
10433
|
+
throw new Error("Inconsistent quoted credit count");
|
|
10434
|
+
if (quote.availability && object(quote.availability).status !== "available")
|
|
10435
|
+
throw new Error("This skill is unavailable for remote execution");
|
|
10436
|
+
return { ...quote, skill: quote.skill, pricing: { ...pricing, costCredits: costCents, costCents, formattedCost: `${costCents} credits` } };
|
|
10437
|
+
}
|
|
10438
|
+
function parseRemoteCreditPacks(value) {
|
|
10439
|
+
if (!Array.isArray(value))
|
|
10440
|
+
throw new Error("Invalid credit pack response");
|
|
10441
|
+
const ids = new Set;
|
|
10442
|
+
return value.map((value2) => {
|
|
10443
|
+
const row = object(value2);
|
|
10444
|
+
const counts = [row.credits, row.creditsCents, row.amountCents].filter((value3) => value3 !== undefined).map(creditCount);
|
|
10445
|
+
if (!counts.length || counts[0] === 0 || counts.some((count) => count !== counts[0]))
|
|
10446
|
+
throw new Error("Inconsistent credit pack counts");
|
|
10447
|
+
const credits = counts[0];
|
|
10448
|
+
const id = row.id ?? `credits_${credits}`;
|
|
10449
|
+
if (typeof id !== "string" || !/^[a-z0-9][a-z0-9_-]{0,99}$/.test(id) || ids.has(id))
|
|
10450
|
+
throw new Error("Invalid credit pack ID");
|
|
10451
|
+
if (id.startsWith("credits_") && id !== `credits_${credits}`)
|
|
10452
|
+
throw new Error("Inconsistent credit pack ID");
|
|
10453
|
+
ids.add(id);
|
|
10454
|
+
return { id, credits, ...row.expiresInDays === undefined ? {} : { expiresInDays: creditCount(row.expiresInDays) } };
|
|
10455
|
+
});
|
|
10456
|
+
}
|
|
10457
|
+
function parseRemoteBillingStatus(value) {
|
|
10458
|
+
const row = object(value);
|
|
10459
|
+
const counts = [row.creditBalance, row.balanceCents].filter((value2) => value2 !== undefined).map(creditCount);
|
|
10460
|
+
if (!counts.length || counts.some((count) => count !== counts[0]))
|
|
10461
|
+
throw new Error("Inconsistent credit balance");
|
|
10462
|
+
return {
|
|
10463
|
+
creditBalance: counts[0],
|
|
10464
|
+
formattedCreditBalance: `${counts[0]} credits`,
|
|
10465
|
+
...typeof row.plan === "string" ? { plan: row.plan } : {},
|
|
10466
|
+
...typeof row.hasPaymentMethod === "boolean" ? { hasPaymentMethod: row.hasPaymentMethod } : {}
|
|
10467
|
+
};
|
|
10468
|
+
}
|
|
10469
|
+
function parseRemoteCheckout(value) {
|
|
10470
|
+
const row = object(value);
|
|
10471
|
+
if (typeof row.url !== "string")
|
|
10472
|
+
throw new Error("Invalid checkout URL");
|
|
10473
|
+
const url = new URL(row.url);
|
|
10474
|
+
if (url.protocol !== "https:" || url.username || url.password)
|
|
10475
|
+
throw new Error("Invalid checkout URL");
|
|
10476
|
+
return { url: row.url };
|
|
10477
|
+
}
|
|
10478
|
+
function object(value) {
|
|
10479
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
10480
|
+
throw new Error("Invalid Skills server response");
|
|
10481
|
+
return value;
|
|
10482
|
+
}
|
|
10483
|
+
|
|
10484
|
+
// src/lib/remote-files.ts
|
|
10485
|
+
import { createHash as createHash3 } from "crypto";
|
|
10486
|
+
var MAX_REMOTE_FILE_BYTES = 64 * 1024 * 1024;
|
|
10487
|
+
function describeRemoteFiles(files) {
|
|
10488
|
+
if (files.length > 10)
|
|
10489
|
+
throw new Error("At most 10 input files are supported");
|
|
10490
|
+
const names = new Set;
|
|
10491
|
+
let total = 0;
|
|
10492
|
+
return files.map((file) => {
|
|
10493
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(file.name) || file.name === "." || file.name === ".." || names.has(file.name))
|
|
10494
|
+
throw new Error("Input file names must be unique safe basenames");
|
|
10495
|
+
names.add(file.name);
|
|
10496
|
+
total += file.bytes.byteLength;
|
|
10497
|
+
if (file.bytes.byteLength > 20 * 1024 * 1024 || total > 50 * 1024 * 1024)
|
|
10498
|
+
throw new Error("Input files exceed the supported size limit");
|
|
10499
|
+
return { name: file.name, sizeBytes: file.bytes.byteLength, sha256: sha256(file.bytes), contentType: file.contentType ?? "application/octet-stream" };
|
|
10500
|
+
});
|
|
10501
|
+
}
|
|
10502
|
+
function sha256(bytes) {
|
|
10503
|
+
return createHash3("sha256").update(bytes).digest("hex");
|
|
10504
|
+
}
|
|
10505
|
+
async function readBoundedResponse(response, maximum) {
|
|
10506
|
+
if (!Number.isSafeInteger(maximum) || maximum < 0 || maximum > MAX_REMOTE_FILE_BYTES)
|
|
10507
|
+
throw new Error("Invalid artifact size limit");
|
|
10508
|
+
const length = response.headers.get("content-length");
|
|
10509
|
+
if (length && (!/^\d+$/.test(length) || Number(length) > maximum)) {
|
|
10510
|
+
await response.body?.cancel();
|
|
10511
|
+
throw new Error("Artifact exceeds its declared size limit");
|
|
10512
|
+
}
|
|
10513
|
+
const reader = response.body?.getReader();
|
|
10514
|
+
if (!reader)
|
|
10515
|
+
return new Uint8Array;
|
|
10516
|
+
const chunks = [];
|
|
10517
|
+
let size = 0;
|
|
10518
|
+
try {
|
|
10519
|
+
while (true) {
|
|
10520
|
+
const next = await reader.read();
|
|
10521
|
+
if (next.done)
|
|
10522
|
+
break;
|
|
10523
|
+
size += next.value.byteLength;
|
|
10524
|
+
if (size > maximum)
|
|
10525
|
+
throw new Error("Artifact exceeds its declared size limit");
|
|
10526
|
+
chunks.push(next.value);
|
|
10527
|
+
}
|
|
10528
|
+
} catch (error) {
|
|
10529
|
+
await reader.cancel().catch(() => {});
|
|
10530
|
+
throw error;
|
|
10531
|
+
} finally {
|
|
10532
|
+
reader.releaseLock();
|
|
10533
|
+
}
|
|
10534
|
+
const bytes = new Uint8Array(size);
|
|
10535
|
+
let offset = 0;
|
|
10536
|
+
for (const chunk of chunks) {
|
|
10537
|
+
bytes.set(chunk, offset);
|
|
10538
|
+
offset += chunk.byteLength;
|
|
10539
|
+
}
|
|
10540
|
+
return bytes;
|
|
10541
|
+
}
|
|
10542
|
+
|
|
10543
|
+
// src/lib/remote-profile.ts
|
|
10544
|
+
function customerNamePatch(input, field) {
|
|
10545
|
+
if (!isRecord4(input) || Object.keys(input).length !== 1 || !Object.hasOwn(input, field))
|
|
10546
|
+
throw new Error("Provide only the requested name field.");
|
|
10547
|
+
const value = input[field];
|
|
10548
|
+
if (typeof value !== "string" || /[\p{Cc}\p{Cs}\u2028\u2029]/u.test(value) || !value.trim() || [...value.trim()].length > 100) {
|
|
10549
|
+
throw new Error("Use a name of 1\u2013100 characters without control characters or newlines.");
|
|
10550
|
+
}
|
|
10551
|
+
return { [field]: value.trim() };
|
|
10552
|
+
}
|
|
10553
|
+
function isRecord4(value) {
|
|
10554
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
10555
|
+
}
|
|
10556
|
+
function string(value) {
|
|
10557
|
+
return typeof value === "string" && value.length > 0;
|
|
10558
|
+
}
|
|
10559
|
+
function parseUpdatedProfile(value) {
|
|
10560
|
+
const user = isRecord4(value) && value.user;
|
|
10561
|
+
if (!isRecord4(user) || !string(user.id) || !string(user.email) || !(user.displayName === null || typeof user.displayName === "string") || typeof user.role !== "string" || !["owner", "admin", "member", "viewer"].includes(user.role)) {
|
|
10562
|
+
throw new Error("The server returned an invalid account profile.");
|
|
10563
|
+
}
|
|
10564
|
+
return { user: { id: user.id, email: user.email, displayName: user.displayName, role: user.role } };
|
|
10565
|
+
}
|
|
10566
|
+
function parseUpdatedWorkspace(value) {
|
|
10567
|
+
const organization = isRecord4(value) && value.organization;
|
|
10568
|
+
if (!isRecord4(organization) || !string(organization.id) || !string(organization.slug) || !string(organization.name)) {
|
|
10569
|
+
throw new Error("The server returned an invalid workspace.");
|
|
10570
|
+
}
|
|
10571
|
+
return { organization: { id: organization.id, slug: organization.slug, name: organization.name } };
|
|
10572
|
+
}
|
|
10573
|
+
|
|
10184
10574
|
// src/lib/remote-client.ts
|
|
10185
10575
|
class RemoteRouteUnsupportedError extends Error {
|
|
10186
10576
|
path;
|
|
@@ -10198,24 +10588,46 @@ class RemoteRouteUnsupportedError extends Error {
|
|
|
10198
10588
|
class RemoteRequestError extends Error {
|
|
10199
10589
|
path;
|
|
10200
10590
|
status;
|
|
10201
|
-
constructor(path, status,
|
|
10202
|
-
super(`Remote request to ${path} failed: HTTP ${status}
|
|
10591
|
+
constructor(path, status, _statusText) {
|
|
10592
|
+
super(`Remote request to ${path} failed: HTTP ${status}`);
|
|
10203
10593
|
this.path = path;
|
|
10204
10594
|
this.status = status;
|
|
10205
10595
|
this.name = "RemoteRequestError";
|
|
10206
10596
|
}
|
|
10207
10597
|
}
|
|
10208
10598
|
|
|
10599
|
+
class RemoteWorkspaceMemberError extends RemoteRequestError {
|
|
10600
|
+
code;
|
|
10601
|
+
constructor(path, code) {
|
|
10602
|
+
super(path, workspaceMemberFailures[code][0]);
|
|
10603
|
+
this.code = code;
|
|
10604
|
+
this.name = "RemoteWorkspaceMemberError";
|
|
10605
|
+
this.message = workspaceMemberFailures[code][1];
|
|
10606
|
+
}
|
|
10607
|
+
}
|
|
10608
|
+
|
|
10609
|
+
class RemoteCapabilityUnavailableError extends RemoteRequestError {
|
|
10610
|
+
code = "SUBSCRIPTION_CHECKOUT_UNAVAILABLE";
|
|
10611
|
+
constructor() {
|
|
10612
|
+
super("/api/v1/billing/checkout", 503);
|
|
10613
|
+
this.name = "RemoteCapabilityUnavailableError";
|
|
10614
|
+
this.message = "Subscription checkout is unavailable on the configured Skills server. " + "Use skills credits packs to view credit packs, or skills billing portal to manage an existing subscription.";
|
|
10615
|
+
}
|
|
10616
|
+
}
|
|
10617
|
+
|
|
10209
10618
|
class RemoteSkillsClient {
|
|
10210
10619
|
apiUrl;
|
|
10211
10620
|
apiKey;
|
|
10621
|
+
capabilities;
|
|
10212
10622
|
constructor(apiKey, apiUrl = getApiUrl()) {
|
|
10213
10623
|
this.apiKey = apiKey;
|
|
10214
|
-
this.apiUrl = apiUrl
|
|
10624
|
+
this.apiUrl = normalizeSkillsApiOrigin(apiUrl);
|
|
10215
10625
|
}
|
|
10216
10626
|
async request(path, options) {
|
|
10217
10627
|
return fetch(`${this.apiUrl}${path}`, {
|
|
10218
10628
|
...options,
|
|
10629
|
+
redirect: "error",
|
|
10630
|
+
signal: options?.signal ?? AbortSignal.timeout(15000),
|
|
10219
10631
|
headers: {
|
|
10220
10632
|
Authorization: `Bearer ${this.apiKey}`,
|
|
10221
10633
|
"Content-Type": "application/json",
|
|
@@ -10230,16 +10642,20 @@ class RemoteSkillsClient {
|
|
|
10230
10642
|
if (response.status === 404 && opts.domainNotFoundCodes?.length && await responseBodyCarriesCode(response, opts.domainNotFoundCodes)) {
|
|
10231
10643
|
return response;
|
|
10232
10644
|
}
|
|
10645
|
+
response.body?.cancel().catch(() => {});
|
|
10233
10646
|
throw new RemoteRouteUnsupportedError(routePath, response.status, this.apiUrl);
|
|
10234
10647
|
}
|
|
10235
10648
|
if (!response.ok) {
|
|
10649
|
+
if (path === "/api/v1/billing/checkout" && options?.method === "POST" && response.status === 503 && await responseBodyCarriesCode(response, ["SUBSCRIPTION_CHECKOUT_UNAVAILABLE"])) {
|
|
10650
|
+
throw new RemoteCapabilityUnavailableError;
|
|
10651
|
+
}
|
|
10652
|
+
response.body?.cancel().catch(() => {});
|
|
10236
10653
|
throw new RemoteRequestError(routePath, response.status, response.statusText);
|
|
10237
10654
|
}
|
|
10238
10655
|
return response;
|
|
10239
10656
|
}
|
|
10240
10657
|
async listSkills() {
|
|
10241
|
-
|
|
10242
|
-
return res.json();
|
|
10658
|
+
return this.arrayResponse("/api/v1/skills");
|
|
10243
10659
|
}
|
|
10244
10660
|
async getSkillMd(slug) {
|
|
10245
10661
|
const res = await this.request(`/api/v1/skills/${slug}/skill.md`);
|
|
@@ -10261,39 +10677,269 @@ class RemoteSkillsClient {
|
|
|
10261
10677
|
} catch {}
|
|
10262
10678
|
return { status: res.status, body };
|
|
10263
10679
|
}
|
|
10264
|
-
async submitRun(slug, input, args) {
|
|
10265
|
-
|
|
10680
|
+
async submitRun(slug, input, args, approval = {}) {
|
|
10681
|
+
if (approval.idempotencyKey !== undefined && !/^[A-Za-z0-9._:-]{1,128}$/.test(approval.idempotencyKey))
|
|
10682
|
+
throw new Error("Idempotency key must be 1-128 URL-safe characters");
|
|
10683
|
+
if (approval.maxCostCents !== undefined)
|
|
10684
|
+
creditCount(approval.maxCostCents);
|
|
10685
|
+
if (approval.maxCredits !== undefined)
|
|
10686
|
+
creditCount(approval.maxCredits);
|
|
10687
|
+
if (approval.maxCredits !== undefined && approval.maxCostCents !== undefined && approval.maxCredits !== approval.maxCostCents)
|
|
10688
|
+
throw new Error("Credit approval fields disagree");
|
|
10689
|
+
const res = await this.request(`/api/v1/runs/${encodeURIComponent(slug)}`, {
|
|
10266
10690
|
method: "POST",
|
|
10267
|
-
body: JSON.stringify({
|
|
10691
|
+
body: JSON.stringify({
|
|
10692
|
+
input,
|
|
10693
|
+
args,
|
|
10694
|
+
...approval.maxCredits !== undefined ? { maxCredits: approval.maxCredits } : {},
|
|
10695
|
+
...approval.maxCostCents !== undefined ? { maxCostCents: approval.maxCostCents } : {},
|
|
10696
|
+
...approval.idempotencyKey !== undefined ? { idempotencyKey: approval.idempotencyKey } : {},
|
|
10697
|
+
...approval.inputFiles !== undefined ? { files: approval.inputFiles } : {}
|
|
10698
|
+
})
|
|
10268
10699
|
});
|
|
10269
10700
|
return normalizeRemoteSkillRunContract(await res.json(), slug);
|
|
10270
10701
|
}
|
|
10702
|
+
async quoteRun(slug, input = {}, args = []) {
|
|
10703
|
+
const response = await this.requestNewRoute(`/api/v1/skills/${encodeURIComponent(slug)}/quote`, {
|
|
10704
|
+
method: "POST",
|
|
10705
|
+
body: JSON.stringify({ input, args })
|
|
10706
|
+
});
|
|
10707
|
+
return parseRemoteRunQuote(await response.json());
|
|
10708
|
+
}
|
|
10709
|
+
getCapabilities() {
|
|
10710
|
+
if (!this.capabilities)
|
|
10711
|
+
this.capabilities = (async () => {
|
|
10712
|
+
const value = await (await this.requestNewRoute("/api/v1/capabilities")).json();
|
|
10713
|
+
if (value.contractVersion !== 1 || value.apiVersion !== 1 || !Array.isArray(value.capabilities) || value.capabilities.some((item) => typeof item !== "string"))
|
|
10714
|
+
throw new Error("Unsupported Skills server capability contract");
|
|
10715
|
+
const billing = value.billing;
|
|
10716
|
+
return { contractVersion: 1, apiVersion: 1, capabilities: value.capabilities, ...billing ? { billing } : {} };
|
|
10717
|
+
})();
|
|
10718
|
+
return this.capabilities;
|
|
10719
|
+
}
|
|
10720
|
+
async submitQuotedRun(slug, input = {}, args = [], approval = {}) {
|
|
10721
|
+
const maximum = creditCount(approval.maxCredits ?? approval.maxCostCents ?? 0);
|
|
10722
|
+
if (approval.maxCostCents !== undefined && approval.maxCostCents !== maximum)
|
|
10723
|
+
throw new Error("Credit approval fields disagree");
|
|
10724
|
+
const quote = await this.quoteRun(slug, input, args);
|
|
10725
|
+
if (quote.pricing.costCents > maximum)
|
|
10726
|
+
throw new RemoteCreditApprovalError(quote.pricing.costCents, maximum);
|
|
10727
|
+
const capabilities = await this.getCapabilities();
|
|
10728
|
+
if (!capabilities.capabilities.includes("runs.submit") || capabilities.billing?.boundedRunApproval !== true || capabilities.billing.unit !== "credits") {
|
|
10729
|
+
throw new Error("The configured server does not support bounded credit approval; refusing remote submission");
|
|
10730
|
+
}
|
|
10731
|
+
return this.submitRun(quote.skill, input, args, { ...approval, maxCredits: maximum, maxCostCents: maximum });
|
|
10732
|
+
}
|
|
10733
|
+
async getIdentity() {
|
|
10734
|
+
return (await this.requestNewRoute("/api/auth/whoami")).json();
|
|
10735
|
+
}
|
|
10736
|
+
async updateProfile(input) {
|
|
10737
|
+
const body = customerNamePatch(input, "displayName");
|
|
10738
|
+
return parseUpdatedProfile(await (await this.requestNewRoute("/api/v1/account/profile", { method: "PATCH", body: JSON.stringify(body) })).json());
|
|
10739
|
+
}
|
|
10740
|
+
async updateCurrentWorkspace(input) {
|
|
10741
|
+
const body = customerNamePatch(input, "name");
|
|
10742
|
+
return parseUpdatedWorkspace(await (await this.requestNewRoute("/api/v1/workspaces/current", { method: "PATCH", body: JSON.stringify(body) })).json());
|
|
10743
|
+
}
|
|
10744
|
+
async listWorkspaceMembers(options = {}) {
|
|
10745
|
+
const query = workspaceMembersQuery(options);
|
|
10746
|
+
const requestedCursor = options.cursor;
|
|
10747
|
+
const response = await this.requestNewRoute(`/api/v1/workspace/members${query}`);
|
|
10748
|
+
let value;
|
|
10749
|
+
try {
|
|
10750
|
+
value = await response.json();
|
|
10751
|
+
} catch {
|
|
10752
|
+
throw new Error("The server returned an invalid workspace roster.");
|
|
10753
|
+
}
|
|
10754
|
+
const page = parseWorkspaceMembersPage(value);
|
|
10755
|
+
if (requestedCursor !== undefined && page.nextCursor === requestedCursor)
|
|
10756
|
+
throw new Error("The server returned an invalid workspace roster.");
|
|
10757
|
+
return page;
|
|
10758
|
+
}
|
|
10759
|
+
async setWorkspaceMemberRole(membershipId, input) {
|
|
10760
|
+
const captured = workspaceMemberRoleInput(membershipId, input);
|
|
10761
|
+
const value = await this.requestWorkspaceMember(captured.membershipId, "PATCH", captured.body);
|
|
10762
|
+
return parseWorkspaceMemberRoleResult(value, captured.membershipId, captured.body.role);
|
|
10763
|
+
}
|
|
10764
|
+
async removeWorkspaceMember(membershipId, input) {
|
|
10765
|
+
const captured = workspaceMemberRemovalInput(membershipId, input);
|
|
10766
|
+
return parseWorkspaceMemberRemovalResult(await this.requestWorkspaceMember(captured.membershipId, "DELETE", captured.body), captured.membershipId);
|
|
10767
|
+
}
|
|
10768
|
+
async requestWorkspaceMember(membershipId, method, body) {
|
|
10769
|
+
const path = `/api/v1/workspace/members/${membershipId}`;
|
|
10770
|
+
const response = await this.request(path, { method, body: JSON.stringify(body) });
|
|
10771
|
+
let value;
|
|
10772
|
+
try {
|
|
10773
|
+
value = JSON.parse(new TextDecoder().decode(await readBoundedResponse(response, response.ok ? 64 * 1024 : 4096)));
|
|
10774
|
+
} catch {
|
|
10775
|
+
if (response.ok)
|
|
10776
|
+
throw new Error(invalidMemberResult);
|
|
10777
|
+
}
|
|
10778
|
+
if (!response.ok) {
|
|
10779
|
+
const code = workspaceMemberFailure(value, response.status);
|
|
10780
|
+
if (code)
|
|
10781
|
+
throw new RemoteWorkspaceMemberError(path, code);
|
|
10782
|
+
if (response.status === 404 || response.status === 405)
|
|
10783
|
+
throw new RemoteRouteUnsupportedError(path, response.status, this.apiUrl);
|
|
10784
|
+
throw new RemoteRequestError(path, response.status);
|
|
10785
|
+
}
|
|
10786
|
+
return value;
|
|
10787
|
+
}
|
|
10788
|
+
async listApiKeys() {
|
|
10789
|
+
return this.arrayResponse("/api/auth/keys");
|
|
10790
|
+
}
|
|
10791
|
+
async createApiKey(name, scopes) {
|
|
10792
|
+
if (!name.trim() || name.length > 100)
|
|
10793
|
+
throw new Error("API key name must be 1-100 characters");
|
|
10794
|
+
const value = await (await this.requestNewRoute("/api/auth/keys", { method: "POST", body: JSON.stringify({ name, ...scopes ? { scopes } : {} }) })).json();
|
|
10795
|
+
if (!value || typeof value.key !== "string" || !value.key.trim())
|
|
10796
|
+
throw new Error("The server did not return a created API key");
|
|
10797
|
+
return value;
|
|
10798
|
+
}
|
|
10799
|
+
async revokeApiKey(keyId) {
|
|
10800
|
+
return (await this.requestNewRoute(`/api/auth/keys/${encodeURIComponent(keyId)}`, { method: "DELETE" })).json();
|
|
10801
|
+
}
|
|
10802
|
+
async getBillingStatus() {
|
|
10803
|
+
return parseRemoteBillingStatus(await (await this.requestNewRoute("/api/v1/billing/status")).json());
|
|
10804
|
+
}
|
|
10805
|
+
async listCreditPacks() {
|
|
10806
|
+
return parseRemoteCreditPacks(await (await this.requestNewRoute("/api/v1/billing/credits")).json());
|
|
10807
|
+
}
|
|
10808
|
+
async createCreditCheckout(packId) {
|
|
10809
|
+
const packs = await this.listCreditPacks();
|
|
10810
|
+
if (!packs.some((pack) => pack.id === packId))
|
|
10811
|
+
throw new Error("Choose a credit pack returned by skills credits packs");
|
|
10812
|
+
return parseRemoteCheckout(await (await this.requestNewRoute("/api/v1/billing/credits", {
|
|
10813
|
+
method: "POST",
|
|
10814
|
+
body: JSON.stringify({ packId })
|
|
10815
|
+
})).json());
|
|
10816
|
+
}
|
|
10817
|
+
async getUsage() {
|
|
10818
|
+
return this.arrayResponse("/api/v1/billing/usage");
|
|
10819
|
+
}
|
|
10820
|
+
async listInvoices() {
|
|
10821
|
+
return this.arrayResponse("/api/v1/billing/invoices");
|
|
10822
|
+
}
|
|
10823
|
+
async createBillingCheckout() {
|
|
10824
|
+
return this.checkoutResponse("/api/v1/billing/checkout");
|
|
10825
|
+
}
|
|
10826
|
+
async createBillingPortal() {
|
|
10827
|
+
return this.checkoutResponse("/api/v1/billing/portal");
|
|
10828
|
+
}
|
|
10829
|
+
async cancelRun(runId) {
|
|
10830
|
+
return this.controlRun(runId, "cancel");
|
|
10831
|
+
}
|
|
10832
|
+
async resumeRun(runId) {
|
|
10833
|
+
return this.controlRun(runId, "resume");
|
|
10834
|
+
}
|
|
10835
|
+
async controlRun(runId, action) {
|
|
10836
|
+
const response = await this.requestNewRoute(`/api/v1/runs/${encodeURIComponent(runId)}/${action}`, { method: "POST", body: "{}" });
|
|
10837
|
+
return normalizeRemoteSkillRunContract(await response.json());
|
|
10838
|
+
}
|
|
10839
|
+
async checkoutResponse(path) {
|
|
10840
|
+
return parseRemoteCheckout(await (await this.requestNewRoute(path, { method: "POST", body: "{}" })).json());
|
|
10841
|
+
}
|
|
10842
|
+
async arrayResponse(path) {
|
|
10843
|
+
const rows = await (await this.requestNewRoute(path)).json();
|
|
10844
|
+
if (!Array.isArray(rows) || rows.some((row) => !row || typeof row !== "object" || Array.isArray(row)))
|
|
10845
|
+
throw new Error("Invalid Skills server list response");
|
|
10846
|
+
return rows;
|
|
10847
|
+
}
|
|
10271
10848
|
async getRun(runId) {
|
|
10272
|
-
const
|
|
10273
|
-
|
|
10849
|
+
const path = `/api/v1/runs/${encodeURIComponent(runId)}`;
|
|
10850
|
+
const res = await this.request(path);
|
|
10851
|
+
if (res.status === 404)
|
|
10274
10852
|
return null;
|
|
10853
|
+
if (!res.ok)
|
|
10854
|
+
throw new RemoteRequestError(path, res.status, res.statusText);
|
|
10275
10855
|
return normalizeRemoteSkillRunContract(await res.json());
|
|
10276
10856
|
}
|
|
10277
10857
|
async getRunLogs(runId) {
|
|
10278
|
-
|
|
10279
|
-
if (!res.ok)
|
|
10280
|
-
return [];
|
|
10281
|
-
const payload = await res.json();
|
|
10282
|
-
return Array.isArray(payload) ? payload : [];
|
|
10858
|
+
return this.arrayResponse(`/api/v1/runs/${encodeURIComponent(runId)}/logs`);
|
|
10283
10859
|
}
|
|
10284
10860
|
async listRuns(limit = 20) {
|
|
10285
|
-
|
|
10286
|
-
|
|
10861
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
10862
|
+
throw new Error("Run limit must be an integer from 1 to 100");
|
|
10863
|
+
return this.arrayResponse(`/api/v1/runs?limit=${limit}`);
|
|
10287
10864
|
}
|
|
10288
10865
|
async getRunArtifacts(runId) {
|
|
10289
|
-
|
|
10290
|
-
return res.json();
|
|
10866
|
+
return this.arrayResponse(`/api/v1/runs/${encodeURIComponent(runId)}/artifacts`);
|
|
10291
10867
|
}
|
|
10292
10868
|
async downloadRunArtifact(runId, artifactId) {
|
|
10293
|
-
return this.request(`/api/v1/runs/${runId}/artifacts/${artifactId}/download`, {
|
|
10869
|
+
return this.request(`/api/v1/runs/${encodeURIComponent(runId)}/artifacts/${encodeURIComponent(artifactId)}/download`, {
|
|
10294
10870
|
method: "GET"
|
|
10295
10871
|
});
|
|
10296
10872
|
}
|
|
10873
|
+
async getVerifiedRunArtifact(runId, artifactId, maximumBytes = MAX_REMOTE_FILE_BYTES) {
|
|
10874
|
+
const artifacts = await this.getRunArtifacts(runId);
|
|
10875
|
+
const artifact = artifacts.find((row) => row.id === artifactId);
|
|
10876
|
+
if (!artifact)
|
|
10877
|
+
throw new Error("Run artifact not found");
|
|
10878
|
+
if (!Number.isSafeInteger(artifact.byteSize) || artifact.byteSize < 0 || artifact.byteSize > maximumBytes || typeof artifact.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(artifact.sha256))
|
|
10879
|
+
throw new Error("The server does not provide valid artifact integrity metadata");
|
|
10880
|
+
const response = await this.downloadRunArtifact(runId, artifactId);
|
|
10881
|
+
if (!response.ok)
|
|
10882
|
+
throw new RemoteRequestError("artifact download", response.status, response.statusText);
|
|
10883
|
+
const bytes = await readBoundedResponse(response, artifact.byteSize);
|
|
10884
|
+
if (bytes.byteLength !== artifact.byteSize || sha256(bytes) !== artifact.sha256)
|
|
10885
|
+
throw new Error("Artifact integrity verification failed");
|
|
10886
|
+
return { id: artifactId, fileName: String(artifact.fileName ?? artifactId), bytes, byteSize: bytes.byteLength, sha256: artifact.sha256 };
|
|
10887
|
+
}
|
|
10888
|
+
async submitQuotedRunWithFiles(slug, input, args, files, approval = {}) {
|
|
10889
|
+
const inputFiles = describeRemoteFiles(files);
|
|
10890
|
+
if (files.length && !(await this.getCapabilities()).capabilities.includes("runs.uploads"))
|
|
10891
|
+
throw new Error("The configured server does not support input uploads");
|
|
10892
|
+
const run = await this.submitQuotedRun(slug, input, args, { ...approval, inputFiles });
|
|
10893
|
+
if (run.error || !run.id || !files.length)
|
|
10894
|
+
return run;
|
|
10895
|
+
const pastUploads = (status) => typeof status === "string" && [
|
|
10896
|
+
"running",
|
|
10897
|
+
"completed",
|
|
10898
|
+
"failed",
|
|
10899
|
+
"cancelled",
|
|
10900
|
+
"expired",
|
|
10901
|
+
"pending_approval",
|
|
10902
|
+
"approved",
|
|
10903
|
+
"waiting"
|
|
10904
|
+
].includes(status);
|
|
10905
|
+
if (pastUploads(run.status))
|
|
10906
|
+
return run;
|
|
10907
|
+
try {
|
|
10908
|
+
await this.uploadRunFiles(run.id, files);
|
|
10909
|
+
} catch {
|
|
10910
|
+
try {
|
|
10911
|
+
const current = await this.getRun(run.id);
|
|
10912
|
+
if (current && pastUploads(current.status))
|
|
10913
|
+
return current;
|
|
10914
|
+
} catch {}
|
|
10915
|
+
let cancellationRequested = false;
|
|
10916
|
+
try {
|
|
10917
|
+
await this.cancelRun(run.id);
|
|
10918
|
+
cancellationRequested = true;
|
|
10919
|
+
} catch {}
|
|
10920
|
+
throw new Error(`Input upload failed for run ${run.id}; ${cancellationRequested ? "cancellation requested" : "check its status and cancel the run"}`);
|
|
10921
|
+
}
|
|
10922
|
+
return run;
|
|
10923
|
+
}
|
|
10924
|
+
async uploadRunFiles(runId, files) {
|
|
10925
|
+
const descriptors = describeRemoteFiles(files);
|
|
10926
|
+
const response = await this.requestNewRoute(`/api/v1/runs/${encodeURIComponent(runId)}/uploads`, { method: "POST", body: JSON.stringify({ files: descriptors }) });
|
|
10927
|
+
const payload = await response.json();
|
|
10928
|
+
if (!Array.isArray(payload.files) || payload.files.length !== files.length || new Set(payload.files.map((file) => file.name)).size !== files.length)
|
|
10929
|
+
throw new Error("Invalid input upload response");
|
|
10930
|
+
for (const file of files) {
|
|
10931
|
+
const upload = payload.files.find((row) => row.name === file.name);
|
|
10932
|
+
if (!upload)
|
|
10933
|
+
throw new Error("Missing input upload URL");
|
|
10934
|
+
const url = new URL(upload.uploadUrl);
|
|
10935
|
+
if (url.username || url.password || url.hash || url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)))
|
|
10936
|
+
throw new Error("Unsafe input upload URL");
|
|
10937
|
+
const uploaded = await fetch(url, { method: "PUT", body: file.bytes, headers: { "Content-Type": file.contentType ?? "application/octet-stream" }, redirect: "error", signal: AbortSignal.timeout(60000) });
|
|
10938
|
+
if (!uploaded.ok)
|
|
10939
|
+
throw new Error("Input upload failed");
|
|
10940
|
+
await uploaded.body?.cancel();
|
|
10941
|
+
}
|
|
10942
|
+
}
|
|
10297
10943
|
async publishSkill(manifest, bundle, ifMatch) {
|
|
10298
10944
|
const form = new FormData;
|
|
10299
10945
|
form.set("manifest", JSON.stringify(manifest));
|
|
@@ -10306,7 +10952,9 @@ class RemoteSkillsClient {
|
|
|
10306
10952
|
return fetch(`${this.apiUrl}/api/v1/skills`, {
|
|
10307
10953
|
method: "POST",
|
|
10308
10954
|
headers,
|
|
10309
|
-
body: form
|
|
10955
|
+
body: form,
|
|
10956
|
+
redirect: "error",
|
|
10957
|
+
signal: AbortSignal.timeout(15000)
|
|
10310
10958
|
});
|
|
10311
10959
|
}
|
|
10312
10960
|
async deleteSkill(slug) {
|
|
@@ -10328,8 +10976,10 @@ class RemoteSkillsClient {
|
|
|
10328
10976
|
return [];
|
|
10329
10977
|
if (!response.ok)
|
|
10330
10978
|
throw new Error(`versions request failed: ${response.status}`);
|
|
10331
|
-
const body = await response
|
|
10332
|
-
|
|
10979
|
+
const body = await readSkillVersionPayload(response);
|
|
10980
|
+
if (!isVersionRecord(body) || !Array.isArray(body.versions) || body.slug !== undefined && body.slug !== slug)
|
|
10981
|
+
throw new Error(INVALID_SKILL_VERSION_RESPONSE);
|
|
10982
|
+
return body.versions.map((entry) => normalizeSkillVersion(entry, slug));
|
|
10333
10983
|
}
|
|
10334
10984
|
async getSkillVersion(slug, version) {
|
|
10335
10985
|
const response = await this.requestNewRoute(`/api/v1/skills/${encodeURIComponent(slug)}/versions/${encodeURIComponent(version)}`, undefined, { domainNotFoundCodes: ["SKILL_NOT_FOUND", "SKILL_VERSION_NOT_FOUND"] });
|
|
@@ -10337,7 +10987,7 @@ class RemoteSkillsClient {
|
|
|
10337
10987
|
return null;
|
|
10338
10988
|
if (!response.ok)
|
|
10339
10989
|
throw new Error(`version request failed: ${response.status}`);
|
|
10340
|
-
return await response
|
|
10990
|
+
return normalizeSkillVersion(await readSkillVersionPayload(response), slug, version);
|
|
10341
10991
|
}
|
|
10342
10992
|
async listPins() {
|
|
10343
10993
|
const response = await this.requestNewRoute("/api/v1/pins");
|
|
@@ -10362,12 +11012,13 @@ class RemoteSkillsClient {
|
|
|
10362
11012
|
if (!Array.isArray(payload)) {
|
|
10363
11013
|
throw new Error("Remote tags payload did not match the expected contract (expected an array of tag names)");
|
|
10364
11014
|
}
|
|
10365
|
-
|
|
10366
|
-
|
|
10367
|
-
|
|
10368
|
-
|
|
11015
|
+
const isName = (value) => typeof value === "string" && value.trim().length > 0;
|
|
11016
|
+
if (payload.every(isName))
|
|
11017
|
+
return payload;
|
|
11018
|
+
if (payload.every((tag) => tag !== null && typeof tag === "object" && !Array.isArray(tag) && isName(tag.name) && Number.isSafeInteger(tag.count) && tag.count >= 0)) {
|
|
11019
|
+
return payload.map((tag) => tag.name);
|
|
10369
11020
|
}
|
|
10370
|
-
|
|
11021
|
+
throw new Error("Remote tags payload did not match the expected contract (every element must be a non-empty tag name, or every element must be a counted tag record)");
|
|
10371
11022
|
}
|
|
10372
11023
|
async skillsByTag(tag) {
|
|
10373
11024
|
const path = `/api/v1/tags/${encodeURIComponent(tag)}/skills`;
|
|
@@ -10384,31 +11035,48 @@ class RemoteSkillsClient {
|
|
|
10384
11035
|
return normalizeUpdatedSincePage(await response.json());
|
|
10385
11036
|
}
|
|
10386
11037
|
}
|
|
10387
|
-
function requireOptionalString(
|
|
10388
|
-
if (
|
|
11038
|
+
function requireOptionalString(record2, field) {
|
|
11039
|
+
if (record2[field] === undefined)
|
|
10389
11040
|
return;
|
|
10390
|
-
if (typeof
|
|
11041
|
+
if (typeof record2[field] !== "string") {
|
|
10391
11042
|
throw new Error(`Remote payload did not match the expected contract (${field} must be a string when present)`);
|
|
10392
11043
|
}
|
|
10393
|
-
return
|
|
11044
|
+
return record2[field];
|
|
11045
|
+
}
|
|
11046
|
+
var INVALID_SKILL_VERSION_RESPONSE = "Remote skill version payload did not match the expected contract.";
|
|
11047
|
+
function isVersionRecord(value) {
|
|
11048
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
11049
|
+
}
|
|
11050
|
+
async function readSkillVersionPayload(response) {
|
|
11051
|
+
try {
|
|
11052
|
+
return await response.json();
|
|
11053
|
+
} catch {
|
|
11054
|
+
throw new Error(INVALID_SKILL_VERSION_RESPONSE);
|
|
11055
|
+
}
|
|
11056
|
+
}
|
|
11057
|
+
function normalizeSkillVersion(entry, slug, version) {
|
|
11058
|
+
if (!isVersionRecord(entry) || typeof entry.slug !== "string" || !entry.slug.trim() || entry.slug !== slug || typeof entry.version !== "string" || !entry.version.trim() || version !== undefined && entry.version !== version || typeof entry.bundleSha256 !== "string" || !/^[a-f0-9]{64}$/i.test(entry.bundleSha256) || typeof entry.bundleByteSize !== "number" || !Number.isSafeInteger(entry.bundleByteSize) || entry.bundleByteSize < 0 || typeof entry.createdAt !== "string" || !entry.createdAt.trim() || entry.current !== undefined && typeof entry.current !== "boolean" || entry.storageKind !== undefined && typeof entry.storageKind !== "string" || entry.manifest !== undefined && !isVersionRecord(entry.manifest)) {
|
|
11059
|
+
throw new Error(INVALID_SKILL_VERSION_RESPONSE);
|
|
11060
|
+
}
|
|
11061
|
+
return entry;
|
|
10394
11062
|
}
|
|
10395
11063
|
function normalizePin(entry) {
|
|
10396
11064
|
if (!entry || typeof entry !== "object") {
|
|
10397
11065
|
throw new Error("Remote pin payload did not match the expected contract (expected an object)");
|
|
10398
11066
|
}
|
|
10399
|
-
const
|
|
10400
|
-
const slug = typeof
|
|
11067
|
+
const record2 = entry;
|
|
11068
|
+
const slug = typeof record2.slug === "string" && record2.slug.trim() ? record2.slug.trim() : undefined;
|
|
10401
11069
|
if (!slug) {
|
|
10402
11070
|
throw new Error("Remote pin payload did not match the expected contract (missing slug)");
|
|
10403
11071
|
}
|
|
10404
11072
|
let metadata;
|
|
10405
|
-
if (
|
|
10406
|
-
if (!
|
|
11073
|
+
if (record2.metadata !== undefined) {
|
|
11074
|
+
if (!record2.metadata || typeof record2.metadata !== "object" || Array.isArray(record2.metadata)) {
|
|
10407
11075
|
throw new Error("Remote pin payload did not match the expected contract (metadata must be a JSON object when present)");
|
|
10408
11076
|
}
|
|
10409
|
-
metadata =
|
|
11077
|
+
metadata = record2.metadata;
|
|
10410
11078
|
}
|
|
10411
|
-
const pinnedAt = requireOptionalString(
|
|
11079
|
+
const pinnedAt = requireOptionalString(record2, "pinnedAt");
|
|
10412
11080
|
return {
|
|
10413
11081
|
slug,
|
|
10414
11082
|
...pinnedAt !== undefined ? { pinnedAt } : {},
|
|
@@ -10425,16 +11093,16 @@ function normalizeSkillSummary(entry) {
|
|
|
10425
11093
|
if (!entry || typeof entry !== "object") {
|
|
10426
11094
|
throw new Error("Remote skill payload did not match the expected contract (expected an object)");
|
|
10427
11095
|
}
|
|
10428
|
-
const
|
|
10429
|
-
const slug = typeof
|
|
11096
|
+
const record2 = entry;
|
|
11097
|
+
const slug = typeof record2.slug === "string" && record2.slug.trim() ? record2.slug.trim() : undefined;
|
|
10430
11098
|
if (!slug) {
|
|
10431
11099
|
throw new Error("Remote skill payload did not match the expected contract (missing slug)");
|
|
10432
11100
|
}
|
|
10433
11101
|
return {
|
|
10434
11102
|
slug,
|
|
10435
|
-
...requireOptionalString(
|
|
10436
|
-
...requireOptionalString(
|
|
10437
|
-
...requireOptionalString(
|
|
11103
|
+
...requireOptionalString(record2, "name") !== undefined ? { name: requireOptionalString(record2, "name") } : {},
|
|
11104
|
+
...requireOptionalString(record2, "version") !== undefined ? { version: requireOptionalString(record2, "version") } : {},
|
|
11105
|
+
...requireOptionalString(record2, "updatedAt") !== undefined ? { updatedAt: requireOptionalString(record2, "updatedAt") } : {}
|
|
10438
11106
|
};
|
|
10439
11107
|
}
|
|
10440
11108
|
function normalizeSkillSummaryList(payload) {
|
|
@@ -10444,40 +11112,63 @@ function normalizeSkillSummaryList(payload) {
|
|
|
10444
11112
|
return payload.map(normalizeSkillSummary);
|
|
10445
11113
|
}
|
|
10446
11114
|
async function responseBodyCarriesCode(response, codes) {
|
|
11115
|
+
const reader = response.body?.getReader();
|
|
11116
|
+
if (!reader)
|
|
11117
|
+
return false;
|
|
11118
|
+
const maximum = 8 * 1024;
|
|
11119
|
+
let deadline;
|
|
11120
|
+
const expired = new Promise((_, reject) => {
|
|
11121
|
+
deadline = setTimeout(() => reject(new Error("Error response read deadline exceeded")), 1000);
|
|
11122
|
+
});
|
|
10447
11123
|
try {
|
|
10448
|
-
const
|
|
10449
|
-
|
|
11124
|
+
const chunks = [];
|
|
11125
|
+
let size = 0;
|
|
11126
|
+
while (true) {
|
|
11127
|
+
const next = await Promise.race([reader.read(), expired]);
|
|
11128
|
+
if (next.done)
|
|
11129
|
+
break;
|
|
11130
|
+
size += next.value.byteLength;
|
|
11131
|
+
if (size > maximum)
|
|
11132
|
+
return false;
|
|
11133
|
+
chunks.push(next.value);
|
|
11134
|
+
}
|
|
11135
|
+
const bytes = new Uint8Array(size);
|
|
11136
|
+
let offset = 0;
|
|
11137
|
+
for (const chunk of chunks) {
|
|
11138
|
+
bytes.set(chunk, offset);
|
|
11139
|
+
offset += chunk.byteLength;
|
|
11140
|
+
}
|
|
11141
|
+
const payload = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
11142
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload) || !Object.hasOwn(payload, "code"))
|
|
10450
11143
|
return false;
|
|
10451
11144
|
const code = payload.code;
|
|
10452
11145
|
return typeof code === "string" && codes.includes(code);
|
|
10453
11146
|
} catch {
|
|
10454
11147
|
return false;
|
|
11148
|
+
} finally {
|
|
11149
|
+
clearTimeout(deadline);
|
|
11150
|
+
reader.cancel().catch(() => {});
|
|
11151
|
+
reader.releaseLock();
|
|
10455
11152
|
}
|
|
10456
11153
|
}
|
|
10457
11154
|
function normalizeUpdatedSincePage(payload) {
|
|
10458
11155
|
if (!payload || typeof payload !== "object") {
|
|
10459
11156
|
throw new Error("Updated-since payload did not match the expected contract (expected an object)");
|
|
10460
11157
|
}
|
|
10461
|
-
const
|
|
10462
|
-
if (!Array.isArray(
|
|
11158
|
+
const record2 = payload;
|
|
11159
|
+
if (!Array.isArray(record2.skills)) {
|
|
10463
11160
|
throw new Error("Updated-since payload did not match the expected contract (missing skills array)");
|
|
10464
11161
|
}
|
|
10465
|
-
const skills =
|
|
10466
|
-
const nextCursor =
|
|
11162
|
+
const skills = record2.skills.map(normalizeSkillSummary);
|
|
11163
|
+
const nextCursor = record2.nextCursor === undefined || record2.nextCursor === null ? null : record2.nextCursor;
|
|
10467
11164
|
if (nextCursor !== null && typeof nextCursor !== "string") {
|
|
10468
11165
|
throw new Error("Updated-since payload did not match the expected contract (nextCursor must be a string or absent)");
|
|
10469
11166
|
}
|
|
10470
11167
|
return { skills, nextCursor };
|
|
10471
11168
|
}
|
|
10472
11169
|
async function createRemoteSkillsClient(env = process.env) {
|
|
10473
|
-
const
|
|
10474
|
-
|
|
10475
|
-
return null;
|
|
10476
|
-
const apiKey = await resolveSkillsApiKey(env);
|
|
10477
|
-
if (!apiKey) {
|
|
10478
|
-
throw new Error("A Skills authority resolved but no API key did. Sign in with: skills auth login");
|
|
10479
|
-
}
|
|
10480
|
-
return new RemoteSkillsClient(apiKey, fleet.apiOrigin);
|
|
11170
|
+
const connection = await resolveSkillsConnection(env);
|
|
11171
|
+
return connection ? new RemoteSkillsClient(connection.apiKey, connection.apiOrigin) : null;
|
|
10481
11172
|
}
|
|
10482
11173
|
// src/lib/scheduler.ts
|
|
10483
11174
|
import { existsSync as existsSync14, readFileSync as readFileSync13, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7 } from "fs";
|
|
@@ -10688,7 +11379,7 @@ import { existsSync as existsSync15, mkdirSync as mkdirSync8, mkdtempSync as mkd
|
|
|
10688
11379
|
import { dirname as dirname6, join as join17 } from "path";
|
|
10689
11380
|
|
|
10690
11381
|
// src/lib/revision.ts
|
|
10691
|
-
import { createHash as
|
|
11382
|
+
import { createHash as createHash4 } from "crypto";
|
|
10692
11383
|
var REVISION_ID_PATTERN = /^[0-9a-f]{64}$/;
|
|
10693
11384
|
function revisionIdOf(content) {
|
|
10694
11385
|
const canonical = JSON.stringify({
|
|
@@ -10704,14 +11395,14 @@ function revisionIdOf(content) {
|
|
|
10704
11395
|
bundleSha256: content.bundleSha256 ?? null,
|
|
10705
11396
|
bundleByteSize: content.bundleByteSize ?? null
|
|
10706
11397
|
});
|
|
10707
|
-
return
|
|
11398
|
+
return createHash4("sha256").update(canonical).digest("hex");
|
|
10708
11399
|
}
|
|
10709
|
-
function revisionIdOfRecord(
|
|
10710
|
-
return revisionIdOf(
|
|
11400
|
+
function revisionIdOfRecord(record2) {
|
|
11401
|
+
return revisionIdOf(record2);
|
|
10711
11402
|
}
|
|
10712
11403
|
|
|
10713
11404
|
// src/lib/skill-bundle.ts
|
|
10714
|
-
import { createHash as
|
|
11405
|
+
import { createHash as createHash5 } from "crypto";
|
|
10715
11406
|
import { readFileSync as readFileSync14, readdirSync as readdirSync9, statSync as statSync9 } from "fs";
|
|
10716
11407
|
import { join as join16, relative as relative3 } from "path";
|
|
10717
11408
|
var BLOCK = 512;
|
|
@@ -10865,7 +11556,7 @@ function ownBytes(view) {
|
|
|
10865
11556
|
return out;
|
|
10866
11557
|
}
|
|
10867
11558
|
function sha256Hex(bytes) {
|
|
10868
|
-
return
|
|
11559
|
+
return createHash5("sha256").update(bytes).digest("hex");
|
|
10869
11560
|
}
|
|
10870
11561
|
function collectSkillBundleEntries(dir) {
|
|
10871
11562
|
const entries = [];
|
|
@@ -11233,7 +11924,7 @@ function provenRevision(meta, slug, bundle) {
|
|
|
11233
11924
|
}
|
|
11234
11925
|
function reconcileTombstone(slug, corpusOptions) {
|
|
11235
11926
|
const target = join17(getPortableSkillsRoot(corpusOptions), slug);
|
|
11236
|
-
if (!
|
|
11927
|
+
if (!hasSkillsOwnershipMarker(target)) {
|
|
11237
11928
|
return { name: slug, success: true, tombstoned: true, removed: false, leftInPlace: true };
|
|
11238
11929
|
}
|
|
11239
11930
|
rmSync4(target, { recursive: true, force: true });
|
|
@@ -11414,16 +12105,16 @@ function installBundleAtomically(name, entries, options = {}, marker = {}) {
|
|
|
11414
12105
|
}
|
|
11415
12106
|
return { path: target, created };
|
|
11416
12107
|
}
|
|
11417
|
-
function writePullMarker(dir,
|
|
12108
|
+
function writePullMarker(dir, record2) {
|
|
11418
12109
|
const marker = {
|
|
11419
12110
|
managedBy: "@hasna/skills",
|
|
11420
|
-
skill:
|
|
11421
|
-
source:
|
|
11422
|
-
...
|
|
11423
|
-
...
|
|
11424
|
-
...
|
|
11425
|
-
...
|
|
11426
|
-
...
|
|
12111
|
+
skill: record2.skill,
|
|
12112
|
+
source: record2.source ?? "pull",
|
|
12113
|
+
...record2.version ? { version: record2.version } : {},
|
|
12114
|
+
...record2.contentHash ? { contentHash: record2.contentHash } : {},
|
|
12115
|
+
...record2.sourceCommit ? { sourceCommit: record2.sourceCommit } : {},
|
|
12116
|
+
...record2.signature ? { signature: record2.signature } : {},
|
|
12117
|
+
...record2.revisionId ? { revisionId: record2.revisionId } : {},
|
|
11427
12118
|
syncedAt: new Date().toISOString()
|
|
11428
12119
|
};
|
|
11429
12120
|
writeFileSync8(join17(dir, PULL_MARKER_FILE), `${JSON.stringify(marker, null, 2)}
|
|
@@ -11438,19 +12129,19 @@ async function safeMeta(client, slug) {
|
|
|
11438
12129
|
}
|
|
11439
12130
|
if (!raw || typeof raw !== "object")
|
|
11440
12131
|
return null;
|
|
11441
|
-
const
|
|
11442
|
-
const kind =
|
|
11443
|
-
const tags = Array.isArray(
|
|
12132
|
+
const record2 = raw;
|
|
12133
|
+
const kind = record2.kind === "instruction" || record2.kind === "executable" ? record2.kind : undefined;
|
|
12134
|
+
const tags = Array.isArray(record2.tags) ? record2.tags.filter((tag) => typeof tag === "string" && tag.trim().length > 0) : undefined;
|
|
11444
12135
|
return {
|
|
11445
|
-
...str(
|
|
11446
|
-
...str(
|
|
11447
|
-
...str(
|
|
12136
|
+
...str(record2.displayName) ? { displayName: str(record2.displayName) } : {},
|
|
12137
|
+
...str(record2.description) ? { description: str(record2.description) } : {},
|
|
12138
|
+
...str(record2.category) ? { category: str(record2.category) } : {},
|
|
11448
12139
|
...tags && tags.length ? { tags } : {},
|
|
11449
|
-
...str(
|
|
12140
|
+
...str(record2.version) ? { version: str(record2.version) } : {},
|
|
11450
12141
|
...kind ? { kind } : {},
|
|
11451
|
-
...REVISION_ID_PATTERN.test(str(
|
|
11452
|
-
...typeof
|
|
11453
|
-
...str(
|
|
12142
|
+
...REVISION_ID_PATTERN.test(str(record2.revisionId) ?? "") ? { revisionId: str(record2.revisionId) } : {},
|
|
12143
|
+
...typeof record2.skillMd === "string" && record2.skillMd.length > 0 ? { skillMd: record2.skillMd } : {},
|
|
12144
|
+
...str(record2.publishedSource) ? { publishedSource: str(record2.publishedSource) } : {}
|
|
11454
12145
|
};
|
|
11455
12146
|
}
|
|
11456
12147
|
function pickCorpusOptions(options) {
|
|
@@ -11459,8 +12150,8 @@ function pickCorpusOptions(options) {
|
|
|
11459
12150
|
function extractSlug(entry) {
|
|
11460
12151
|
if (!entry || typeof entry !== "object")
|
|
11461
12152
|
return;
|
|
11462
|
-
const
|
|
11463
|
-
return str(
|
|
12153
|
+
const record2 = entry;
|
|
12154
|
+
return str(record2.slug) ?? str(record2.name);
|
|
11464
12155
|
}
|
|
11465
12156
|
function dedupe(values) {
|
|
11466
12157
|
return [...new Set(values)];
|
|
@@ -11586,7 +12277,7 @@ import { dirname as dirname7, relative as relative4 } from "path";
|
|
|
11586
12277
|
// package.json
|
|
11587
12278
|
var package_default = {
|
|
11588
12279
|
name: "@hasna/skills",
|
|
11589
|
-
version: "0.
|
|
12280
|
+
version: "0.5.0",
|
|
11590
12281
|
description: "Skills library for AI coding agents",
|
|
11591
12282
|
type: "module",
|
|
11592
12283
|
bin: {
|
|
@@ -11618,6 +12309,7 @@ var package_default = {
|
|
|
11618
12309
|
files: [
|
|
11619
12310
|
"dist/",
|
|
11620
12311
|
"!dist/**/*.test.d.ts",
|
|
12312
|
+
"!dist/**/*.fixture.d.ts",
|
|
11621
12313
|
"!dist/test-preload.d.ts",
|
|
11622
12314
|
"!dist/platform",
|
|
11623
12315
|
"bin/",
|
|
@@ -11643,10 +12335,10 @@ var package_default = {
|
|
|
11643
12335
|
migrate: "bun run ./src/server/migrate.ts",
|
|
11644
12336
|
typecheck: "tsc --noEmit",
|
|
11645
12337
|
"verify:release": "bun run scripts/release-guard.ts",
|
|
12338
|
+
"verify:consumer-types": "bun run scripts/consumer-types.ts",
|
|
11646
12339
|
prepare: "bun run build:js",
|
|
11647
|
-
prepack: "bun run build && bun run verify:release",
|
|
11648
|
-
prepublishOnly: "bun run typecheck && bun run test"
|
|
11649
|
-
postinstall: "mkdir -p $HOME/.hasna/skills/custom 2>/dev/null || true"
|
|
12340
|
+
prepack: "bun run build && bun run verify:release && bun run verify:consumer-types",
|
|
12341
|
+
prepublishOnly: "bun run typecheck && bun run test"
|
|
11650
12342
|
},
|
|
11651
12343
|
keywords: [
|
|
11652
12344
|
"skills",
|
|
@@ -11667,6 +12359,7 @@ var package_default = {
|
|
|
11667
12359
|
author: "Hasna",
|
|
11668
12360
|
license: "Apache-2.0",
|
|
11669
12361
|
devDependencies: {
|
|
12362
|
+
"@hasna/contracts": "1.0.2",
|
|
11670
12363
|
"@types/bun": "1.3.14",
|
|
11671
12364
|
"@types/node": "25.2.3",
|
|
11672
12365
|
"@types/react": "^18.2.0",
|
|
@@ -11678,8 +12371,7 @@ var package_default = {
|
|
|
11678
12371
|
dependencies: {
|
|
11679
12372
|
"@aws-sdk/client-ecs": "^3.1079.0",
|
|
11680
12373
|
"@aws-sdk/client-s3": "^3.1079.0",
|
|
11681
|
-
"@hasna/
|
|
11682
|
-
"@hasna/events": "0.1.16",
|
|
12374
|
+
"@hasna/events": "0.1.18",
|
|
11683
12375
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
11684
12376
|
chalk: "^5.3.0",
|
|
11685
12377
|
commander: "^12.1.0",
|
|
@@ -11775,6 +12467,24 @@ function buildDocs(name) {
|
|
|
11775
12467
|
best: docs.skillMd ?? docs.readme ?? docs.claudeMd ?? null
|
|
11776
12468
|
};
|
|
11777
12469
|
}
|
|
12470
|
+
// src/lib/remote-customer-operations.ts
|
|
12471
|
+
var REMOTE_CUSTOMER_OPERATIONS = [
|
|
12472
|
+
{ name: "get_account", title: "Get Account Identity", parameter: null, mutates: false, invoke: (client) => client.getIdentity() },
|
|
12473
|
+
{ name: "get_server_capabilities", title: "Get Server Capabilities", parameter: null, mutates: false, invoke: (client) => client.getCapabilities() },
|
|
12474
|
+
{ name: "list_remote_skills", title: "List Remote Skills", parameter: null, mutates: false, invoke: (client) => client.listSkills() },
|
|
12475
|
+
{ name: "get_billing_status", title: "Get Billing Status", parameter: null, mutates: false, invoke: (client) => client.getBillingStatus() },
|
|
12476
|
+
{ name: "list_credit_packs", title: "List Credit Packs", parameter: null, mutates: false, invoke: (client) => client.listCreditPacks() },
|
|
12477
|
+
{ name: "create_credit_checkout", title: "Create Credit Checkout", parameter: "pack_id", mutates: true, invoke: (client, value) => client.createCreditCheckout(value) },
|
|
12478
|
+
{ name: "get_billing_usage", title: "Get Billing Usage", parameter: null, mutates: false, invoke: (client) => client.getUsage() },
|
|
12479
|
+
{ name: "list_invoices", title: "List Invoices", parameter: null, mutates: false, invoke: (client) => client.listInvoices() },
|
|
12480
|
+
{ name: "create_billing_checkout", title: "Create Billing Checkout", parameter: null, mutates: true, invoke: (client) => client.createBillingCheckout() },
|
|
12481
|
+
{ name: "create_billing_portal", title: "Create Billing Portal", parameter: null, mutates: true, invoke: (client) => client.createBillingPortal() },
|
|
12482
|
+
{ name: "get_run_logs", title: "Get Run Logs", parameter: "run_id", mutates: false, invoke: (client, value) => client.getRunLogs(value) },
|
|
12483
|
+
{ name: "cancel_run", title: "Cancel Run", parameter: "run_id", mutates: true, invoke: (client, value) => client.cancelRun(value) },
|
|
12484
|
+
{ name: "resume_run", title: "Resume Run", parameter: "run_id", mutates: true, invoke: (client, value) => client.resumeRun(value) },
|
|
12485
|
+
{ name: "list_run_artifacts", title: "List Run Artifacts", parameter: "run_id", mutates: false, invoke: (client, value) => client.getRunArtifacts(value) }
|
|
12486
|
+
];
|
|
12487
|
+
|
|
11778
12488
|
// src/lib/mcp-contracts.ts
|
|
11779
12489
|
var MCP_CONTRACT_SCHEMA_VERSION = 1;
|
|
11780
12490
|
var stringSchema = (description) => ({
|
|
@@ -12166,11 +12876,142 @@ var toolContracts = [
|
|
|
12166
12876
|
dependencies: objectSchema({}, [], "Package dependencies.", true)
|
|
12167
12877
|
})
|
|
12168
12878
|
},
|
|
12879
|
+
{
|
|
12880
|
+
name: "update_account_profile",
|
|
12881
|
+
title: "Update Account Display Name",
|
|
12882
|
+
description: "Update your display name with fresh email verification on the configured server.",
|
|
12883
|
+
params: ["name", "email", "code"],
|
|
12884
|
+
category: "execution",
|
|
12885
|
+
sideEffects: "local-process-or-remote-run",
|
|
12886
|
+
stable: true,
|
|
12887
|
+
inputSchema: objectSchema({ name: stringSchema("Display name, 1\u2013100 characters"), email: { type: "string", format: "email" }, code: { type: "string", pattern: "^\\d{6}$" } }, ["name", "email", "code"]),
|
|
12888
|
+
outputSchema: objectSchema({ user: objectSchema({ id: stringSchema("Account identifier."), email: stringSchema("Account email."), displayName: stringSchema("Display name."), role: stringSchema("Current workspace role.") }, ["id", "email", "displayName", "role"]) }, ["user"])
|
|
12889
|
+
},
|
|
12890
|
+
{
|
|
12891
|
+
name: "update_workspace_name",
|
|
12892
|
+
title: "Update Workspace Name",
|
|
12893
|
+
description: "Update the current workspace name as an owner/admin with fresh email verification.",
|
|
12894
|
+
params: ["name", "email", "code"],
|
|
12895
|
+
category: "execution",
|
|
12896
|
+
sideEffects: "local-process-or-remote-run",
|
|
12897
|
+
stable: true,
|
|
12898
|
+
inputSchema: objectSchema({ name: stringSchema("Workspace name, 1\u2013100 characters"), email: { type: "string", format: "email" }, code: { type: "string", pattern: "^\\d{6}$" } }, ["name", "email", "code"]),
|
|
12899
|
+
outputSchema: objectSchema({ organization: objectSchema({ id: stringSchema("Workspace identifier."), slug: stringSchema("Stable workspace slug."), name: stringSchema("Workspace name.") }, ["id", "slug", "name"]) }, ["organization"])
|
|
12900
|
+
},
|
|
12901
|
+
{
|
|
12902
|
+
name: "set_workspace_member_role",
|
|
12903
|
+
title: "Set Current Workspace Member Role",
|
|
12904
|
+
description: "Set an exact membership incarnation's role with its observed expectedRole and fresh verification; no automatic retry.",
|
|
12905
|
+
params: ["membershipId", "role", "expectedRole", "email", "code"],
|
|
12906
|
+
category: "execution",
|
|
12907
|
+
sideEffects: "local-process-or-remote-run",
|
|
12908
|
+
stable: true,
|
|
12909
|
+
inputSchema: objectSchema({
|
|
12910
|
+
membershipId: { type: "string", pattern: "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$" },
|
|
12911
|
+
role: { type: "string", enum: ["owner", "admin", "member", "viewer"] },
|
|
12912
|
+
expectedRole: { type: "string", enum: ["owner", "admin", "member", "viewer"] },
|
|
12913
|
+
email: { type: "string", format: "email" },
|
|
12914
|
+
code: { type: "string", pattern: "^\\d{6}$" }
|
|
12915
|
+
}, ["membershipId", "role", "expectedRole", "email", "code"]),
|
|
12916
|
+
outputSchema: objectSchema({
|
|
12917
|
+
organizationId: stringSchema("Current workspace identifier."),
|
|
12918
|
+
changed: { type: "boolean" },
|
|
12919
|
+
member: objectSchema({
|
|
12920
|
+
membershipId: stringSchema("Membership incarnation."),
|
|
12921
|
+
userId: stringSchema("Account identifier."),
|
|
12922
|
+
email: stringSchema("Member email."),
|
|
12923
|
+
displayName: { oneOf: [{ type: "string" }, { type: "null" }] },
|
|
12924
|
+
role: { type: "string", enum: ["owner", "admin", "member", "viewer"] },
|
|
12925
|
+
createdAt: stringSchema("Exact server timestamp including microseconds.")
|
|
12926
|
+
}, ["membershipId", "userId", "email", "displayName", "role", "createdAt"])
|
|
12927
|
+
}, ["organizationId", "member", "changed"])
|
|
12928
|
+
},
|
|
12929
|
+
{
|
|
12930
|
+
name: "remove_workspace_member",
|
|
12931
|
+
title: "Remove Current Workspace Member",
|
|
12932
|
+
description: "Remove exactly this membership incarnation using its observed expectedRole and fresh verification; self-removal is unavailable.",
|
|
12933
|
+
params: ["membershipId", "expectedRole", "email", "code"],
|
|
12934
|
+
category: "execution",
|
|
12935
|
+
sideEffects: "local-process-or-remote-run",
|
|
12936
|
+
stable: true,
|
|
12937
|
+
inputSchema: objectSchema({
|
|
12938
|
+
membershipId: { type: "string", pattern: "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$" },
|
|
12939
|
+
expectedRole: { type: "string", enum: ["owner", "admin", "member", "viewer"] },
|
|
12940
|
+
email: { type: "string", format: "email" },
|
|
12941
|
+
code: { type: "string", pattern: "^\\d{6}$" }
|
|
12942
|
+
}, ["membershipId", "expectedRole", "email", "code"]),
|
|
12943
|
+
outputSchema: objectSchema({
|
|
12944
|
+
organizationId: stringSchema("Current workspace identifier."),
|
|
12945
|
+
membershipId: stringSchema("Removed membership incarnation."),
|
|
12946
|
+
removed: { type: "boolean", const: true },
|
|
12947
|
+
alreadyRemoved: { type: "boolean" }
|
|
12948
|
+
}, ["organizationId", "membershipId", "removed", "alreadyRemoved"])
|
|
12949
|
+
},
|
|
12950
|
+
{
|
|
12951
|
+
name: "list_workspace_members",
|
|
12952
|
+
title: "List Current Workspace Members",
|
|
12953
|
+
description: "Read one current-workspace roster page with fresh owner/admin email verification; saved credentials stay unchanged.",
|
|
12954
|
+
params: ["email", "code", "limit?", "cursor?"],
|
|
12955
|
+
category: "execution",
|
|
12956
|
+
sideEffects: "local-process-or-remote-run",
|
|
12957
|
+
stable: true,
|
|
12958
|
+
inputSchema: objectSchema({
|
|
12959
|
+
email: { type: "string", format: "email" },
|
|
12960
|
+
code: { type: "string", pattern: "^\\d{6}$" },
|
|
12961
|
+
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
12962
|
+
cursor: { type: "string", pattern: "^[A-Za-z0-9_-]{1,512}$" }
|
|
12963
|
+
}, ["email", "code"]),
|
|
12964
|
+
outputSchema: objectSchema({
|
|
12965
|
+
organizationId: stringSchema("Current workspace identifier."),
|
|
12966
|
+
members: arraySchema(objectSchema({
|
|
12967
|
+
membershipId: stringSchema("Membership incarnation."),
|
|
12968
|
+
userId: stringSchema("Account identifier."),
|
|
12969
|
+
email: stringSchema("Member email."),
|
|
12970
|
+
displayName: { oneOf: [{ type: "string" }, { type: "null" }] },
|
|
12971
|
+
role: { type: "string", enum: ["owner", "admin", "member", "viewer"] },
|
|
12972
|
+
createdAt: stringSchema("Exact server timestamp including microseconds.")
|
|
12973
|
+
}, ["membershipId", "userId", "email", "displayName", "role", "createdAt"])),
|
|
12974
|
+
nextCursor: { oneOf: [{ type: "string" }, { type: "null" }] }
|
|
12975
|
+
}, ["organizationId", "members", "nextCursor"])
|
|
12976
|
+
},
|
|
12977
|
+
{
|
|
12978
|
+
name: "list_api_keys",
|
|
12979
|
+
title: "List API Keys",
|
|
12980
|
+
description: "List keys using fresh email OTP reauthentication.",
|
|
12981
|
+
params: ["email", "code"],
|
|
12982
|
+
category: "execution",
|
|
12983
|
+
sideEffects: "local-process-or-remote-run",
|
|
12984
|
+
stable: true,
|
|
12985
|
+
inputSchema: objectSchema({ email: { type: "string", format: "email" }, code: { type: "string", pattern: "^\\d{6}$" } }, ["email", "code"]),
|
|
12986
|
+
outputSchema: arraySchema(objectSchema({}, [], "API key metadata", true))
|
|
12987
|
+
},
|
|
12988
|
+
{
|
|
12989
|
+
name: "revoke_api_key",
|
|
12990
|
+
title: "Revoke API Key",
|
|
12991
|
+
description: "Revoke a key using fresh email OTP reauthentication.",
|
|
12992
|
+
params: ["key_id", "email", "code"],
|
|
12993
|
+
category: "execution",
|
|
12994
|
+
sideEffects: "local-process-or-remote-run",
|
|
12995
|
+
stable: true,
|
|
12996
|
+
inputSchema: objectSchema({ key_id: stringSchema("API key ID"), email: { type: "string", format: "email" }, code: { type: "string", pattern: "^\\d{6}$" } }, ["key_id", "email", "code"]),
|
|
12997
|
+
outputSchema: objectSchema({}, [], "Revocation result", true)
|
|
12998
|
+
},
|
|
12999
|
+
{
|
|
13000
|
+
name: "create_api_key",
|
|
13001
|
+
title: "Create API Key",
|
|
13002
|
+
description: "Create a key with fresh email OTP reauthentication; returns the secret once.",
|
|
13003
|
+
params: ["name", "email", "code", "scopes?"],
|
|
13004
|
+
category: "execution",
|
|
13005
|
+
sideEffects: "local-process-or-remote-run",
|
|
13006
|
+
stable: true,
|
|
13007
|
+
inputSchema: objectSchema({ name: stringSchema("Key name"), email: { type: "string", format: "email" }, code: { type: "string", pattern: "^\\d{6}$" }, scopes: arraySchema(stringSchema("Scope")) }, ["name", "email", "code"]),
|
|
13008
|
+
outputSchema: objectSchema({}, [], "Created key and one-time secret", true)
|
|
13009
|
+
},
|
|
12169
13010
|
{
|
|
12170
13011
|
name: "run_skill",
|
|
12171
13012
|
title: "Run Skill",
|
|
12172
13013
|
description: "Run a skill locally or through a configured remote runner. Returns compact stdout/stderr previews and run summaries by default; pass detail:true for full records.",
|
|
12173
|
-
params: ["name", "input?", "args?", "detail?"],
|
|
13014
|
+
params: ["name", "input?", "args?", "detail?", "remote?", "maxCredits?", "maxCostCents?", "idempotency_key?", "files?"],
|
|
12174
13015
|
category: "execution",
|
|
12175
13016
|
sideEffects: "local-process-or-remote-run",
|
|
12176
13017
|
stable: true,
|
|
@@ -12178,7 +13019,12 @@ var toolContracts = [
|
|
|
12178
13019
|
name: skillNameInput,
|
|
12179
13020
|
input: runInputSchema,
|
|
12180
13021
|
args: runArgsSchema,
|
|
12181
|
-
detail: { type: "boolean", default: false, description: "Return full stdout/stderr, remote run, and local run metadata." }
|
|
13022
|
+
detail: { type: "boolean", default: false, description: "Return full stdout/stderr, remote run, and local run metadata." },
|
|
13023
|
+
remote: { type: "boolean", description: "Use the configured server catalog." },
|
|
13024
|
+
maxCredits: { type: "integer", minimum: 0, description: "Maximum explicitly approved integer credits; omitted permits only free remote runs." },
|
|
13025
|
+
maxCostCents: { type: "integer", minimum: 0, description: "Legacy alias for maxCredits; both must agree." },
|
|
13026
|
+
idempotency_key: { type: "string", pattern: "^[A-Za-z0-9._:-]{1,128}$", description: "Stable retry key for the same remote submission." },
|
|
13027
|
+
files: { type: "array", maxItems: 10, items: objectSchema({ name: stringSchema("Safe basename"), base64: { type: "string", maxLength: 1398104 }, contentType: stringSchema("MIME type") }, ["name", "base64"]), description: "Inline remote inputs, at most 1 MiB combined." }
|
|
12182
13028
|
}, ["name"]),
|
|
12183
13029
|
outputSchema: runOutputSchema
|
|
12184
13030
|
},
|
|
@@ -12445,7 +13291,40 @@ var toolContracts = [
|
|
|
12445
13291
|
outputSchema: objectSchema({}, [], "Feedback save result.", true)
|
|
12446
13292
|
}
|
|
12447
13293
|
];
|
|
12448
|
-
var
|
|
13294
|
+
var remoteCustomerContracts = REMOTE_CUSTOMER_OPERATIONS.map((operation) => ({
|
|
13295
|
+
name: operation.name,
|
|
13296
|
+
title: operation.title,
|
|
13297
|
+
description: `${operation.title} on the configured server; unavailable capabilities fail explicitly.`,
|
|
13298
|
+
params: operation.parameter ? [operation.parameter] : [],
|
|
13299
|
+
category: "execution",
|
|
13300
|
+
sideEffects: operation.mutates ? "local-process-or-remote-run" : "none",
|
|
13301
|
+
stable: true,
|
|
13302
|
+
inputSchema: objectSchema(operation.parameter ? { [operation.parameter]: stringSchema("Server resource identifier.") } : {}, operation.parameter ? [operation.parameter] : []),
|
|
13303
|
+
outputSchema: { oneOf: [objectSchema({}, [], "Server response.", true), { type: "array", items: objectSchema({}, [], "Server record.", true) }] }
|
|
13304
|
+
}));
|
|
13305
|
+
remoteCustomerContracts.push({
|
|
13306
|
+
name: "quote_skill",
|
|
13307
|
+
title: "Quote Remote Skill",
|
|
13308
|
+
description: "Get a server credit quote without submitting a run.",
|
|
13309
|
+
params: ["name", "input?", "args?"],
|
|
13310
|
+
category: "execution",
|
|
13311
|
+
sideEffects: "none",
|
|
13312
|
+
stable: true,
|
|
13313
|
+
inputSchema: objectSchema({ name: skillNameInput, input: runInputSchema, args: runArgsSchema }, ["name"]),
|
|
13314
|
+
outputSchema: objectSchema({ skill: stringSchema("Canonical server skill."), pricing: objectSchema({}, [], "Quoted integer credits.", true) }, ["skill", "pricing"], undefined, true)
|
|
13315
|
+
});
|
|
13316
|
+
remoteCustomerContracts.push({
|
|
13317
|
+
name: "download_run_artifact",
|
|
13318
|
+
title: "Download Verified Run Artifact",
|
|
13319
|
+
description: "Return verified artifact bytes as base64, bounded to 1 MiB.",
|
|
13320
|
+
params: ["run_id", "artifact_id"],
|
|
13321
|
+
category: "execution",
|
|
13322
|
+
sideEffects: "none",
|
|
13323
|
+
stable: true,
|
|
13324
|
+
inputSchema: objectSchema({ run_id: stringSchema("Run identifier."), artifact_id: stringSchema("Artifact identifier.") }, ["run_id", "artifact_id"]),
|
|
13325
|
+
outputSchema: objectSchema({ id: stringSchema("Artifact identifier."), fileName: stringSchema("Artifact file name."), base64: stringSchema("Verified bytes."), sha256: stringSchema("SHA256 digest."), byteSize: { type: "integer", minimum: 0 } }, ["id", "fileName", "base64", "sha256", "byteSize"])
|
|
13326
|
+
});
|
|
13327
|
+
var contracts = [...toolContracts, ...remoteCustomerContracts].sort((a, b) => a.name.localeCompare(b.name));
|
|
12449
13328
|
var resourceContracts = [
|
|
12450
13329
|
{
|
|
12451
13330
|
uri: "skills://mcp/contracts",
|
|
@@ -12664,7 +13543,7 @@ function isApiMode(env = process.env) {
|
|
|
12664
13543
|
}
|
|
12665
13544
|
}
|
|
12666
13545
|
// src/lib/native-storage.ts
|
|
12667
|
-
import { createHash as
|
|
13546
|
+
import { createHash as createHash6, createHmac as createHmac2 } from "crypto";
|
|
12668
13547
|
import {
|
|
12669
13548
|
existsSync as existsSync17,
|
|
12670
13549
|
mkdirSync as mkdirSync11,
|
|
@@ -12840,7 +13719,7 @@ function exportSkillsLocalSnapshot(targetDir = process.cwd(), options = {}) {
|
|
|
12840
13719
|
files.push({
|
|
12841
13720
|
path: relativePath,
|
|
12842
13721
|
sizeBytes: bytes.byteLength,
|
|
12843
|
-
sha256:
|
|
13722
|
+
sha256: createHash6("sha256").update(bytes).digest("hex"),
|
|
12844
13723
|
...options.includeFileContents ? { contentBase64: Buffer.from(bytes).toString("base64") } : {}
|
|
12845
13724
|
});
|
|
12846
13725
|
}
|
|
@@ -12865,7 +13744,7 @@ function importSkillsLocalSnapshot(snapshot, targetDir = process.cwd(), options
|
|
|
12865
13744
|
continue;
|
|
12866
13745
|
}
|
|
12867
13746
|
const bytes = Buffer.from(file.contentBase64, "base64");
|
|
12868
|
-
const hash =
|
|
13747
|
+
const hash = createHash6("sha256").update(bytes).digest("hex");
|
|
12869
13748
|
if (hash !== file.sha256) {
|
|
12870
13749
|
throw new Error(`Snapshot file checksum mismatch: ${file.path}`);
|
|
12871
13750
|
}
|
|
@@ -12909,7 +13788,7 @@ class SkillsPostgresSyncStore {
|
|
|
12909
13788
|
}
|
|
12910
13789
|
async upsertRecords(records) {
|
|
12911
13790
|
let count = 0;
|
|
12912
|
-
for (const
|
|
13791
|
+
for (const record2 of records) {
|
|
12913
13792
|
await this.client.query([
|
|
12914
13793
|
"INSERT INTO skills_sync_records",
|
|
12915
13794
|
"(scope, kind, id, updated_at, deleted_at, source, payload)",
|
|
@@ -12920,13 +13799,13 @@ class SkillsPostgresSyncStore {
|
|
|
12920
13799
|
"source = EXCLUDED.source,",
|
|
12921
13800
|
"payload = EXCLUDED.payload"
|
|
12922
13801
|
].join(" "), [
|
|
12923
|
-
|
|
12924
|
-
|
|
12925
|
-
|
|
12926
|
-
|
|
12927
|
-
|
|
12928
|
-
|
|
12929
|
-
JSON.stringify(
|
|
13802
|
+
record2.scope,
|
|
13803
|
+
record2.kind,
|
|
13804
|
+
record2.id,
|
|
13805
|
+
record2.updatedAt,
|
|
13806
|
+
record2.deletedAt ?? null,
|
|
13807
|
+
record2.source ?? null,
|
|
13808
|
+
JSON.stringify(record2.payload)
|
|
12930
13809
|
]);
|
|
12931
13810
|
count += 1;
|
|
12932
13811
|
}
|
|
@@ -13197,7 +14076,7 @@ function toIsoString(value) {
|
|
|
13197
14076
|
return Number.isNaN(date.getTime()) ? value : date.toISOString();
|
|
13198
14077
|
}
|
|
13199
14078
|
function sha256Hex2(value) {
|
|
13200
|
-
return
|
|
14079
|
+
return createHash6("sha256").update(value).digest("hex");
|
|
13201
14080
|
}
|
|
13202
14081
|
function normalizeHeaders(headers) {
|
|
13203
14082
|
const result = {};
|
|
@@ -13245,7 +14124,7 @@ function toArrayBuffer(bytes) {
|
|
|
13245
14124
|
return buffer;
|
|
13246
14125
|
}
|
|
13247
14126
|
// src/lib/station-snapshot.ts
|
|
13248
|
-
import { createHash as
|
|
14127
|
+
import { createHash as createHash7 } from "crypto";
|
|
13249
14128
|
import {
|
|
13250
14129
|
copyFileSync as copyFileSync2,
|
|
13251
14130
|
mkdirSync as mkdirSync12,
|
|
@@ -13420,7 +14299,7 @@ function validateStationId(stationId) {
|
|
|
13420
14299
|
}
|
|
13421
14300
|
}
|
|
13422
14301
|
function sha256File(filePath) {
|
|
13423
|
-
return
|
|
14302
|
+
return createHash7("sha256").update(readFileSync17(filePath)).digest("hex");
|
|
13424
14303
|
}
|
|
13425
14304
|
function scanHome(definition, homesRoot) {
|
|
13426
14305
|
const homePath = homePathFor(definition, homesRoot);
|
|
@@ -13545,7 +14424,7 @@ function writeStationSnapshot(options) {
|
|
|
13545
14424
|
copyFileSync2(plan.source.fullPath, destination);
|
|
13546
14425
|
written += 1;
|
|
13547
14426
|
}
|
|
13548
|
-
const
|
|
14427
|
+
const unchanged2 = plans.length - untouched.length;
|
|
13549
14428
|
const manifest = {
|
|
13550
14429
|
schema: STATION_SYNC_MANIFEST_SCHEMA,
|
|
13551
14430
|
stationId: options.stationId,
|
|
@@ -13553,7 +14432,7 @@ function writeStationSnapshot(options) {
|
|
|
13553
14432
|
producer: STATION_SNAPSHOT_PRODUCER,
|
|
13554
14433
|
stats: {
|
|
13555
14434
|
written,
|
|
13556
|
-
unchanged,
|
|
14435
|
+
unchanged: unchanged2,
|
|
13557
14436
|
files: plans.length,
|
|
13558
14437
|
bytes: totalBytes
|
|
13559
14438
|
},
|
|
@@ -13566,12 +14445,12 @@ function writeStationSnapshot(options) {
|
|
|
13566
14445
|
return {
|
|
13567
14446
|
...base,
|
|
13568
14447
|
mode: "populate",
|
|
13569
|
-
stats: { files: plans.length, bytes: totalBytes, written, unchanged },
|
|
14448
|
+
stats: { files: plans.length, bytes: totalBytes, written, unchanged: unchanged2 },
|
|
13570
14449
|
manifestPath
|
|
13571
14450
|
};
|
|
13572
14451
|
}
|
|
13573
14452
|
// src/lib/station-hydrate.ts
|
|
13574
|
-
import { createHash as
|
|
14453
|
+
import { createHash as createHash8 } from "crypto";
|
|
13575
14454
|
import {
|
|
13576
14455
|
copyFileSync as copyFileSync3,
|
|
13577
14456
|
mkdirSync as mkdirSync13,
|
|
@@ -13777,7 +14656,7 @@ function skillSha256(skill) {
|
|
|
13777
14656
|
return sha256File(skill.files[0].winner.fullPath);
|
|
13778
14657
|
}
|
|
13779
14658
|
const joined = skill.files.map((file) => sha256File(file.winner.fullPath));
|
|
13780
|
-
return
|
|
14659
|
+
return createHash8("sha256").update(joined.sort().join(`
|
|
13781
14660
|
`)).digest("hex");
|
|
13782
14661
|
}
|
|
13783
14662
|
function writeStationHydration(options) {
|
|
@@ -13840,7 +14719,7 @@ function writeStationHydration(options) {
|
|
|
13840
14719
|
copyFileSync3(entry.fullPath, entry.destination);
|
|
13841
14720
|
written += 1;
|
|
13842
14721
|
}
|
|
13843
|
-
const
|
|
14722
|
+
const unchanged2 = plan.totalFiles - written;
|
|
13844
14723
|
const hydration = {
|
|
13845
14724
|
schema: STATION_HYDRATION_MANIFEST_SCHEMA,
|
|
13846
14725
|
stationId: options.stationId,
|
|
@@ -13851,7 +14730,7 @@ function writeStationHydration(options) {
|
|
|
13851
14730
|
stats: {
|
|
13852
14731
|
idents: plan.winners.length,
|
|
13853
14732
|
written,
|
|
13854
|
-
unchanged,
|
|
14733
|
+
unchanged: unchanged2,
|
|
13855
14734
|
files: plan.totalFiles,
|
|
13856
14735
|
bytes: plan.totalBytes
|
|
13857
14736
|
},
|
|
@@ -13864,10 +14743,143 @@ function writeStationHydration(options) {
|
|
|
13864
14743
|
return {
|
|
13865
14744
|
...base,
|
|
13866
14745
|
mode: "apply",
|
|
13867
|
-
stats: { ...base.stats, written, unchanged },
|
|
14746
|
+
stats: { ...base.stats, written, unchanged: unchanged2 },
|
|
13868
14747
|
manifestPath: hydrationManifestPath
|
|
13869
14748
|
};
|
|
13870
14749
|
}
|
|
14750
|
+
// src/lib/remote-auth.ts
|
|
14751
|
+
var MAX_ERROR_DETAIL_LENGTH = 200;
|
|
14752
|
+
|
|
14753
|
+
class HostedApiError extends Error {
|
|
14754
|
+
status;
|
|
14755
|
+
code;
|
|
14756
|
+
detail;
|
|
14757
|
+
endpoint;
|
|
14758
|
+
apiUrl;
|
|
14759
|
+
constructor(message, options = {}) {
|
|
14760
|
+
super(message);
|
|
14761
|
+
this.name = "HostedApiError";
|
|
14762
|
+
this.status = options.status;
|
|
14763
|
+
this.code = options.code;
|
|
14764
|
+
this.detail = options.detail;
|
|
14765
|
+
this.endpoint = options.endpoint;
|
|
14766
|
+
this.apiUrl = options.apiUrl;
|
|
14767
|
+
}
|
|
14768
|
+
}
|
|
14769
|
+
async function requestAuthApi(instance, path, options) {
|
|
14770
|
+
const url = normalizeSkillsApiOrigin(instance);
|
|
14771
|
+
const safeUrl = url;
|
|
14772
|
+
const endpoint = `${(options?.method || "GET").toUpperCase()} ${safeUrl}${path}`;
|
|
14773
|
+
let res;
|
|
14774
|
+
try {
|
|
14775
|
+
res = await fetch(`${url}${path}`, {
|
|
14776
|
+
...options,
|
|
14777
|
+
redirect: "error",
|
|
14778
|
+
signal: options?.signal ?? AbortSignal.timeout(15000),
|
|
14779
|
+
headers: { "Content-Type": "application/json", ...options?.headers }
|
|
14780
|
+
});
|
|
14781
|
+
} catch (err) {
|
|
14782
|
+
throw new HostedApiError(`Unable to reach the Skills API: ${err.message}`, {
|
|
14783
|
+
endpoint,
|
|
14784
|
+
apiUrl: safeUrl
|
|
14785
|
+
});
|
|
14786
|
+
}
|
|
14787
|
+
const text = await res.text();
|
|
14788
|
+
const body = text ? parseJsonBody(text) : {};
|
|
14789
|
+
if (!res.ok) {
|
|
14790
|
+
const record2 = isRecord5(body) ? body : {};
|
|
14791
|
+
const detail = typeof record2.detail === "string" ? record2.detail : undefined;
|
|
14792
|
+
const error = typeof record2.error === "string" ? record2.error : undefined;
|
|
14793
|
+
const code = typeof record2.code === "string" ? record2.code : undefined;
|
|
14794
|
+
throw new HostedApiError(detail || error || `${res.status} ${res.statusText}`, {
|
|
14795
|
+
status: res.status,
|
|
14796
|
+
code,
|
|
14797
|
+
detail,
|
|
14798
|
+
endpoint,
|
|
14799
|
+
apiUrl: safeUrl
|
|
14800
|
+
});
|
|
14801
|
+
}
|
|
14802
|
+
return body;
|
|
14803
|
+
}
|
|
14804
|
+
function parseJsonBody(text) {
|
|
14805
|
+
try {
|
|
14806
|
+
return JSON.parse(text);
|
|
14807
|
+
} catch {
|
|
14808
|
+
return { detail: condenseErrorBody(text) };
|
|
14809
|
+
}
|
|
14810
|
+
}
|
|
14811
|
+
function condenseErrorBody(text) {
|
|
14812
|
+
const stripped = /<[a-z!/]/i.test(text) ? text.replace(/<(script|style)[\s\S]*?<\/\1>/gi, " ").replace(/<[^>]*>/g, " ") : text;
|
|
14813
|
+
const collapsed = stripped.replace(/\s+/g, " ").trim();
|
|
14814
|
+
if (collapsed.length <= MAX_ERROR_DETAIL_LENGTH)
|
|
14815
|
+
return collapsed;
|
|
14816
|
+
return `${collapsed.slice(0, MAX_ERROR_DETAIL_LENGTH - 1).trimEnd()}\u2026`;
|
|
14817
|
+
}
|
|
14818
|
+
function isRecord5(value) {
|
|
14819
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
14820
|
+
}
|
|
14821
|
+
|
|
14822
|
+
class RemoteSkillsAuthClient {
|
|
14823
|
+
apiOrigin;
|
|
14824
|
+
constructor(apiUrl) {
|
|
14825
|
+
this.apiOrigin = normalizeSkillsApiOrigin(apiUrl);
|
|
14826
|
+
}
|
|
14827
|
+
requestCode(email) {
|
|
14828
|
+
return this.request("/api/auth/login", { method: "POST", body: JSON.stringify({ email }) });
|
|
14829
|
+
}
|
|
14830
|
+
verifyCode(email, code) {
|
|
14831
|
+
return this.request("/api/auth/verify", { method: "POST", body: JSON.stringify({ email, code }) });
|
|
14832
|
+
}
|
|
14833
|
+
startDevice() {
|
|
14834
|
+
return this.request("/api/auth/device/start", { method: "POST", body: JSON.stringify({ client: "skills-sdk" }) });
|
|
14835
|
+
}
|
|
14836
|
+
pollDevice(deviceCode) {
|
|
14837
|
+
return this.request("/api/auth/device/token", { method: "POST", body: JSON.stringify({ deviceCode }) });
|
|
14838
|
+
}
|
|
14839
|
+
async sessionClient(email, code) {
|
|
14840
|
+
const apiOrigin = this.apiOrigin;
|
|
14841
|
+
if (!email.includes("@") || !/^\d{6}$/.test(code))
|
|
14842
|
+
throw new Error("Fresh email and six-digit verification code are required to manage this account");
|
|
14843
|
+
const login = await this.verifyCode(email, code);
|
|
14844
|
+
if (!login || typeof login.token !== "string" || !login.token)
|
|
14845
|
+
throw new Error("The server did not return an authorized account session");
|
|
14846
|
+
return new RemoteSkillsClient(login.token, apiOrigin);
|
|
14847
|
+
}
|
|
14848
|
+
async createApiKey(email, code, name, scopes) {
|
|
14849
|
+
return (await this.sessionClient(email, code)).createApiKey(name, scopes);
|
|
14850
|
+
}
|
|
14851
|
+
async listApiKeys(email, code) {
|
|
14852
|
+
return (await this.sessionClient(email, code)).listApiKeys();
|
|
14853
|
+
}
|
|
14854
|
+
async revokeApiKey(email, code, keyId) {
|
|
14855
|
+
return (await this.sessionClient(email, code)).revokeApiKey(keyId);
|
|
14856
|
+
}
|
|
14857
|
+
async updateProfile(email, code, input) {
|
|
14858
|
+
customerNamePatch(input, "displayName");
|
|
14859
|
+
return (await this.sessionClient(email, code)).updateProfile(input);
|
|
14860
|
+
}
|
|
14861
|
+
async updateCurrentWorkspace(email, code, input) {
|
|
14862
|
+
customerNamePatch(input, "name");
|
|
14863
|
+
return (await this.sessionClient(email, code)).updateCurrentWorkspace(input);
|
|
14864
|
+
}
|
|
14865
|
+
async listWorkspaceMembers(email, code, options = {}) {
|
|
14866
|
+
workspaceMembersQuery(options);
|
|
14867
|
+
return (await this.sessionClient(email, code)).listWorkspaceMembers(options);
|
|
14868
|
+
}
|
|
14869
|
+
async setWorkspaceMemberRole(email, code, membershipId, input) {
|
|
14870
|
+
const captured = workspaceMemberRoleInput(membershipId, input);
|
|
14871
|
+
return (await this.sessionClient(email, code)).setWorkspaceMemberRole(captured.membershipId, captured.body);
|
|
14872
|
+
}
|
|
14873
|
+
async removeWorkspaceMember(email, code, membershipId, input) {
|
|
14874
|
+
const captured = workspaceMemberRemovalInput(membershipId, input);
|
|
14875
|
+
return (await this.sessionClient(email, code)).removeWorkspaceMember(captured.membershipId, captured.body);
|
|
14876
|
+
}
|
|
14877
|
+
request(path, options) {
|
|
14878
|
+
if (!["/api/auth/login", "/api/auth/verify", "/api/auth/device/start", "/api/auth/device/token", "/api/auth/keys", "/api/auth/whoami"].includes(path))
|
|
14879
|
+
throw new Error("Unsupported authentication operation");
|
|
14880
|
+
return requestAuthApi(this.apiOrigin, path, options);
|
|
14881
|
+
}
|
|
14882
|
+
}
|
|
13871
14883
|
export {
|
|
13872
14884
|
writeStationSnapshot,
|
|
13873
14885
|
writeStationHydration,
|
|
@@ -13904,6 +14916,7 @@ export {
|
|
|
13904
14916
|
sha256File,
|
|
13905
14917
|
setSkillDisabled,
|
|
13906
14918
|
setScheduleEnabled,
|
|
14919
|
+
selectsSkillsLocalMode,
|
|
13907
14920
|
searchSkills,
|
|
13908
14921
|
scaffoldPortableSkill,
|
|
13909
14922
|
saveProjectConfig,
|
|
@@ -13964,6 +14977,7 @@ export {
|
|
|
13964
14977
|
listPinnedSkills,
|
|
13965
14978
|
listMcpToolContracts,
|
|
13966
14979
|
isSyncAgent,
|
|
14980
|
+
isSkillsLocalOptIn,
|
|
13967
14981
|
isRegularFile,
|
|
13968
14982
|
isPortableWithinSkill,
|
|
13969
14983
|
isGatewayBackedSkill,
|
|
@@ -14071,6 +15085,7 @@ export {
|
|
|
14071
15085
|
SKILLS_PROJECT_DIR,
|
|
14072
15086
|
SKILLS_NATIVE_STORAGE_FALLBACK_ENV,
|
|
14073
15087
|
SKILLS_NATIVE_STORAGE_ENV,
|
|
15088
|
+
SKILLS_LOCAL_OPT_IN_ENV_KEYS,
|
|
14074
15089
|
SKILLS_CLI_MCP_PARITY,
|
|
14075
15090
|
SKILLS_APP,
|
|
14076
15091
|
SKILLS_API_URL_ENV_KEYS,
|
|
@@ -14078,9 +15093,13 @@ export {
|
|
|
14078
15093
|
SKILLS_API_KEY_ENV_KEYS,
|
|
14079
15094
|
SKILLS_API_KEY_ENV,
|
|
14080
15095
|
SKILLS,
|
|
15096
|
+
RemoteWorkspaceMemberError,
|
|
14081
15097
|
RemoteSkillsClient,
|
|
15098
|
+
RemoteSkillsAuthClient,
|
|
14082
15099
|
RemoteRouteUnsupportedError,
|
|
14083
15100
|
RemoteRequestError,
|
|
15101
|
+
RemoteCreditApprovalError,
|
|
15102
|
+
RemoteCapabilityUnavailableError,
|
|
14084
15103
|
REMOTE_SKILL_RUN_CONTRACT_VERSION,
|
|
14085
15104
|
REFUSED_SCANNER_FLAGGED,
|
|
14086
15105
|
PullSkillError,
|
|
@@ -14090,6 +15109,7 @@ export {
|
|
|
14090
15109
|
PORTABLE_SKILL_DEFAULT_VERSION,
|
|
14091
15110
|
MissingSkillsFleetError,
|
|
14092
15111
|
MCP_CONTRACT_SCHEMA_VERSION,
|
|
15112
|
+
HostedApiError,
|
|
14093
15113
|
DEFAULT_EXPORT_DIR,
|
|
14094
15114
|
CATEGORIES,
|
|
14095
15115
|
BASIC_SKILL_NAMES,
|