@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.
Files changed (55) hide show
  1. package/README.md +269 -14
  2. package/bin/index.js +7835 -5440
  3. package/bin/mcp.js +2040 -589
  4. package/bin/migrate.js +148 -40
  5. package/bin/server.js +66 -87
  6. package/bin/worker.js +42 -75
  7. package/dist/admin-contract.d.ts +37 -19
  8. package/dist/admin-contract.js +1 -1
  9. package/dist/cli/cli.test-utils.d.ts +10 -8
  10. package/dist/cli/commands/customer-profile.d.ts +2 -0
  11. package/dist/cli/commands/customer-verification.d.ts +5 -0
  12. package/dist/cli/commands/remote-account.d.ts +7 -0
  13. package/dist/cli/commands/tool-primitives.d.ts +1 -1
  14. package/dist/cli/commands/workspace-member-mutations.d.ts +2 -0
  15. package/dist/cli/commands/workspace-members.d.ts +2 -0
  16. package/dist/cli/env-assignment.d.ts +9 -0
  17. package/dist/index.d.ts +7 -2
  18. package/dist/index.js +1369 -349
  19. package/dist/lib/agent-sync.d.ts +13 -8
  20. package/dist/lib/api-url.d.ts +4 -3
  21. package/dist/lib/app-home.d.ts +0 -1
  22. package/dist/lib/auth-store.d.ts +1 -1
  23. package/dist/lib/client-types.d.ts +75 -0
  24. package/dist/lib/credential-state.d.ts +12 -0
  25. package/dist/lib/fleet-credentials.d.ts +49 -17
  26. package/dist/lib/home-adoption.d.ts +2 -0
  27. package/dist/lib/home-census.d.ts +3 -1
  28. package/dist/lib/instance-credentials.d.ts +13 -0
  29. package/dist/lib/local-opt-in.d.ts +24 -0
  30. package/dist/lib/mcp-contracts.d.ts +4 -0
  31. package/dist/lib/portable-skills-files.d.ts +10 -2
  32. package/dist/lib/portable-skills-types.d.ts +2 -0
  33. package/dist/lib/read-access.d.ts +83 -0
  34. package/dist/lib/remote-account.d.ts +42 -0
  35. package/dist/lib/remote-auth.d.ts +46 -0
  36. package/dist/lib/remote-client.d.ts +90 -6
  37. package/dist/lib/remote-customer-operations.d.ts +106 -0
  38. package/dist/lib/remote-files.d.ts +21 -0
  39. package/dist/lib/remote-profile.d.ts +26 -0
  40. package/dist/lib/remote-registry.d.ts +7 -3
  41. package/dist/lib/remote-workspace.d.ts +76 -0
  42. package/dist/lib/run-routing.d.ts +1 -0
  43. package/dist/lib/run-state.d.ts +3 -0
  44. package/dist/lib/skillinfo.d.ts +1 -1
  45. package/dist/mcp/helpers.d.ts +22 -0
  46. package/dist/mcp/index.d.ts +16 -0
  47. package/dist/mcp/remote-customer-tools.d.ts +2 -0
  48. package/dist/sdk/governance-store.d.ts +1 -0
  49. package/dist/sdk/index.d.ts +8 -1
  50. package/dist/sdk/index.js +1994 -416
  51. package/dist/sdk/outputs.d.ts +0 -11
  52. package/dist/sdk/runs.d.ts +5 -5
  53. package/dist/storage.js +6 -40
  54. package/docs/skill-standard.md +30 -2
  55. 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 PATHS_RESOLVER_KIND_ENV = {
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 pathsResolverAssertKind(kind) {
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[PATHS_RESOLVER_KIND_ENV[kind]];
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
- if (platform === "darwin") {
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
- return pathsResolverResolve("data", options);
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.replace(/^["']|["']$/g, "");
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 = normalizePortableSkillName(stringField(jsonManifest, "name") ?? frontmatter?.name ?? stringValue(pkg?.name) ?? fallbackName);
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 = normalizePortableSkillName(name);
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 = readPortableSkillManifest(absoluteSource, basename2(absoluteSource));
2591
+ const inferred = readPortableSkillManifestForImport(absoluteSource);
2565
2592
  const explicitName = options.name != null;
2566
- const skillName = normalizePortableSkillName(options.name ?? inferred.name);
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 = existsSync9(markerPath);
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 (!existsSync9(join9(dir, SYNC_MARKER_FILE)))
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 (!existsSync11(join11(dir, SYNC_MARKER_FILE)))
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
- class ClientTransportConfigurationError extends Error {
9087
- appName;
9088
- sources;
9089
- constructor(appName, message, sources = []) {
9090
- super(message);
9091
- this.name = "ClientTransportConfigurationError";
9092
- this.appName = appName;
9093
- this.sources = Object.freeze([...sources]);
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 resolveClientTransportSnapshot(name, env = process.env, options = {}) {
9097
- env = snapshotClientEnvironment(name, env);
9098
- const keys = clientTransportEnvKeys(name);
9099
- const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
9100
- const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
9101
- if (blankUrl) {
9102
- throw new ClientTransportConfigurationError(name, `${blankUrl.key} is set but blank; public clients require an explicit HTTPS API URL and never select local storage.`, [blankUrl.key]);
9103
- }
9104
- const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN.test(entry.raw));
9105
- if (controlledUrl) {
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
- const credential = resolveCredential(name, env, options.credentials);
9133
- if (!credential) {
9134
- const diskHint = credentialDiskSourcesForMessage(name, env);
9135
- const lead = configuredUrl ? `${configuredUrl.key} selects the HTTP server for '${name}', but no API key could be resolved` : `${keys.apiUrlKeys[0]} is not set and no API key could be resolved for '${name}'; a credential is required before the default fleet gateway authority applies`;
9136
- warnings.push(`${lead}; refusing to create an unauthenticated client \u2014 public clients never fall back to SQLite or another local store. ` + `Looked in the Keychain (macOS only), then for a credential file at ${diskHint}, then for ${keys.apiKeyKeys[0]} in the environment.`);
9137
- throw new ClientTransportConfigurationError(name, warnings.join(" "), [configuredUrl?.key ?? keys.apiUrlKeys[0]]);
9138
- }
9139
- if (credential.warning)
9140
- warnings.push(credential.warning);
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
- const apiUrlSource = urlHit.key;
9153
- let baseUrl;
9170
+ };
9171
+ }
9172
+ function readMetadataText(file) {
9173
+ let fd;
9154
9174
  try {
9155
- baseUrl = toV1BaseUrl(urlHit.value);
9175
+ fd = openSync2(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
9156
9176
  } catch (error) {
9157
- const message = error instanceof Error ? error.message : String(error);
9158
- throw new ClientTransportConfigurationError(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
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 resolveClientTransport(name, env = process.env, options = {}) {
9176
- return resolveClientTransportSnapshot(name, env, options).resolution;
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
- function credentialDiskSourcesForMessage(name, env) {
9179
- const paths = credentialDiskSources(name, env);
9180
- return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
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
- for (const key of SKILLS_API_URL_ENV_KEYS) {
9232
- const value = env[key]?.trim();
9233
- if (value)
9234
- return { value, source: key };
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 credentialDiskSources(SKILLS_APP, env);
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 \u2014 no ${SKILLS_API_KEY_ENV} and no ${SKILLS_API_URL_ENV} resolved, ` + `so this runs on this machine against the bundled corpus. ` + `Sign in with: skills auth login`);
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
- return resolveSkillsFleetOrThrow(env, options);
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 resolveSkillsFleetOrThrow(env, options) {
9276
- let resolution;
9277
- try {
9278
- resolution = resolveClientTransport(SKILLS_APP, env, { credentials: options.credentials });
9279
- } catch (error) {
9280
- if (!isClientTransportConfigurationError(error))
9281
- throw error;
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
- throw error;
9359
+ snapshot[key] = descriptor.value;
9293
9360
  }
9294
- const configured = configuredSkillsApiUrl(env, options.credentials?.keychain);
9295
- const apiOrigin = configured ? normalizeSkillsApiOrigin(configured.value) : stripV1(resolution.baseUrl);
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
- throw new SkillsFleetCredentialError(`A Skills authority resolved but no API key did. Sign in with: skills auth login`);
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: resolution.apiUrlSource ?? (configured?.source ?? "default"),
9304
- apiKeySource: resolution.apiKeySource ?? credential.source,
9305
- apiKeyTier: resolution.apiKeyTier,
9306
- warning: resolution.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
- const fleet = resolveSkillsFleet(env, options);
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, env);
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 apiKey = await resolveSkillsApiKey(env, options);
9352
- return apiKey ? { apiKey, reason: null } : { apiKey: null, reason: null };
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 instanceof SkillsFleetCredentialError || error?.name === "SkillsFleetCredentialError") {
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
- const fleet = resolveSkillsFleet(env, options);
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 resolveSkillsApiKey();
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 record = isRecord3(payload) ? payload : {};
10367
+ const record2 = isRecord3(payload) ? payload : {};
10144
10368
  return {
10145
10369
  contractVersion: REMOTE_SKILL_RUN_CONTRACT_VERSION,
10146
- ...pickString(record, "id"),
10147
- skill: pickStringValue(record, "skill") ?? fallbackSkill,
10148
- ...pickString(record, "requestedSlug"),
10149
- ...pickString(record, "status"),
10150
- ...pickNumber(record, "exitCode"),
10151
- ...pickString(record, "correlationId"),
10152
- ...pickString(record, "createdAt"),
10153
- ...pickString(record, "startedAt"),
10154
- ...pickString(record, "completedAt"),
10155
- ...pickNumber(record, "durationMs"),
10156
- ...pickString(record, "outputType"),
10157
- ...hasOwn(record, "outputPreview") ? { outputPreview: record.outputPreview } : {},
10158
- ...pickString(record, "errorCode"),
10159
- ...pickString(record, "errorMessage"),
10160
- ...pickString(record, "error"),
10161
- ...pickString(record, "code"),
10162
- ...hasOwn(record, "details") ? { details: record.details } : {}
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(record, key) {
10169
- return Object.prototype.hasOwnProperty.call(record, key);
10392
+ function hasOwn(record2, key) {
10393
+ return Object.prototype.hasOwnProperty.call(record2, key);
10170
10394
  }
10171
- function pickString(record, key) {
10172
- const value = pickStringValue(record, key);
10395
+ function pickString(record2, key) {
10396
+ const value = pickStringValue(record2, key);
10173
10397
  return value === undefined ? {} : { [key]: value };
10174
10398
  }
10175
- function pickStringValue(record, key) {
10176
- const value = record[key];
10399
+ function pickStringValue(record2, key) {
10400
+ const value = record2[key];
10177
10401
  return typeof value === "string" ? value : undefined;
10178
10402
  }
10179
- function pickNumber(record, key) {
10180
- const value = record[key];
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, statusText) {
10202
- super(`Remote request to ${path} failed: HTTP ${status}${statusText ? ` ${statusText}` : ""}`);
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.replace(/\/$/, "");
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
- const res = await this.request("/api/v1/skills");
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
- const res = await this.request(`/api/v1/runs/${slug}`, {
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({ input, args })
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 res = await this.request(`/api/v1/runs/${runId}`);
10273
- if (!res.ok)
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
- const res = await this.request(`/api/v1/runs/${runId}/logs`);
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
- const res = await this.request(`/api/v1/runs?limit=${limit}`);
10286
- return res.json();
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
- const res = await this.request(`/api/v1/runs/${runId}/artifacts`);
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.json();
10332
- return Array.isArray(body.versions) ? body.versions : [];
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.json();
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
- for (const tag of payload) {
10366
- if (typeof tag !== "string" || tag.trim().length === 0) {
10367
- throw new Error("Remote tags payload did not match the expected contract (every element must be a non-empty tag name)");
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
- return payload;
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(record, field) {
10388
- if (record[field] === undefined)
11038
+ function requireOptionalString(record2, field) {
11039
+ if (record2[field] === undefined)
10389
11040
  return;
10390
- if (typeof record[field] !== "string") {
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 record[field];
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 record = entry;
10400
- const slug = typeof record.slug === "string" && record.slug.trim() ? record.slug.trim() : undefined;
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 (record.metadata !== undefined) {
10406
- if (!record.metadata || typeof record.metadata !== "object" || Array.isArray(record.metadata)) {
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 = record.metadata;
11077
+ metadata = record2.metadata;
10410
11078
  }
10411
- const pinnedAt = requireOptionalString(record, "pinnedAt");
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 record = entry;
10429
- const slug = typeof record.slug === "string" && record.slug.trim() ? record.slug.trim() : undefined;
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(record, "name") !== undefined ? { name: requireOptionalString(record, "name") } : {},
10436
- ...requireOptionalString(record, "version") !== undefined ? { version: requireOptionalString(record, "version") } : {},
10437
- ...requireOptionalString(record, "updatedAt") !== undefined ? { updatedAt: requireOptionalString(record, "updatedAt") } : {}
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 payload = await response.clone().json();
10449
- if (!payload || typeof payload !== "object")
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 record = payload;
10462
- if (!Array.isArray(record.skills)) {
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 = record.skills.map(normalizeSkillSummary);
10466
- const nextCursor = record.nextCursor === undefined || record.nextCursor === null ? null : record.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 fleet = resolveSkillsFleet(env);
10474
- if (fleet.mode !== "hosted")
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 createHash3 } from "crypto";
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 createHash3("sha256").update(canonical).digest("hex");
11398
+ return createHash4("sha256").update(canonical).digest("hex");
10708
11399
  }
10709
- function revisionIdOfRecord(record) {
10710
- return revisionIdOf(record);
11400
+ function revisionIdOfRecord(record2) {
11401
+ return revisionIdOf(record2);
10711
11402
  }
10712
11403
 
10713
11404
  // src/lib/skill-bundle.ts
10714
- import { createHash as createHash4 } from "crypto";
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 createHash4("sha256").update(bytes).digest("hex");
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 (!existsSync15(join17(target, PULL_MARKER_FILE))) {
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, record) {
12108
+ function writePullMarker(dir, record2) {
11418
12109
  const marker = {
11419
12110
  managedBy: "@hasna/skills",
11420
- skill: record.skill,
11421
- source: record.source ?? "pull",
11422
- ...record.version ? { version: record.version } : {},
11423
- ...record.contentHash ? { contentHash: record.contentHash } : {},
11424
- ...record.sourceCommit ? { sourceCommit: record.sourceCommit } : {},
11425
- ...record.signature ? { signature: record.signature } : {},
11426
- ...record.revisionId ? { revisionId: record.revisionId } : {},
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 record = raw;
11442
- const kind = record.kind === "instruction" || record.kind === "executable" ? record.kind : undefined;
11443
- const tags = Array.isArray(record.tags) ? record.tags.filter((tag) => typeof tag === "string" && tag.trim().length > 0) : undefined;
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(record.displayName) ? { displayName: str(record.displayName) } : {},
11446
- ...str(record.description) ? { description: str(record.description) } : {},
11447
- ...str(record.category) ? { category: str(record.category) } : {},
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(record.version) ? { version: str(record.version) } : {},
12140
+ ...str(record2.version) ? { version: str(record2.version) } : {},
11450
12141
  ...kind ? { kind } : {},
11451
- ...REVISION_ID_PATTERN.test(str(record.revisionId) ?? "") ? { revisionId: str(record.revisionId) } : {},
11452
- ...typeof record.skillMd === "string" && record.skillMd.length > 0 ? { skillMd: record.skillMd } : {},
11453
- ...str(record.publishedSource) ? { publishedSource: str(record.publishedSource) } : {}
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 record = entry;
11463
- return str(record.slug) ?? str(record.name);
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.3.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/contracts": "1.0.1",
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 contracts = [...toolContracts].sort((a, b) => a.name.localeCompare(b.name));
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 createHash5, createHmac as createHmac2 } from "crypto";
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: createHash5("sha256").update(bytes).digest("hex"),
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 = createHash5("sha256").update(bytes).digest("hex");
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 record of records) {
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
- record.scope,
12924
- record.kind,
12925
- record.id,
12926
- record.updatedAt,
12927
- record.deletedAt ?? null,
12928
- record.source ?? null,
12929
- JSON.stringify(record.payload)
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 createHash5("sha256").update(value).digest("hex");
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 createHash6 } from "crypto";
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 createHash6("sha256").update(readFileSync17(filePath)).digest("hex");
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 unchanged = plans.length - untouched.length;
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 createHash7 } from "crypto";
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 createHash7("sha256").update(joined.sort().join(`
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 unchanged = plan.totalFiles - written;
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,