@hasna/skills 0.4.0 → 0.5.1

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 (48) hide show
  1. package/README.md +220 -5
  2. package/bin/index.js +7747 -5645
  3. package/bin/mcp.js +1493 -431
  4. package/bin/migrate.js +148 -40
  5. package/bin/server.js +53 -83
  6. package/bin/worker.js +41 -73
  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/tool-primitives.d.ts +1 -1
  13. package/dist/cli/commands/workspace-member-mutations.d.ts +2 -0
  14. package/dist/cli/commands/workspace-members.d.ts +2 -0
  15. package/dist/cli/commands/workspace-selection.d.ts +11 -0
  16. package/dist/cli/env-assignment.d.ts +9 -0
  17. package/dist/index.d.ts +8 -2
  18. package/dist/index.js +841 -167
  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/client-types.d.ts +75 -0
  23. package/dist/lib/credential-state.d.ts +12 -0
  24. package/dist/lib/fleet-credentials.d.ts +41 -15
  25. package/dist/lib/home-adoption.d.ts +2 -0
  26. package/dist/lib/home-census.d.ts +3 -1
  27. package/dist/lib/local-opt-in.d.ts +24 -0
  28. package/dist/lib/portable-skills-files.d.ts +6 -2
  29. package/dist/lib/read-access.d.ts +83 -0
  30. package/dist/lib/remote-auth.d.ts +23 -3
  31. package/dist/lib/remote-client.d.ts +45 -5
  32. package/dist/lib/remote-profile.d.ts +26 -0
  33. package/dist/lib/remote-registry.d.ts +7 -3
  34. package/dist/lib/remote-workspace-selection.d.ts +58 -0
  35. package/dist/lib/remote-workspace.d.ts +76 -0
  36. package/dist/lib/skillinfo.d.ts +1 -1
  37. package/dist/lib/workspace-profile.d.ts +49 -0
  38. package/dist/mcp/helpers.d.ts +22 -0
  39. package/dist/mcp/index.d.ts +16 -0
  40. package/dist/sdk/governance-store.d.ts +1 -0
  41. package/dist/sdk/index.d.ts +8 -2
  42. package/dist/sdk/index.js +1312 -297
  43. package/dist/sdk/outputs.d.ts +0 -11
  44. package/dist/sdk/runs.d.ts +1 -1
  45. package/dist/storage.js +6 -40
  46. package/docs/skill-standard.md +30 -2
  47. package/package.json +6 -4
  48. package/dist/lib/instance-credentials-race.fixture.d.ts +0 -1
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";
@@ -1838,14 +1804,27 @@ function normalizePortableSkillName(name) {
1838
1804
  }
1839
1805
  return normalized;
1840
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
+ }
1841
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) {
1842
1821
  const skillJsonPath = join6(skillPath, "skill.json");
1843
1822
  const skillMdPath = join6(skillPath, "SKILL.md");
1844
1823
  const pkgPath = join6(skillPath, "package.json");
1845
1824
  const jsonManifest = existsSync6(skillJsonPath) ? readJsonObject(skillJsonPath) : undefined;
1846
1825
  const frontmatter = existsSync6(skillMdPath) ? parseSkillFrontmatter(readFileSync5(skillMdPath, "utf-8")) ?? undefined : undefined;
1847
1826
  const pkg = existsSync6(pkgPath) ? readJsonObject(pkgPath) : undefined;
1848
- 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);
1849
1828
  const description = stringField(jsonManifest, "description") ?? frontmatter?.description ?? stringValue(pkg?.description) ?? `${name} skill`;
1850
1829
  const version = readDeclaredSkillVersion(skillPath) ?? PORTABLE_SKILL_DEFAULT_VERSION;
1851
1830
  const kind = parseSkillKind(stringField(jsonManifest, "kind") ?? frontmatter?.kind);
@@ -2093,10 +2072,45 @@ function ensureInstructionSkillFiles(skillPath, manifest) {
2093
2072
  };
2094
2073
  if (!existsSync6(join6(skillPath, "SKILL.md"))) {
2095
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
+ }
2096
2090
  }
2097
2091
  writeSkillJsonWithHash(skillPath, next);
2098
2092
  return readPortableSkillManifest(skillPath, next.name);
2099
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
+ }
2100
2114
  function copySkillDirectory(source, destination) {
2101
2115
  const resolvedSource = lstatSync2(source).isSymbolicLink() ? realpathSync(source) : source;
2102
2116
  mkdirSync2(destination, { recursive: true });
@@ -2504,7 +2518,7 @@ function isOfficialSkillName(name) {
2504
2518
  return OFFICIAL_SKILL_NAMES.has(name);
2505
2519
  }
2506
2520
  function scaffoldPortableSkill(name, options = {}) {
2507
- const skillName = normalizePortableSkillName(name);
2521
+ const skillName = normalizeNewPortableSkillName(name);
2508
2522
  const root = getPortableSkillsRoot(options);
2509
2523
  const skillPath = join7(root, skillName);
2510
2524
  if (existsSync7(skillPath)) {
@@ -2574,9 +2588,9 @@ function portPortableSkill(sourcePath, options = {}) {
2574
2588
  if (!existsSync7(absoluteSource) || !statSync6(absoluteSource).isDirectory()) {
2575
2589
  throw new Error(`Skill source directory not found: ${sourcePath}`);
2576
2590
  }
2577
- const inferred = readPortableSkillManifest(absoluteSource, basename2(absoluteSource));
2591
+ const inferred = readPortableSkillManifestForImport(absoluteSource);
2578
2592
  const explicitName = options.name != null;
2579
- const skillName = normalizePortableSkillName(options.name ?? inferred.name);
2593
+ const skillName = normalizeNewPortableSkillName(options.name ?? inferred.name);
2580
2594
  if (isOfficialSkillName(skillName) && !options.allowShadow) {
2581
2595
  const sourceSlug = safeNormalizeName(basename2(absoluteSource));
2582
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.`;
@@ -3191,6 +3205,7 @@ import { fileURLToPath } from "url";
3191
3205
  import {
3192
3206
  cpSync as cpSync3,
3193
3207
  existsSync as existsSync9,
3208
+ lstatSync as lstatSync3,
3194
3209
  mkdirSync as mkdirSync4,
3195
3210
  mkdtempSync as mkdtempSync2,
3196
3211
  readFileSync as readFileSync7,
@@ -3212,6 +3227,20 @@ var SYNC_AGENTS = ["claude", "codewith", "codex", "opencode", "cursor"];
3212
3227
  var SKILLS_SOURCE_ENV = "SKILLS_SOURCE";
3213
3228
  var SYNC_MARKER_FILE = ".hasna-skills.json";
3214
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
+ }
3215
3244
  function isSyncAgent(value) {
3216
3245
  return SYNC_AGENTS.includes(value);
3217
3246
  }
@@ -3395,7 +3424,7 @@ function writeManagedSkillDir(dir, skillMd, options) {
3395
3424
  const skillMdPath = join9(dir, "SKILL.md");
3396
3425
  const markerPath = join9(dir, SYNC_MARKER_FILE);
3397
3426
  const dirExists = existsSync9(dir);
3398
- const managed = existsSync9(markerPath);
3427
+ const managed = hasSkillsOwnershipMarker(dir);
3399
3428
  const hasSkillMd = existsSync9(skillMdPath);
3400
3429
  if (dirExists && !managed && !hasSkillMd) {
3401
3430
  return {
@@ -3408,7 +3437,7 @@ function writeManagedSkillDir(dir, skillMd, options) {
3408
3437
  return {
3409
3438
  action: "skip",
3410
3439
  path: skillMdPath,
3411
- 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"
3412
3441
  };
3413
3442
  }
3414
3443
  if (dirExists && managed && hasSkillMd && !options.force && isPointerSkillMd(skillMd)) {
@@ -3485,7 +3514,7 @@ function writeManagedSkillDir(dir, skillMd, options) {
3485
3514
  }
3486
3515
  function removeManagedAgentSkill(skill, agent, homeDir = homedir2()) {
3487
3516
  const dir = join9(agentGlobalSkillsDir(agent, homeDir), skill);
3488
- if (!existsSync9(join9(dir, SYNC_MARKER_FILE)))
3517
+ if (!hasSkillsOwnershipMarker(dir))
3489
3518
  return false;
3490
3519
  rmSync2(dir, { recursive: true, force: true });
3491
3520
  return true;
@@ -3861,7 +3890,7 @@ function removeSkillForAgent(name, options) {
3861
3890
  const canonicalName = getCanonicalSkillName(name);
3862
3891
  const scope = options.scope ?? "global";
3863
3892
  const dir = getAgentSkillPath(canonicalName, options.agent, scope, options.projectDir);
3864
- if (!existsSync11(join11(dir, SYNC_MARKER_FILE)))
3893
+ if (!hasSkillsOwnershipMarker(dir))
3865
3894
  return false;
3866
3895
  rmSync3(dir, { recursive: true, force: true });
3867
3896
  return true;
@@ -4288,7 +4317,7 @@ async function runSkill(name, args, options = {}) {
4288
4317
  }
4289
4318
  const proc = Bun.spawn(["bun", "run", entryPath, ...args], {
4290
4319
  cwd: skillPath,
4291
- stdout: options.stdio === "pipe" ? "pipe" : "inherit",
4320
+ stdout: options.stdio === "pipe" ? "pipe" : options.stdio === "stderr" ? 2 : "inherit",
4292
4321
  stderr: options.stdio === "pipe" ? "pipe" : "inherit",
4293
4322
  stdin: "inherit",
4294
4323
  env: { ...process.env, ...options.env }
@@ -9106,7 +9135,7 @@ var AUTHORITY_OVERRIDE_HEADERS = new Set([
9106
9135
  ]);
9107
9136
 
9108
9137
  // src/lib/instance-credentials.ts
9109
- import { closeSync as closeSync2, constants, fstatSync as fstatSync2, lstatSync as lstatSync3, openSync as openSync2, readSync } from "fs";
9138
+ import { closeSync as closeSync2, constants, fstatSync as fstatSync2, lstatSync as lstatSync4, openSync as openSync2, readSync } from "fs";
9110
9139
  var SKILLS_BOUND_API_URL = "HASNA_SKILLS_BOUND_API_URL";
9111
9140
  function selectedSkillsProfile(env, explicit) {
9112
9141
  const selected = explicit ?? env.HASNA_PROFILE;
@@ -9122,7 +9151,7 @@ function skillsProfileCredentialFiles(env, explicit) {
9122
9151
  }
9123
9152
  function fileIdentity(file) {
9124
9153
  try {
9125
- return lstatSync3(file);
9154
+ return lstatSync4(file);
9126
9155
  } catch (error) {
9127
9156
  if (["ENOENT", "ENOTDIR"].includes(error.code ?? ""))
9128
9157
  return null;
@@ -9193,6 +9222,28 @@ function readSkillsInstanceMetadata(file) {
9193
9222
  return { apiUrl: urls[0], binding: values.get(SKILLS_BOUND_API_URL) };
9194
9223
  }
9195
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);
9245
+ }
9246
+
9196
9247
  // src/lib/fleet-credentials.ts
9197
9248
  var SKILLS_APP = "skills";
9198
9249
  var ENV_KEYS = clientTransportEnvKeys(SKILLS_APP);
@@ -9212,6 +9263,9 @@ class SkillsFleetCredentialError extends Error {
9212
9263
  function isCredentialResolutionError(error) {
9213
9264
  return error instanceof CredentialResolutionError || typeof error === "object" && error !== null && error.name === "CredentialResolutionError";
9214
9265
  }
9266
+ function isSkillsFleetCredentialError(error) {
9267
+ return error instanceof SkillsFleetCredentialError || typeof error === "object" && error !== null && error.name === "SkillsFleetCredentialError";
9268
+ }
9215
9269
  function asSkillsFleetCredentialError(error) {
9216
9270
  if (!isCredentialResolutionError(error))
9217
9271
  return null;
@@ -9278,7 +9332,7 @@ function noticeLocalSkillsMode(write = (line) => console.error(line)) {
9278
9332
  if (localNoticePrinted)
9279
9333
  return;
9280
9334
  localNoticePrinted = true;
9281
- 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.`);
9282
9336
  }
9283
9337
  function resolveSkillsFleet(env = process.env, options = {}) {
9284
9338
  try {
@@ -9315,13 +9369,16 @@ function snapshotSkillsOptions(env, options) {
9315
9369
  } } };
9316
9370
  }
9317
9371
  function resolveSkillsFleetOrThrow(env, options) {
9372
+ if (selectsSkillsLocalMode(env))
9373
+ return { mode: "local", apiOrigin: null, apiKey: null };
9318
9374
  const assertFilesUnchanged = captureSkillsCredentialFiles(skillsProfileCredentialFiles(env, options.credentials?.profile));
9319
9375
  const configured = configuredSkillsApiUrl(env, options.credentials?.keychain, options.credentials?.profile);
9320
9376
  const credential = resolveCredential(SKILLS_APP, env, options.credentials);
9321
9377
  if (!credential) {
9322
- if (!configured)
9323
- return { mode: "local", apiOrigin: null, apiKey: null };
9324
- 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 hasna.credentials.skills.api-key, ${skillsCredentialFiles(env).join(" or ") || "no credentials file"}, and ${SKILLS_API_KEY_ENV}. 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`);
9325
9382
  }
9326
9383
  const apiOrigin = normalizeSkillsApiOrigin(configured?.value ?? defaultFleetGatewayBaseUrl(SKILLS_APP));
9327
9384
  toV1BaseUrl(apiOrigin);
@@ -9343,6 +9400,10 @@ function resolveSkillsFleetOrThrow(env, options) {
9343
9400
  }
9344
9401
  return { ...base, apiKey: credential.apiKey, apiKeyPointer: null };
9345
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
+ }
9346
9407
  function assertCredentialInstance(credential, apiOrigin, env, options) {
9347
9408
  let bound;
9348
9409
  if (credential.tier === "disk" || credential.tier === "profile") {
@@ -9396,7 +9457,7 @@ async function skillsCredentialOrReason(env = process.env, options = {}) {
9396
9457
  const connection = await resolveSkillsConnection(env, options);
9397
9458
  return connection ? { apiKey: connection.apiKey, apiOrigin: connection.apiOrigin, reason: null } : { apiKey: null, apiOrigin: null, reason: null };
9398
9459
  } catch (error) {
9399
- if (error instanceof SkillsFleetCredentialError || error?.name === "SkillsFleetCredentialError") {
9460
+ if (isSkillsFleetCredentialError(error)) {
9400
9461
  return { apiKey: null, apiOrigin: null, reason: error.message };
9401
9462
  }
9402
9463
  throw error;
@@ -9408,7 +9469,14 @@ function resolveSkillsApiOrigin(env = process.env, options = {}) {
9408
9469
  toV1BaseUrl(configured.value);
9409
9470
  return { origin: normalizeSkillsApiOrigin(configured.value), source: configured.source };
9410
9471
  }
9411
- 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
+ }
9412
9480
  return fleet.mode === "hosted" ? { origin: fleet.apiOrigin, source: fleet.apiUrlSource } : null;
9413
9481
  }
9414
9482
  function requireSkillsApiOrigin(action = "This command", env = process.env, options = {}) {
@@ -9640,12 +9708,21 @@ function parseRemoteContract(schema, payload, message) {
9640
9708
  }
9641
9709
  async function remoteRequestHeaders(options) {
9642
9710
  const headers = new Headers({ Accept: "application/json" });
9643
- const token = options.authToken !== undefined ? options.authToken : await resolveSkillsApiKey();
9711
+ const token = options.authToken !== undefined ? options.authToken : await ambientTokenFor(options.apiUrl);
9644
9712
  const trimmed = token?.trim();
9645
9713
  if (trimmed)
9646
9714
  headers.set("Authorization", `Bearer ${trimmed}`);
9647
9715
  return headers;
9648
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
+ }
9649
9726
  async function fetchRemoteJson(url, options) {
9650
9727
  const fetchImpl = options.fetchImpl || fetch;
9651
9728
  const headers = await remoteRequestHeaders(options);
@@ -10177,6 +10254,202 @@ function primitiveHaystack(primitive) {
10177
10254
  function clone(value) {
10178
10255
  return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
10179
10256
  }
10257
+ // src/lib/remote-workspace-selection.ts
10258
+ var record = (v) => !!v && typeof v === "object" && !Array.isArray(v);
10259
+ var uuid = (v) => typeof v === "string" && /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(v);
10260
+ var text = (v, max = 1024) => typeof v === "string" && !!v.trim() && v.length <= max && !/[\p{Cc}\p{Cs}\u2028\u2029]/u.test(v);
10261
+ var role = (v) => typeof v === "string" && ["owner", "admin", "member", "viewer"].includes(v);
10262
+ var invalidWorkspaceResult = "The server returned an invalid workspace selection result.";
10263
+
10264
+ class WorkspaceContextInputError extends Error {
10265
+ constructor() {
10266
+ super("Provide the observed user ID and exact lowercase membership ID.");
10267
+ this.name = "WorkspaceContextInputError";
10268
+ }
10269
+ }
10270
+
10271
+ class WorkspaceIdentityMismatchError extends Error {
10272
+ constructor() {
10273
+ super("The verified account does not match the requested workspace context.");
10274
+ this.name = "WorkspaceIdentityMismatchError";
10275
+ }
10276
+ }
10277
+ function workspaceExpectedUserId(value) {
10278
+ if (!uuid(value))
10279
+ throw new WorkspaceContextInputError;
10280
+ return value;
10281
+ }
10282
+ function workspaceContext(value) {
10283
+ if (!record(value) || Object.keys(value).sort().join(",") !== "membershipId,userId" || !uuid(value.userId) || !uuid(value.membershipId))
10284
+ throw new WorkspaceContextInputError;
10285
+ return { userId: value.userId, membershipId: value.membershipId };
10286
+ }
10287
+ function invalid() {
10288
+ throw new Error(invalidWorkspaceResult);
10289
+ }
10290
+ function organization(v) {
10291
+ if (!record(v) || !uuid(v.id) || !text(v.slug) || !text(v.name))
10292
+ return invalid();
10293
+ return { id: v.id, slug: v.slug, name: v.name };
10294
+ }
10295
+ function parseAccountWorkspaces(value) {
10296
+ if (!record(value) || !Array.isArray(value.workspaces) || !value.workspaces.length || value.workspaces.length > 1000)
10297
+ return invalid();
10298
+ const workspaces = value.workspaces.map((v) => {
10299
+ if (!record(v) || !uuid(v.membershipId) || !role(v.role) || typeof v.current !== "boolean")
10300
+ return invalid();
10301
+ return { membershipId: v.membershipId, organization: organization(v.organization), role: v.role, current: v.current };
10302
+ });
10303
+ if (workspaces.filter((w) => w.current).length !== 1 || new Set(workspaces.map((w) => w.membershipId)).size !== workspaces.length || new Set(workspaces.map((w) => w.organization.id)).size !== workspaces.length)
10304
+ return invalid();
10305
+ return { workspaces };
10306
+ }
10307
+ function parseWorkspaceIdentity(value, expectedUserId) {
10308
+ if (!record(value))
10309
+ return invalid();
10310
+ const user = value.user;
10311
+ if (!record(user) || !uuid(user.id) || !uuid(user.membershipId) || !text(user.email, 320) || !(user.displayName === null || text(user.displayName)) || !role(user.role))
10312
+ return invalid();
10313
+ if (user.id !== expectedUserId)
10314
+ throw new WorkspaceIdentityMismatchError;
10315
+ return { user: { id: user.id, membershipId: user.membershipId, email: user.email, displayName: user.displayName, role: user.role }, organization: organization(value.organization) };
10316
+ }
10317
+ function sessionToken(value) {
10318
+ if (typeof value !== "string" || !value || value.length > 8192 || /[^\x21-\x7e]/.test(value) || value.startsWith("sk_"))
10319
+ return invalid();
10320
+ return value;
10321
+ }
10322
+ function parseWorkspaceSession(value, expected) {
10323
+ const identity = parseWorkspaceIdentity(value, expected.userId);
10324
+ if (identity.user.membershipId !== expected.membershipId)
10325
+ throw new WorkspaceIdentityMismatchError;
10326
+ return { token: sessionToken(value.token), ...identity };
10327
+ }
10328
+ function parseWorkspaceLogin(value, expectedUserId) {
10329
+ const user = record(value) && value.user;
10330
+ if (!record(value) || !record(user) || !uuid(user.id))
10331
+ return invalid();
10332
+ if (expectedUserId !== undefined && user.id !== expectedUserId)
10333
+ throw new WorkspaceIdentityMismatchError;
10334
+ return { token: sessionToken(value.token), userId: user.id };
10335
+ }
10336
+ var workspaceSelectionFailures = {
10337
+ INVALID_WORKSPACE_SELECTION: [400, "Provide only the exact membership ID from your workspace list."],
10338
+ SESSION_EXPIRED: [401, "Sign in again before selecting a workspace."],
10339
+ ACCOUNT_UNAVAILABLE: [403, "Account is unavailable."],
10340
+ INTERACTIVE_SESSION_REQUIRED: [403, "Interactive sign-in is required to select a workspace."],
10341
+ WORKSPACE_UNAVAILABLE: [404, "Workspace is unavailable. Refresh your workspace list."],
10342
+ WORKSPACE_BUSY: [503, "Workspace is busy. Refresh before retrying."]
10343
+ };
10344
+ function workspaceSelectionFailure(value, status) {
10345
+ if (!record(value) || typeof value.code !== "string" || !Object.hasOwn(workspaceSelectionFailures, value.code))
10346
+ return null;
10347
+ const code = value.code;
10348
+ return workspaceSelectionFailures[code][0] === status ? code : null;
10349
+ }
10350
+
10351
+ // src/lib/remote-workspace.ts
10352
+ var record2 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
10353
+ var cursor = (value) => typeof value === "string" && /^[A-Za-z0-9_-]{1,512}$/.test(value);
10354
+ var uuid2 = (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);
10355
+ function workspaceMembersQuery(options = {}) {
10356
+ if (!record2(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))
10357
+ throw new Error("Use a roster limit from 1 to 100 and an unchanged continuation cursor.");
10358
+ const query = new URLSearchParams;
10359
+ if (options.limit !== undefined)
10360
+ query.set("limit", String(options.limit));
10361
+ if (options.cursor !== undefined)
10362
+ query.set("cursor", options.cursor);
10363
+ return query.size ? `?${query}` : "";
10364
+ }
10365
+ function timestamp(value) {
10366
+ if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z$/.test(value))
10367
+ return false;
10368
+ const time = Date.parse(value);
10369
+ return Number.isFinite(time) && new Date(time).toISOString().slice(0, 23) === value.slice(0, 23);
10370
+ }
10371
+ function parseMember(row, fail) {
10372
+ if (!record2(row) || !uuid2(row.membershipId) || !uuid2(row.userId) || typeof row.email !== "string" || !row.email || !(row.displayName === null || typeof row.displayName === "string") || !isRole(row.role) || !timestamp(row.createdAt))
10373
+ return fail();
10374
+ return {
10375
+ membershipId: row.membershipId,
10376
+ userId: row.userId,
10377
+ email: row.email,
10378
+ displayName: row.displayName,
10379
+ role: row.role,
10380
+ createdAt: row.createdAt
10381
+ };
10382
+ }
10383
+ var isRole = (value) => typeof value === "string" && ["owner", "admin", "member", "viewer"].includes(value);
10384
+
10385
+ class WorkspaceMemberInputError extends Error {
10386
+ constructor() {
10387
+ super("Use an unchanged lowercase membership ID and the exact role and expected-role parameters from the roster.");
10388
+ this.name = "WorkspaceMemberInputError";
10389
+ }
10390
+ }
10391
+ function mutationInput(membershipId, input, roleChange) {
10392
+ if (typeof membershipId !== "string" || !uuid2(membershipId) || membershipId !== membershipId.toLowerCase() || !record2(input) || Object.keys(input).sort().join(",") !== (roleChange ? "expectedRole,role" : "expectedRole"))
10393
+ throw new WorkspaceMemberInputError;
10394
+ const expectedRole = input.expectedRole, role2 = roleChange ? input.role : undefined;
10395
+ if (!isRole(expectedRole) || roleChange && !isRole(role2))
10396
+ throw new WorkspaceMemberInputError;
10397
+ return { membershipId, role: role2, expectedRole };
10398
+ }
10399
+ function workspaceMemberRoleInput(membershipId, input) {
10400
+ const value = mutationInput(membershipId, input, true);
10401
+ return { membershipId: value.membershipId, body: { role: value.role, expectedRole: value.expectedRole } };
10402
+ }
10403
+ function workspaceMemberRemovalInput(membershipId, input) {
10404
+ const value = mutationInput(membershipId, input, false);
10405
+ return { membershipId: value.membershipId, body: { expectedRole: value.expectedRole } };
10406
+ }
10407
+ var invalidMemberResult = "The server returned an invalid workspace member result. Refresh the roster before another action.";
10408
+ function parseWorkspaceMemberRoleResult(value, membershipId, role2) {
10409
+ const fail = () => {
10410
+ throw new Error(invalidMemberResult);
10411
+ };
10412
+ if (!record2(value) || !uuid2(value.organizationId) || typeof value.changed !== "boolean")
10413
+ return fail();
10414
+ const member = parseMember(value.member, fail);
10415
+ if (member.membershipId !== membershipId || member.role !== role2)
10416
+ return fail();
10417
+ return { organizationId: value.organizationId, member, changed: value.changed };
10418
+ }
10419
+ function parseWorkspaceMemberRemovalResult(value, membershipId) {
10420
+ if (!record2(value) || !uuid2(value.organizationId) || value.membershipId !== membershipId || value.removed !== true || typeof value.alreadyRemoved !== "boolean")
10421
+ throw new Error(invalidMemberResult);
10422
+ return { organizationId: value.organizationId, membershipId, removed: true, alreadyRemoved: value.alreadyRemoved };
10423
+ }
10424
+ var workspaceMemberFailures = {
10425
+ INVALID_REQUEST: [400, "Provide the exact membership role parameters."],
10426
+ ACCOUNT_UNAVAILABLE: [403, "Account is unavailable."],
10427
+ INTERACTIVE_SESSION_REQUIRED: [403, "Fresh interactive sign-in is required."],
10428
+ WORKSPACE_ADMIN_REQUIRED: [403, "A current workspace owner or admin is required."],
10429
+ MEMBERSHIP_ACTION_FORBIDDEN: [403, "Your current workspace role cannot perform this membership action."],
10430
+ MEMBERSHIP_NOT_FOUND: [404, "Membership was not found in the current workspace."],
10431
+ SELF_REMOVAL_UNAVAILABLE: [409, "Leaving your own workspace is not available through member removal."],
10432
+ MEMBERSHIP_ROLE_CHANGED: [409, "The member role changed. Refresh the roster before another action."],
10433
+ LAST_OWNER_REQUIRED: [409, "The workspace must retain at least one active owner."],
10434
+ MEMBERSHIP_BUSY: [503, "Membership is busy. Refresh the roster before another action."]
10435
+ };
10436
+ function workspaceMemberFailure(value, status) {
10437
+ if (!record2(value) || typeof value.code !== "string" || !Object.hasOwn(workspaceMemberFailures, value.code))
10438
+ return null;
10439
+ const code = value.code;
10440
+ return workspaceMemberFailures[code][0] === status ? code : null;
10441
+ }
10442
+ function parseWorkspaceMembersPage(value) {
10443
+ const fail = () => {
10444
+ throw new Error("The server returned an invalid workspace roster.");
10445
+ };
10446
+ if (!record2(value) || !uuid2(value.organizationId) || !Array.isArray(value.members) || value.members.length > 100 || !(value.nextCursor === null || cursor(value.nextCursor)) || !value.members.length && value.nextCursor !== null)
10447
+ return fail();
10448
+ const members = value.members.map((row) => parseMember(row, fail));
10449
+ if (new Set(members.map((row) => row.membershipId)).size !== members.length)
10450
+ return fail();
10451
+ return { organizationId: value.organizationId, members, nextCursor: value.nextCursor };
10452
+ }
10180
10453
  // src/lib/auth-store.ts
10181
10454
  function getApiUrl(action, env = process.env, options = {}) {
10182
10455
  return requireSkillsApiOrigin(action, env, options);
@@ -10185,44 +10458,44 @@ function getApiUrl(action, env = process.env, options = {}) {
10185
10458
  // src/lib/remote-run-contract.ts
10186
10459
  var REMOTE_SKILL_RUN_CONTRACT_VERSION = 1;
10187
10460
  function normalizeRemoteSkillRunContract(payload, fallbackSkill) {
10188
- const record = isRecord3(payload) ? payload : {};
10461
+ const record3 = isRecord3(payload) ? payload : {};
10189
10462
  return {
10190
10463
  contractVersion: REMOTE_SKILL_RUN_CONTRACT_VERSION,
10191
- ...pickString(record, "id"),
10192
- skill: pickStringValue(record, "skill") ?? fallbackSkill,
10193
- ...pickString(record, "requestedSlug"),
10194
- ...pickString(record, "status"),
10195
- ...pickNumber(record, "exitCode"),
10196
- ...pickString(record, "correlationId"),
10197
- ...pickString(record, "createdAt"),
10198
- ...pickString(record, "startedAt"),
10199
- ...pickString(record, "completedAt"),
10200
- ...pickNumber(record, "durationMs"),
10201
- ...pickString(record, "outputType"),
10202
- ...hasOwn(record, "outputPreview") ? { outputPreview: record.outputPreview } : {},
10203
- ...pickString(record, "errorCode"),
10204
- ...pickString(record, "errorMessage"),
10205
- ...pickString(record, "error"),
10206
- ...pickString(record, "code"),
10207
- ...hasOwn(record, "details") ? { details: record.details } : {}
10464
+ ...pickString(record3, "id"),
10465
+ skill: pickStringValue(record3, "skill") ?? fallbackSkill,
10466
+ ...pickString(record3, "requestedSlug"),
10467
+ ...pickString(record3, "status"),
10468
+ ...pickNumber(record3, "exitCode"),
10469
+ ...pickString(record3, "correlationId"),
10470
+ ...pickString(record3, "createdAt"),
10471
+ ...pickString(record3, "startedAt"),
10472
+ ...pickString(record3, "completedAt"),
10473
+ ...pickNumber(record3, "durationMs"),
10474
+ ...pickString(record3, "outputType"),
10475
+ ...hasOwn(record3, "outputPreview") ? { outputPreview: record3.outputPreview } : {},
10476
+ ...pickString(record3, "errorCode"),
10477
+ ...pickString(record3, "errorMessage"),
10478
+ ...pickString(record3, "error"),
10479
+ ...pickString(record3, "code"),
10480
+ ...hasOwn(record3, "details") ? { details: record3.details } : {}
10208
10481
  };
10209
10482
  }
10210
10483
  function isRecord3(value) {
10211
10484
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
10212
10485
  }
10213
- function hasOwn(record, key) {
10214
- return Object.prototype.hasOwnProperty.call(record, key);
10486
+ function hasOwn(record3, key) {
10487
+ return Object.prototype.hasOwnProperty.call(record3, key);
10215
10488
  }
10216
- function pickString(record, key) {
10217
- const value = pickStringValue(record, key);
10489
+ function pickString(record3, key) {
10490
+ const value = pickStringValue(record3, key);
10218
10491
  return value === undefined ? {} : { [key]: value };
10219
10492
  }
10220
- function pickStringValue(record, key) {
10221
- const value = record[key];
10493
+ function pickStringValue(record3, key) {
10494
+ const value = record3[key];
10222
10495
  return typeof value === "string" ? value : undefined;
10223
10496
  }
10224
- function pickNumber(record, key) {
10225
- const value = record[key];
10497
+ function pickNumber(record3, key) {
10498
+ const value = record3[key];
10226
10499
  return typeof value === "number" && Number.isFinite(value) ? { [key]: value } : {};
10227
10500
  }
10228
10501
 
@@ -10361,6 +10634,37 @@ async function readBoundedResponse(response, maximum) {
10361
10634
  return bytes;
10362
10635
  }
10363
10636
 
10637
+ // src/lib/remote-profile.ts
10638
+ function customerNamePatch(input, field) {
10639
+ if (!isRecord4(input) || Object.keys(input).length !== 1 || !Object.hasOwn(input, field))
10640
+ throw new Error("Provide only the requested name field.");
10641
+ const value = input[field];
10642
+ if (typeof value !== "string" || /[\p{Cc}\p{Cs}\u2028\u2029]/u.test(value) || !value.trim() || [...value.trim()].length > 100) {
10643
+ throw new Error("Use a name of 1\u2013100 characters without control characters or newlines.");
10644
+ }
10645
+ return { [field]: value.trim() };
10646
+ }
10647
+ function isRecord4(value) {
10648
+ return !!value && typeof value === "object" && !Array.isArray(value);
10649
+ }
10650
+ function string(value) {
10651
+ return typeof value === "string" && value.length > 0;
10652
+ }
10653
+ function parseUpdatedProfile(value) {
10654
+ const user = isRecord4(value) && value.user;
10655
+ 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)) {
10656
+ throw new Error("The server returned an invalid account profile.");
10657
+ }
10658
+ return { user: { id: user.id, email: user.email, displayName: user.displayName, role: user.role } };
10659
+ }
10660
+ function parseUpdatedWorkspace(value) {
10661
+ const organization2 = isRecord4(value) && value.organization;
10662
+ if (!isRecord4(organization2) || !string(organization2.id) || !string(organization2.slug) || !string(organization2.name)) {
10663
+ throw new Error("The server returned an invalid workspace.");
10664
+ }
10665
+ return { organization: { id: organization2.id, slug: organization2.slug, name: organization2.name } };
10666
+ }
10667
+
10364
10668
  // src/lib/remote-client.ts
10365
10669
  class RemoteRouteUnsupportedError extends Error {
10366
10670
  path;
@@ -10378,14 +10682,43 @@ class RemoteRouteUnsupportedError extends Error {
10378
10682
  class RemoteRequestError extends Error {
10379
10683
  path;
10380
10684
  status;
10381
- constructor(path, status, statusText) {
10382
- super(`Remote request to ${path} failed: HTTP ${status}${statusText ? ` ${statusText}` : ""}`);
10685
+ constructor(path, status, _statusText) {
10686
+ super(`Remote request to ${path} failed: HTTP ${status}`);
10383
10687
  this.path = path;
10384
10688
  this.status = status;
10385
10689
  this.name = "RemoteRequestError";
10386
10690
  }
10387
10691
  }
10388
10692
 
10693
+ class RemoteWorkspaceMemberError extends RemoteRequestError {
10694
+ code;
10695
+ constructor(path, code) {
10696
+ super(path, workspaceMemberFailures[code][0]);
10697
+ this.code = code;
10698
+ this.name = "RemoteWorkspaceMemberError";
10699
+ this.message = workspaceMemberFailures[code][1];
10700
+ }
10701
+ }
10702
+
10703
+ class RemoteWorkspaceSelectionError extends RemoteRequestError {
10704
+ code;
10705
+ constructor(path, code) {
10706
+ super(path, workspaceSelectionFailures[code][0]);
10707
+ this.code = code;
10708
+ this.name = "RemoteWorkspaceSelectionError";
10709
+ this.message = workspaceSelectionFailures[code][1];
10710
+ }
10711
+ }
10712
+
10713
+ class RemoteCapabilityUnavailableError extends RemoteRequestError {
10714
+ code = "SUBSCRIPTION_CHECKOUT_UNAVAILABLE";
10715
+ constructor() {
10716
+ super("/api/v1/billing/checkout", 503);
10717
+ this.name = "RemoteCapabilityUnavailableError";
10718
+ 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.";
10719
+ }
10720
+ }
10721
+
10389
10722
  class RemoteSkillsClient {
10390
10723
  apiUrl;
10391
10724
  apiKey;
@@ -10398,6 +10731,7 @@ class RemoteSkillsClient {
10398
10731
  return fetch(`${this.apiUrl}${path}`, {
10399
10732
  ...options,
10400
10733
  redirect: "error",
10734
+ credentials: "omit",
10401
10735
  signal: options?.signal ?? AbortSignal.timeout(15000),
10402
10736
  headers: {
10403
10737
  Authorization: `Bearer ${this.apiKey}`,
@@ -10413,9 +10747,14 @@ class RemoteSkillsClient {
10413
10747
  if (response.status === 404 && opts.domainNotFoundCodes?.length && await responseBodyCarriesCode(response, opts.domainNotFoundCodes)) {
10414
10748
  return response;
10415
10749
  }
10750
+ response.body?.cancel().catch(() => {});
10416
10751
  throw new RemoteRouteUnsupportedError(routePath, response.status, this.apiUrl);
10417
10752
  }
10418
10753
  if (!response.ok) {
10754
+ if (path === "/api/v1/billing/checkout" && options?.method === "POST" && response.status === 503 && await responseBodyCarriesCode(response, ["SUBSCRIPTION_CHECKOUT_UNAVAILABLE"])) {
10755
+ throw new RemoteCapabilityUnavailableError;
10756
+ }
10757
+ response.body?.cancel().catch(() => {});
10419
10758
  throw new RemoteRequestError(routePath, response.status, response.statusText);
10420
10759
  }
10421
10760
  return response;
@@ -10499,6 +10838,117 @@ class RemoteSkillsClient {
10499
10838
  async getIdentity() {
10500
10839
  return (await this.requestNewRoute("/api/auth/whoami")).json();
10501
10840
  }
10841
+ async listAccountWorkspaces(expectedUserId) {
10842
+ const expected = expectedUserId === undefined ? undefined : workspaceExpectedUserId(expectedUserId);
10843
+ const connection = new RemoteSkillsClient(this.apiKey, this.apiUrl);
10844
+ let identity;
10845
+ if (expected !== undefined) {
10846
+ const value = await connection.requestWorkspaceSelection("/api/auth/whoami");
10847
+ if (!value || typeof value !== "object" || value.authMethod !== "jwt")
10848
+ throw new RemoteWorkspaceSelectionError("/api/v1/account/workspaces", "INTERACTIVE_SESSION_REQUIRED");
10849
+ identity = parseWorkspaceIdentity(value, expected);
10850
+ }
10851
+ const result = parseAccountWorkspaces(await connection.requestWorkspaceSelection("/api/v1/account/workspaces"));
10852
+ const current = result.workspaces.find((workspace) => workspace.current);
10853
+ if (identity && (current.membershipId !== identity.user.membershipId || current.organization.id !== identity.organization.id))
10854
+ throw new WorkspaceIdentityMismatchError;
10855
+ return result;
10856
+ }
10857
+ async switchWorkspace(context) {
10858
+ const target = workspaceContext(context);
10859
+ const connection = new RemoteSkillsClient(this.apiKey, this.apiUrl);
10860
+ const value = await connection.requestWorkspaceSelection("/api/auth/whoami");
10861
+ if (!value || typeof value !== "object" || value.authMethod !== "jwt")
10862
+ throw new RemoteWorkspaceSelectionError("/api/v1/account/workspaces/switch", "INTERACTIVE_SESSION_REQUIRED");
10863
+ parseWorkspaceIdentity(value, target.userId);
10864
+ const selected = parseWorkspaceSession(await connection.requestWorkspaceSelection("/api/v1/account/workspaces/switch", {
10865
+ method: "POST",
10866
+ body: JSON.stringify({ membershipId: target.membershipId })
10867
+ }), target);
10868
+ const verified = await new RemoteSkillsClient(selected.token, connection.apiUrl).requestWorkspaceSelection("/api/auth/whoami");
10869
+ if (!verified || typeof verified !== "object" || verified.authMethod !== "jwt")
10870
+ throw new RemoteWorkspaceSelectionError("/api/v1/account/workspaces/switch", "INTERACTIVE_SESSION_REQUIRED");
10871
+ const identity = parseWorkspaceIdentity(verified, target.userId);
10872
+ if (identity.user.membershipId !== target.membershipId || identity.organization.id !== selected.organization.id)
10873
+ throw new WorkspaceIdentityMismatchError;
10874
+ return { token: selected.token, ...identity };
10875
+ }
10876
+ async requestWorkspaceSelection(path, options) {
10877
+ let response;
10878
+ try {
10879
+ response = await this.request(path, { ...options, credentials: "omit" });
10880
+ } catch {
10881
+ throw new Error("Unable to reach the Skills workspace API.");
10882
+ }
10883
+ let value;
10884
+ try {
10885
+ value = JSON.parse(new TextDecoder().decode(await readBoundedResponse(response, response.ok ? 1024 * 1024 : 4096)));
10886
+ } catch {
10887
+ if (response.ok)
10888
+ throw new Error(invalidWorkspaceResult);
10889
+ }
10890
+ if (!response.ok) {
10891
+ const code = workspaceSelectionFailure(value, response.status);
10892
+ if (code)
10893
+ throw new RemoteWorkspaceSelectionError(path, code);
10894
+ if (response.status === 404 || response.status === 405)
10895
+ throw new RemoteRouteUnsupportedError(path, response.status, this.apiUrl);
10896
+ throw new RemoteRequestError(path, response.status);
10897
+ }
10898
+ return value;
10899
+ }
10900
+ async updateProfile(input) {
10901
+ const body = customerNamePatch(input, "displayName");
10902
+ return parseUpdatedProfile(await (await this.requestNewRoute("/api/v1/account/profile", { method: "PATCH", body: JSON.stringify(body) })).json());
10903
+ }
10904
+ async updateCurrentWorkspace(input) {
10905
+ const body = customerNamePatch(input, "name");
10906
+ return parseUpdatedWorkspace(await (await this.requestNewRoute("/api/v1/workspaces/current", { method: "PATCH", body: JSON.stringify(body) })).json());
10907
+ }
10908
+ async listWorkspaceMembers(options = {}) {
10909
+ const query = workspaceMembersQuery(options);
10910
+ const requestedCursor = options.cursor;
10911
+ const response = await this.requestNewRoute(`/api/v1/workspace/members${query}`);
10912
+ let value;
10913
+ try {
10914
+ value = await response.json();
10915
+ } catch {
10916
+ throw new Error("The server returned an invalid workspace roster.");
10917
+ }
10918
+ const page = parseWorkspaceMembersPage(value);
10919
+ if (requestedCursor !== undefined && page.nextCursor === requestedCursor)
10920
+ throw new Error("The server returned an invalid workspace roster.");
10921
+ return page;
10922
+ }
10923
+ async setWorkspaceMemberRole(membershipId, input) {
10924
+ const captured = workspaceMemberRoleInput(membershipId, input);
10925
+ const value = await this.requestWorkspaceMember(captured.membershipId, "PATCH", captured.body);
10926
+ return parseWorkspaceMemberRoleResult(value, captured.membershipId, captured.body.role);
10927
+ }
10928
+ async removeWorkspaceMember(membershipId, input) {
10929
+ const captured = workspaceMemberRemovalInput(membershipId, input);
10930
+ return parseWorkspaceMemberRemovalResult(await this.requestWorkspaceMember(captured.membershipId, "DELETE", captured.body), captured.membershipId);
10931
+ }
10932
+ async requestWorkspaceMember(membershipId, method, body) {
10933
+ const path = `/api/v1/workspace/members/${membershipId}`;
10934
+ const response = await this.request(path, { method, body: JSON.stringify(body) });
10935
+ let value;
10936
+ try {
10937
+ value = JSON.parse(new TextDecoder().decode(await readBoundedResponse(response, response.ok ? 64 * 1024 : 4096)));
10938
+ } catch {
10939
+ if (response.ok)
10940
+ throw new Error(invalidMemberResult);
10941
+ }
10942
+ if (!response.ok) {
10943
+ const code = workspaceMemberFailure(value, response.status);
10944
+ if (code)
10945
+ throw new RemoteWorkspaceMemberError(path, code);
10946
+ if (response.status === 404 || response.status === 405)
10947
+ throw new RemoteRouteUnsupportedError(path, response.status, this.apiUrl);
10948
+ throw new RemoteRequestError(path, response.status);
10949
+ }
10950
+ return value;
10951
+ }
10502
10952
  async listApiKeys() {
10503
10953
  return this.arrayResponse("/api/auth/keys");
10504
10954
  }
@@ -10690,8 +11140,10 @@ class RemoteSkillsClient {
10690
11140
  return [];
10691
11141
  if (!response.ok)
10692
11142
  throw new Error(`versions request failed: ${response.status}`);
10693
- const body = await response.json();
10694
- return Array.isArray(body.versions) ? body.versions : [];
11143
+ const body = await readSkillVersionPayload(response);
11144
+ if (!isVersionRecord(body) || !Array.isArray(body.versions) || body.slug !== undefined && body.slug !== slug)
11145
+ throw new Error(INVALID_SKILL_VERSION_RESPONSE);
11146
+ return body.versions.map((entry) => normalizeSkillVersion(entry, slug));
10695
11147
  }
10696
11148
  async getSkillVersion(slug, version) {
10697
11149
  const response = await this.requestNewRoute(`/api/v1/skills/${encodeURIComponent(slug)}/versions/${encodeURIComponent(version)}`, undefined, { domainNotFoundCodes: ["SKILL_NOT_FOUND", "SKILL_VERSION_NOT_FOUND"] });
@@ -10699,7 +11151,7 @@ class RemoteSkillsClient {
10699
11151
  return null;
10700
11152
  if (!response.ok)
10701
11153
  throw new Error(`version request failed: ${response.status}`);
10702
- return await response.json();
11154
+ return normalizeSkillVersion(await readSkillVersionPayload(response), slug, version);
10703
11155
  }
10704
11156
  async listPins() {
10705
11157
  const response = await this.requestNewRoute("/api/v1/pins");
@@ -10747,31 +11199,48 @@ class RemoteSkillsClient {
10747
11199
  return normalizeUpdatedSincePage(await response.json());
10748
11200
  }
10749
11201
  }
10750
- function requireOptionalString(record, field) {
10751
- if (record[field] === undefined)
11202
+ function requireOptionalString(record3, field) {
11203
+ if (record3[field] === undefined)
10752
11204
  return;
10753
- if (typeof record[field] !== "string") {
11205
+ if (typeof record3[field] !== "string") {
10754
11206
  throw new Error(`Remote payload did not match the expected contract (${field} must be a string when present)`);
10755
11207
  }
10756
- return record[field];
11208
+ return record3[field];
11209
+ }
11210
+ var INVALID_SKILL_VERSION_RESPONSE = "Remote skill version payload did not match the expected contract.";
11211
+ function isVersionRecord(value) {
11212
+ return value !== null && typeof value === "object" && !Array.isArray(value);
11213
+ }
11214
+ async function readSkillVersionPayload(response) {
11215
+ try {
11216
+ return await response.json();
11217
+ } catch {
11218
+ throw new Error(INVALID_SKILL_VERSION_RESPONSE);
11219
+ }
11220
+ }
11221
+ function normalizeSkillVersion(entry, slug, version) {
11222
+ 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)) {
11223
+ throw new Error(INVALID_SKILL_VERSION_RESPONSE);
11224
+ }
11225
+ return entry;
10757
11226
  }
10758
11227
  function normalizePin(entry) {
10759
11228
  if (!entry || typeof entry !== "object") {
10760
11229
  throw new Error("Remote pin payload did not match the expected contract (expected an object)");
10761
11230
  }
10762
- const record = entry;
10763
- const slug = typeof record.slug === "string" && record.slug.trim() ? record.slug.trim() : undefined;
11231
+ const record3 = entry;
11232
+ const slug = typeof record3.slug === "string" && record3.slug.trim() ? record3.slug.trim() : undefined;
10764
11233
  if (!slug) {
10765
11234
  throw new Error("Remote pin payload did not match the expected contract (missing slug)");
10766
11235
  }
10767
11236
  let metadata;
10768
- if (record.metadata !== undefined) {
10769
- if (!record.metadata || typeof record.metadata !== "object" || Array.isArray(record.metadata)) {
11237
+ if (record3.metadata !== undefined) {
11238
+ if (!record3.metadata || typeof record3.metadata !== "object" || Array.isArray(record3.metadata)) {
10770
11239
  throw new Error("Remote pin payload did not match the expected contract (metadata must be a JSON object when present)");
10771
11240
  }
10772
- metadata = record.metadata;
11241
+ metadata = record3.metadata;
10773
11242
  }
10774
- const pinnedAt = requireOptionalString(record, "pinnedAt");
11243
+ const pinnedAt = requireOptionalString(record3, "pinnedAt");
10775
11244
  return {
10776
11245
  slug,
10777
11246
  ...pinnedAt !== undefined ? { pinnedAt } : {},
@@ -10788,16 +11257,16 @@ function normalizeSkillSummary(entry) {
10788
11257
  if (!entry || typeof entry !== "object") {
10789
11258
  throw new Error("Remote skill payload did not match the expected contract (expected an object)");
10790
11259
  }
10791
- const record = entry;
10792
- const slug = typeof record.slug === "string" && record.slug.trim() ? record.slug.trim() : undefined;
11260
+ const record3 = entry;
11261
+ const slug = typeof record3.slug === "string" && record3.slug.trim() ? record3.slug.trim() : undefined;
10793
11262
  if (!slug) {
10794
11263
  throw new Error("Remote skill payload did not match the expected contract (missing slug)");
10795
11264
  }
10796
11265
  return {
10797
11266
  slug,
10798
- ...requireOptionalString(record, "name") !== undefined ? { name: requireOptionalString(record, "name") } : {},
10799
- ...requireOptionalString(record, "version") !== undefined ? { version: requireOptionalString(record, "version") } : {},
10800
- ...requireOptionalString(record, "updatedAt") !== undefined ? { updatedAt: requireOptionalString(record, "updatedAt") } : {}
11267
+ ...requireOptionalString(record3, "name") !== undefined ? { name: requireOptionalString(record3, "name") } : {},
11268
+ ...requireOptionalString(record3, "version") !== undefined ? { version: requireOptionalString(record3, "version") } : {},
11269
+ ...requireOptionalString(record3, "updatedAt") !== undefined ? { updatedAt: requireOptionalString(record3, "updatedAt") } : {}
10801
11270
  };
10802
11271
  }
10803
11272
  function normalizeSkillSummaryList(payload) {
@@ -10807,26 +11276,55 @@ function normalizeSkillSummaryList(payload) {
10807
11276
  return payload.map(normalizeSkillSummary);
10808
11277
  }
10809
11278
  async function responseBodyCarriesCode(response, codes) {
11279
+ const reader = response.body?.getReader();
11280
+ if (!reader)
11281
+ return false;
11282
+ const maximum = 8 * 1024;
11283
+ let deadline;
11284
+ const expired = new Promise((_, reject) => {
11285
+ deadline = setTimeout(() => reject(new Error("Error response read deadline exceeded")), 1000);
11286
+ });
10810
11287
  try {
10811
- const payload = await response.clone().json();
10812
- if (!payload || typeof payload !== "object")
11288
+ const chunks = [];
11289
+ let size = 0;
11290
+ while (true) {
11291
+ const next = await Promise.race([reader.read(), expired]);
11292
+ if (next.done)
11293
+ break;
11294
+ size += next.value.byteLength;
11295
+ if (size > maximum)
11296
+ return false;
11297
+ chunks.push(next.value);
11298
+ }
11299
+ const bytes = new Uint8Array(size);
11300
+ let offset = 0;
11301
+ for (const chunk of chunks) {
11302
+ bytes.set(chunk, offset);
11303
+ offset += chunk.byteLength;
11304
+ }
11305
+ const payload = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
11306
+ if (!payload || typeof payload !== "object" || Array.isArray(payload) || !Object.hasOwn(payload, "code"))
10813
11307
  return false;
10814
11308
  const code = payload.code;
10815
11309
  return typeof code === "string" && codes.includes(code);
10816
11310
  } catch {
10817
11311
  return false;
11312
+ } finally {
11313
+ clearTimeout(deadline);
11314
+ reader.cancel().catch(() => {});
11315
+ reader.releaseLock();
10818
11316
  }
10819
11317
  }
10820
11318
  function normalizeUpdatedSincePage(payload) {
10821
11319
  if (!payload || typeof payload !== "object") {
10822
11320
  throw new Error("Updated-since payload did not match the expected contract (expected an object)");
10823
11321
  }
10824
- const record = payload;
10825
- if (!Array.isArray(record.skills)) {
11322
+ const record3 = payload;
11323
+ if (!Array.isArray(record3.skills)) {
10826
11324
  throw new Error("Updated-since payload did not match the expected contract (missing skills array)");
10827
11325
  }
10828
- const skills = record.skills.map(normalizeSkillSummary);
10829
- const nextCursor = record.nextCursor === undefined || record.nextCursor === null ? null : record.nextCursor;
11326
+ const skills = record3.skills.map(normalizeSkillSummary);
11327
+ const nextCursor = record3.nextCursor === undefined || record3.nextCursor === null ? null : record3.nextCursor;
10830
11328
  if (nextCursor !== null && typeof nextCursor !== "string") {
10831
11329
  throw new Error("Updated-since payload did not match the expected contract (nextCursor must be a string or absent)");
10832
11330
  }
@@ -11063,8 +11561,8 @@ function revisionIdOf(content) {
11063
11561
  });
11064
11562
  return createHash4("sha256").update(canonical).digest("hex");
11065
11563
  }
11066
- function revisionIdOfRecord(record) {
11067
- return revisionIdOf(record);
11564
+ function revisionIdOfRecord(record3) {
11565
+ return revisionIdOf(record3);
11068
11566
  }
11069
11567
 
11070
11568
  // src/lib/skill-bundle.ts
@@ -11590,7 +12088,7 @@ function provenRevision(meta, slug, bundle) {
11590
12088
  }
11591
12089
  function reconcileTombstone(slug, corpusOptions) {
11592
12090
  const target = join17(getPortableSkillsRoot(corpusOptions), slug);
11593
- if (!existsSync15(join17(target, PULL_MARKER_FILE))) {
12091
+ if (!hasSkillsOwnershipMarker(target)) {
11594
12092
  return { name: slug, success: true, tombstoned: true, removed: false, leftInPlace: true };
11595
12093
  }
11596
12094
  rmSync4(target, { recursive: true, force: true });
@@ -11771,16 +12269,16 @@ function installBundleAtomically(name, entries, options = {}, marker = {}) {
11771
12269
  }
11772
12270
  return { path: target, created };
11773
12271
  }
11774
- function writePullMarker(dir, record) {
12272
+ function writePullMarker(dir, record3) {
11775
12273
  const marker = {
11776
12274
  managedBy: "@hasna/skills",
11777
- skill: record.skill,
11778
- source: record.source ?? "pull",
11779
- ...record.version ? { version: record.version } : {},
11780
- ...record.contentHash ? { contentHash: record.contentHash } : {},
11781
- ...record.sourceCommit ? { sourceCommit: record.sourceCommit } : {},
11782
- ...record.signature ? { signature: record.signature } : {},
11783
- ...record.revisionId ? { revisionId: record.revisionId } : {},
12275
+ skill: record3.skill,
12276
+ source: record3.source ?? "pull",
12277
+ ...record3.version ? { version: record3.version } : {},
12278
+ ...record3.contentHash ? { contentHash: record3.contentHash } : {},
12279
+ ...record3.sourceCommit ? { sourceCommit: record3.sourceCommit } : {},
12280
+ ...record3.signature ? { signature: record3.signature } : {},
12281
+ ...record3.revisionId ? { revisionId: record3.revisionId } : {},
11784
12282
  syncedAt: new Date().toISOString()
11785
12283
  };
11786
12284
  writeFileSync8(join17(dir, PULL_MARKER_FILE), `${JSON.stringify(marker, null, 2)}
@@ -11795,19 +12293,19 @@ async function safeMeta(client, slug) {
11795
12293
  }
11796
12294
  if (!raw || typeof raw !== "object")
11797
12295
  return null;
11798
- const record = raw;
11799
- const kind = record.kind === "instruction" || record.kind === "executable" ? record.kind : undefined;
11800
- const tags = Array.isArray(record.tags) ? record.tags.filter((tag) => typeof tag === "string" && tag.trim().length > 0) : undefined;
12296
+ const record3 = raw;
12297
+ const kind = record3.kind === "instruction" || record3.kind === "executable" ? record3.kind : undefined;
12298
+ const tags = Array.isArray(record3.tags) ? record3.tags.filter((tag) => typeof tag === "string" && tag.trim().length > 0) : undefined;
11801
12299
  return {
11802
- ...str(record.displayName) ? { displayName: str(record.displayName) } : {},
11803
- ...str(record.description) ? { description: str(record.description) } : {},
11804
- ...str(record.category) ? { category: str(record.category) } : {},
12300
+ ...str(record3.displayName) ? { displayName: str(record3.displayName) } : {},
12301
+ ...str(record3.description) ? { description: str(record3.description) } : {},
12302
+ ...str(record3.category) ? { category: str(record3.category) } : {},
11805
12303
  ...tags && tags.length ? { tags } : {},
11806
- ...str(record.version) ? { version: str(record.version) } : {},
12304
+ ...str(record3.version) ? { version: str(record3.version) } : {},
11807
12305
  ...kind ? { kind } : {},
11808
- ...REVISION_ID_PATTERN.test(str(record.revisionId) ?? "") ? { revisionId: str(record.revisionId) } : {},
11809
- ...typeof record.skillMd === "string" && record.skillMd.length > 0 ? { skillMd: record.skillMd } : {},
11810
- ...str(record.publishedSource) ? { publishedSource: str(record.publishedSource) } : {}
12306
+ ...REVISION_ID_PATTERN.test(str(record3.revisionId) ?? "") ? { revisionId: str(record3.revisionId) } : {},
12307
+ ...typeof record3.skillMd === "string" && record3.skillMd.length > 0 ? { skillMd: record3.skillMd } : {},
12308
+ ...str(record3.publishedSource) ? { publishedSource: str(record3.publishedSource) } : {}
11811
12309
  };
11812
12310
  }
11813
12311
  function pickCorpusOptions(options) {
@@ -11816,8 +12314,8 @@ function pickCorpusOptions(options) {
11816
12314
  function extractSlug(entry) {
11817
12315
  if (!entry || typeof entry !== "object")
11818
12316
  return;
11819
- const record = entry;
11820
- return str(record.slug) ?? str(record.name);
12317
+ const record3 = entry;
12318
+ return str(record3.slug) ?? str(record3.name);
11821
12319
  }
11822
12320
  function dedupe(values) {
11823
12321
  return [...new Set(values)];
@@ -11943,7 +12441,7 @@ import { dirname as dirname7, relative as relative4 } from "path";
11943
12441
  // package.json
11944
12442
  var package_default = {
11945
12443
  name: "@hasna/skills",
11946
- version: "0.4.0",
12444
+ version: "0.5.1",
11947
12445
  description: "Skills library for AI coding agents",
11948
12446
  type: "module",
11949
12447
  bin: {
@@ -11975,6 +12473,7 @@ var package_default = {
11975
12473
  files: [
11976
12474
  "dist/",
11977
12475
  "!dist/**/*.test.d.ts",
12476
+ "!dist/**/*.fixture.d.ts",
11978
12477
  "!dist/test-preload.d.ts",
11979
12478
  "!dist/platform",
11980
12479
  "bin/",
@@ -12000,8 +12499,9 @@ var package_default = {
12000
12499
  migrate: "bun run ./src/server/migrate.ts",
12001
12500
  typecheck: "tsc --noEmit",
12002
12501
  "verify:release": "bun run scripts/release-guard.ts",
12502
+ "verify:consumer-types": "bun run scripts/consumer-types.ts",
12003
12503
  prepare: "bun run build:js",
12004
- prepack: "bun run build && bun run verify:release",
12504
+ prepack: "bun run build && bun run verify:release && bun run verify:consumer-types",
12005
12505
  prepublishOnly: "bun run typecheck && bun run test"
12006
12506
  },
12007
12507
  keywords: [
@@ -12023,6 +12523,7 @@ var package_default = {
12023
12523
  author: "Hasna",
12024
12524
  license: "Apache-2.0",
12025
12525
  devDependencies: {
12526
+ "@hasna/contracts": "1.0.2",
12026
12527
  "@types/bun": "1.3.14",
12027
12528
  "@types/node": "25.2.3",
12028
12529
  "@types/react": "^18.2.0",
@@ -12034,8 +12535,7 @@ var package_default = {
12034
12535
  dependencies: {
12035
12536
  "@aws-sdk/client-ecs": "^3.1079.0",
12036
12537
  "@aws-sdk/client-s3": "^3.1079.0",
12037
- "@hasna/contracts": "1.0.1",
12038
- "@hasna/events": "0.1.16",
12538
+ "@hasna/events": "0.1.18",
12039
12539
  "@modelcontextprotocol/sdk": "^1.26.0",
12040
12540
  chalk: "^5.3.0",
12041
12541
  commander: "^12.1.0",
@@ -12540,6 +13040,104 @@ var toolContracts = [
12540
13040
  dependencies: objectSchema({}, [], "Package dependencies.", true)
12541
13041
  })
12542
13042
  },
13043
+ {
13044
+ name: "update_account_profile",
13045
+ title: "Update Account Display Name",
13046
+ description: "Update your display name with fresh email verification on the configured server.",
13047
+ params: ["name", "email", "code"],
13048
+ category: "execution",
13049
+ sideEffects: "local-process-or-remote-run",
13050
+ stable: true,
13051
+ inputSchema: objectSchema({ name: stringSchema("Display name, 1\u2013100 characters"), email: { type: "string", format: "email" }, code: { type: "string", pattern: "^\\d{6}$" } }, ["name", "email", "code"]),
13052
+ 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"])
13053
+ },
13054
+ {
13055
+ name: "update_workspace_name",
13056
+ title: "Update Workspace Name",
13057
+ description: "Update the current workspace name as an owner/admin with fresh email verification.",
13058
+ params: ["name", "email", "code"],
13059
+ category: "execution",
13060
+ sideEffects: "local-process-or-remote-run",
13061
+ stable: true,
13062
+ inputSchema: objectSchema({ name: stringSchema("Workspace name, 1\u2013100 characters"), email: { type: "string", format: "email" }, code: { type: "string", pattern: "^\\d{6}$" } }, ["name", "email", "code"]),
13063
+ outputSchema: objectSchema({ organization: objectSchema({ id: stringSchema("Workspace identifier."), slug: stringSchema("Stable workspace slug."), name: stringSchema("Workspace name.") }, ["id", "slug", "name"]) }, ["organization"])
13064
+ },
13065
+ {
13066
+ name: "set_workspace_member_role",
13067
+ title: "Set Current Workspace Member Role",
13068
+ description: "Set an exact membership incarnation's role with its observed expectedRole and fresh verification; no automatic retry.",
13069
+ params: ["membershipId", "role", "expectedRole", "email", "code"],
13070
+ category: "execution",
13071
+ sideEffects: "local-process-or-remote-run",
13072
+ stable: true,
13073
+ inputSchema: objectSchema({
13074
+ membershipId: { type: "string", pattern: "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$" },
13075
+ role: { type: "string", enum: ["owner", "admin", "member", "viewer"] },
13076
+ expectedRole: { type: "string", enum: ["owner", "admin", "member", "viewer"] },
13077
+ email: { type: "string", format: "email" },
13078
+ code: { type: "string", pattern: "^\\d{6}$" }
13079
+ }, ["membershipId", "role", "expectedRole", "email", "code"]),
13080
+ outputSchema: objectSchema({
13081
+ organizationId: stringSchema("Current workspace identifier."),
13082
+ changed: { type: "boolean" },
13083
+ member: objectSchema({
13084
+ membershipId: stringSchema("Membership incarnation."),
13085
+ userId: stringSchema("Account identifier."),
13086
+ email: stringSchema("Member email."),
13087
+ displayName: { oneOf: [{ type: "string" }, { type: "null" }] },
13088
+ role: { type: "string", enum: ["owner", "admin", "member", "viewer"] },
13089
+ createdAt: stringSchema("Exact server timestamp including microseconds.")
13090
+ }, ["membershipId", "userId", "email", "displayName", "role", "createdAt"])
13091
+ }, ["organizationId", "member", "changed"])
13092
+ },
13093
+ {
13094
+ name: "remove_workspace_member",
13095
+ title: "Remove Current Workspace Member",
13096
+ description: "Remove exactly this membership incarnation using its observed expectedRole and fresh verification; self-removal is unavailable.",
13097
+ params: ["membershipId", "expectedRole", "email", "code"],
13098
+ category: "execution",
13099
+ sideEffects: "local-process-or-remote-run",
13100
+ stable: true,
13101
+ inputSchema: objectSchema({
13102
+ membershipId: { type: "string", pattern: "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$" },
13103
+ expectedRole: { type: "string", enum: ["owner", "admin", "member", "viewer"] },
13104
+ email: { type: "string", format: "email" },
13105
+ code: { type: "string", pattern: "^\\d{6}$" }
13106
+ }, ["membershipId", "expectedRole", "email", "code"]),
13107
+ outputSchema: objectSchema({
13108
+ organizationId: stringSchema("Current workspace identifier."),
13109
+ membershipId: stringSchema("Removed membership incarnation."),
13110
+ removed: { type: "boolean", const: true },
13111
+ alreadyRemoved: { type: "boolean" }
13112
+ }, ["organizationId", "membershipId", "removed", "alreadyRemoved"])
13113
+ },
13114
+ {
13115
+ name: "list_workspace_members",
13116
+ title: "List Current Workspace Members",
13117
+ description: "Read one current-workspace roster page with fresh owner/admin email verification; saved credentials stay unchanged.",
13118
+ params: ["email", "code", "limit?", "cursor?"],
13119
+ category: "execution",
13120
+ sideEffects: "local-process-or-remote-run",
13121
+ stable: true,
13122
+ inputSchema: objectSchema({
13123
+ email: { type: "string", format: "email" },
13124
+ code: { type: "string", pattern: "^\\d{6}$" },
13125
+ limit: { type: "integer", minimum: 1, maximum: 100 },
13126
+ cursor: { type: "string", pattern: "^[A-Za-z0-9_-]{1,512}$" }
13127
+ }, ["email", "code"]),
13128
+ outputSchema: objectSchema({
13129
+ organizationId: stringSchema("Current workspace identifier."),
13130
+ members: arraySchema(objectSchema({
13131
+ membershipId: stringSchema("Membership incarnation."),
13132
+ userId: stringSchema("Account identifier."),
13133
+ email: stringSchema("Member email."),
13134
+ displayName: { oneOf: [{ type: "string" }, { type: "null" }] },
13135
+ role: { type: "string", enum: ["owner", "admin", "member", "viewer"] },
13136
+ createdAt: stringSchema("Exact server timestamp including microseconds.")
13137
+ }, ["membershipId", "userId", "email", "displayName", "role", "createdAt"])),
13138
+ nextCursor: { oneOf: [{ type: "string" }, { type: "null" }] }
13139
+ }, ["organizationId", "members", "nextCursor"])
13140
+ },
12543
13141
  {
12544
13142
  name: "list_api_keys",
12545
13143
  title: "List API Keys",
@@ -13354,7 +13952,7 @@ class SkillsPostgresSyncStore {
13354
13952
  }
13355
13953
  async upsertRecords(records) {
13356
13954
  let count = 0;
13357
- for (const record of records) {
13955
+ for (const record3 of records) {
13358
13956
  await this.client.query([
13359
13957
  "INSERT INTO skills_sync_records",
13360
13958
  "(scope, kind, id, updated_at, deleted_at, source, payload)",
@@ -13365,13 +13963,13 @@ class SkillsPostgresSyncStore {
13365
13963
  "source = EXCLUDED.source,",
13366
13964
  "payload = EXCLUDED.payload"
13367
13965
  ].join(" "), [
13368
- record.scope,
13369
- record.kind,
13370
- record.id,
13371
- record.updatedAt,
13372
- record.deletedAt ?? null,
13373
- record.source ?? null,
13374
- JSON.stringify(record.payload)
13966
+ record3.scope,
13967
+ record3.kind,
13968
+ record3.id,
13969
+ record3.updatedAt,
13970
+ record3.deletedAt ?? null,
13971
+ record3.source ?? null,
13972
+ JSON.stringify(record3.payload)
13375
13973
  ]);
13376
13974
  count += 1;
13377
13975
  }
@@ -14350,13 +14948,13 @@ async function requestAuthApi(instance, path, options) {
14350
14948
  apiUrl: safeUrl
14351
14949
  });
14352
14950
  }
14353
- const text = await res.text();
14354
- const body = text ? parseJsonBody(text) : {};
14951
+ const text2 = await res.text();
14952
+ const body = text2 ? parseJsonBody(text2) : {};
14355
14953
  if (!res.ok) {
14356
- const record = isRecord4(body) ? body : {};
14357
- const detail = typeof record.detail === "string" ? record.detail : undefined;
14358
- const error = typeof record.error === "string" ? record.error : undefined;
14359
- const code = typeof record.code === "string" ? record.code : undefined;
14954
+ const record3 = isRecord5(body) ? body : {};
14955
+ const detail = typeof record3.detail === "string" ? record3.detail : undefined;
14956
+ const error = typeof record3.error === "string" ? record3.error : undefined;
14957
+ const code = typeof record3.code === "string" ? record3.code : undefined;
14360
14958
  throw new HostedApiError(detail || error || `${res.status} ${res.statusText}`, {
14361
14959
  status: res.status,
14362
14960
  code,
@@ -14367,21 +14965,21 @@ async function requestAuthApi(instance, path, options) {
14367
14965
  }
14368
14966
  return body;
14369
14967
  }
14370
- function parseJsonBody(text) {
14968
+ function parseJsonBody(text2) {
14371
14969
  try {
14372
- return JSON.parse(text);
14970
+ return JSON.parse(text2);
14373
14971
  } catch {
14374
- return { detail: condenseErrorBody(text) };
14972
+ return { detail: condenseErrorBody(text2) };
14375
14973
  }
14376
14974
  }
14377
- function condenseErrorBody(text) {
14378
- const stripped = /<[a-z!/]/i.test(text) ? text.replace(/<(script|style)[\s\S]*?<\/\1>/gi, " ").replace(/<[^>]*>/g, " ") : text;
14975
+ function condenseErrorBody(text2) {
14976
+ const stripped = /<[a-z!/]/i.test(text2) ? text2.replace(/<(script|style)[\s\S]*?<\/\1>/gi, " ").replace(/<[^>]*>/g, " ") : text2;
14379
14977
  const collapsed = stripped.replace(/\s+/g, " ").trim();
14380
14978
  if (collapsed.length <= MAX_ERROR_DETAIL_LENGTH)
14381
14979
  return collapsed;
14382
14980
  return `${collapsed.slice(0, MAX_ERROR_DETAIL_LENGTH - 1).trimEnd()}\u2026`;
14383
14981
  }
14384
- function isRecord4(value) {
14982
+ function isRecord5(value) {
14385
14983
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
14386
14984
  }
14387
14985
 
@@ -14402,22 +15000,90 @@ class RemoteSkillsAuthClient {
14402
15000
  pollDevice(deviceCode) {
14403
15001
  return this.request("/api/auth/device/token", { method: "POST", body: JSON.stringify({ deviceCode }) });
14404
15002
  }
14405
- async sessionClient(email, code) {
15003
+ async sessionClient(email, code, context) {
15004
+ if (context !== undefined) {
15005
+ const target = workspaceContext(context), apiOrigin2 = this.apiOrigin;
15006
+ const session = await this.switchWorkspace(email, code, target);
15007
+ return new RemoteSkillsClient(session.token, apiOrigin2);
15008
+ }
15009
+ const apiOrigin = this.apiOrigin;
14406
15010
  if (!email.includes("@") || !/^\d{6}$/.test(code))
14407
- throw new Error("Fresh email and six-digit verification code are required to manage API keys");
15011
+ throw new Error("Fresh email and six-digit verification code are required to manage this account");
14408
15012
  const login = await this.verifyCode(email, code);
14409
15013
  if (!login || typeof login.token !== "string" || !login.token)
14410
15014
  throw new Error("The server did not return an authorized account session");
14411
- return new RemoteSkillsClient(login.token, this.apiOrigin);
15015
+ return new RemoteSkillsClient(login.token, apiOrigin);
15016
+ }
15017
+ async listAccountWorkspaces(email, code, expectedUserId) {
15018
+ const login = await this.workspaceLogin(email, code, expectedUserId);
15019
+ const result = await new RemoteSkillsClient(login.token, login.apiOrigin).listAccountWorkspaces(login.userId);
15020
+ return { userId: login.userId, ...result };
15021
+ }
15022
+ async switchWorkspace(email, code, context) {
15023
+ const target = workspaceContext(context);
15024
+ const login = await this.workspaceLogin(email, code, target.userId);
15025
+ return new RemoteSkillsClient(login.token, login.apiOrigin).switchWorkspace(target);
15026
+ }
15027
+ async workspaceLogin(email, code, expectedUserId) {
15028
+ const expected = expectedUserId === undefined ? undefined : workspaceExpectedUserId(expectedUserId);
15029
+ const apiOrigin = this.apiOrigin;
15030
+ if (typeof email !== "string" || !email.includes("@") || typeof code !== "string" || !/^\d{6}$/.test(code))
15031
+ throw new Error("Fresh email and six-digit verification code are required to manage this account");
15032
+ let response;
15033
+ try {
15034
+ response = await fetch(`${apiOrigin}/api/auth/verify`, {
15035
+ method: "POST",
15036
+ redirect: "error",
15037
+ credentials: "omit",
15038
+ signal: AbortSignal.timeout(15000),
15039
+ headers: { "Content-Type": "application/json" },
15040
+ body: JSON.stringify({ email, code })
15041
+ });
15042
+ } catch {
15043
+ throw new HostedApiError("Unable to verify the Skills account.");
15044
+ }
15045
+ if (!response.ok) {
15046
+ response.body?.cancel().catch(() => {});
15047
+ throw new HostedApiError("Unable to verify the Skills account.", { status: response.status });
15048
+ }
15049
+ let value;
15050
+ try {
15051
+ value = JSON.parse(new TextDecoder().decode(await readBoundedResponse(response, 64 * 1024)));
15052
+ } catch {
15053
+ throw new HostedApiError("The server returned an invalid account verification result.");
15054
+ }
15055
+ return { ...parseWorkspaceLogin(value, expected), apiOrigin };
15056
+ }
15057
+ async createApiKey(email, code, name, scopes, context) {
15058
+ const capturedScopes = scopes === undefined ? undefined : [...scopes];
15059
+ return (await this.sessionClient(email, code, context)).createApiKey(name, capturedScopes);
15060
+ }
15061
+ async listApiKeys(email, code, context) {
15062
+ return (await this.sessionClient(email, code, context)).listApiKeys();
15063
+ }
15064
+ async revokeApiKey(email, code, keyId, context) {
15065
+ return (await this.sessionClient(email, code, context)).revokeApiKey(keyId);
15066
+ }
15067
+ async updateProfile(email, code, input, context) {
15068
+ const body = customerNamePatch(input, "displayName");
15069
+ return (await this.sessionClient(email, code, context)).updateProfile({ displayName: body.displayName });
15070
+ }
15071
+ async updateCurrentWorkspace(email, code, input, context) {
15072
+ const body = customerNamePatch(input, "name");
15073
+ return (await this.sessionClient(email, code, context)).updateCurrentWorkspace({ name: body.name });
14412
15074
  }
14413
- async createApiKey(email, code, name, scopes) {
14414
- return (await this.sessionClient(email, code)).createApiKey(name, scopes);
15075
+ async listWorkspaceMembers(email, code, options = {}, context) {
15076
+ workspaceMembersQuery(options);
15077
+ const captured = { ...options };
15078
+ return (await this.sessionClient(email, code, context)).listWorkspaceMembers(captured);
14415
15079
  }
14416
- async listApiKeys(email, code) {
14417
- return (await this.sessionClient(email, code)).listApiKeys();
15080
+ async setWorkspaceMemberRole(email, code, membershipId, input, context) {
15081
+ const captured = workspaceMemberRoleInput(membershipId, input);
15082
+ return (await this.sessionClient(email, code, context)).setWorkspaceMemberRole(captured.membershipId, captured.body);
14418
15083
  }
14419
- async revokeApiKey(email, code, keyId) {
14420
- return (await this.sessionClient(email, code)).revokeApiKey(keyId);
15084
+ async removeWorkspaceMember(email, code, membershipId, input, context) {
15085
+ const captured = workspaceMemberRemovalInput(membershipId, input);
15086
+ return (await this.sessionClient(email, code, context)).removeWorkspaceMember(captured.membershipId, captured.body);
14421
15087
  }
14422
15088
  request(path, options) {
14423
15089
  if (!["/api/auth/login", "/api/auth/verify", "/api/auth/device/start", "/api/auth/device/token", "/api/auth/keys", "/api/auth/whoami"].includes(path))
@@ -14461,6 +15127,7 @@ export {
14461
15127
  sha256File,
14462
15128
  setSkillDisabled,
14463
15129
  setScheduleEnabled,
15130
+ selectsSkillsLocalMode,
14464
15131
  searchSkills,
14465
15132
  scaffoldPortableSkill,
14466
15133
  saveProjectConfig,
@@ -14521,6 +15188,7 @@ export {
14521
15188
  listPinnedSkills,
14522
15189
  listMcpToolContracts,
14523
15190
  isSyncAgent,
15191
+ isSkillsLocalOptIn,
14524
15192
  isRegularFile,
14525
15193
  isPortableWithinSkill,
14526
15194
  isGatewayBackedSkill,
@@ -14605,6 +15273,8 @@ export {
14605
15273
  agentGlobalSkillsDir,
14606
15274
  addSchedule,
14607
15275
  adaptSkillMdForAgent,
15276
+ WorkspaceIdentityMismatchError,
15277
+ WorkspaceContextInputError,
14608
15278
  TOOL_PRIMITIVE_SCHEMA_VERSION,
14609
15279
  TOOL_PRIMITIVES,
14610
15280
  StationSnapshotError,
@@ -14628,6 +15298,7 @@ export {
14628
15298
  SKILLS_PROJECT_DIR,
14629
15299
  SKILLS_NATIVE_STORAGE_FALLBACK_ENV,
14630
15300
  SKILLS_NATIVE_STORAGE_ENV,
15301
+ SKILLS_LOCAL_OPT_IN_ENV_KEYS,
14631
15302
  SKILLS_CLI_MCP_PARITY,
14632
15303
  SKILLS_APP,
14633
15304
  SKILLS_API_URL_ENV_KEYS,
@@ -14635,11 +15306,14 @@ export {
14635
15306
  SKILLS_API_KEY_ENV_KEYS,
14636
15307
  SKILLS_API_KEY_ENV,
14637
15308
  SKILLS,
15309
+ RemoteWorkspaceSelectionError,
15310
+ RemoteWorkspaceMemberError,
14638
15311
  RemoteSkillsClient,
14639
15312
  RemoteSkillsAuthClient,
14640
15313
  RemoteRouteUnsupportedError,
14641
15314
  RemoteRequestError,
14642
15315
  RemoteCreditApprovalError,
15316
+ RemoteCapabilityUnavailableError,
14643
15317
  REMOTE_SKILL_RUN_CONTRACT_VERSION,
14644
15318
  REFUSED_SCANNER_FLAGGED,
14645
15319
  PullSkillError,