@hasna/skills 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -674,7 +674,7 @@ skills/ # Public skill contracts and local OSS skills
674
674
  |---|---|---|
675
675
  | Catalog skills | 86 | `SKILLS.length` (`src/lib/registry-data/`) |
676
676
  | Categories | 17 | `CATEGORIES` (`src/lib/registry-types.ts`) |
677
- | MCP tools | 61 | `tools/list` against a live `buildServer()` |
677
+ | MCP tools | 62 | `tools/list` against a live `buildServer()` |
678
678
 
679
679
  Every number in this table is re-derived from the source tree on each test run by
680
680
  `src/lib/readme-derived-counts.test.ts`, so a drifted figure fails a test rather
@@ -933,3 +933,30 @@ through a tool remains a separate follow-up.
933
933
  If enrollment reports that key issuance was attempted but not confirmed, inspect
934
934
  the selected profile and workspace keys before retrying. A lost response can
935
935
  still have created a server key; the CLI does not retry issuance automatically.
936
+
937
+ ### Leave a workspace
938
+
939
+ Use the exact membership ID and observed role from fresh `workspace list` output.
940
+ Leaving requires deliberate confirmation and a fresh verification code:
941
+
942
+ ```sh
943
+ HASNA_PROFILE=team-b skills workspace leave <membership-id> --expected-role member --email you@example.com --code-stdin --confirm --json
944
+ ```
945
+
946
+ The selected profile must authenticate the same membership. Without a named
947
+ profile, supply `--user-id <user-id>` from discovery; this also supports viewers
948
+ who cannot create API keys. The server refuses stale roles, the last active
949
+ owner, and leaving your last available workspace. It decides authority atomically.
950
+
951
+ The SDK offers `RemoteSkillsAuthClient.leaveWorkspace(email, code,
952
+ { userId, membershipId }, { expectedRole, confirm: true })`; an existing interactive
953
+ session can use `RemoteSkillsClient.leaveWorkspace(context, input)`. MCP exposes
954
+ `leave_workspace` with those same explicit IDs, role, confirmation and fresh code.
955
+ All surfaces call the same HTTP method once. API keys cannot authorize the leave.
956
+
957
+ Success returns `{ organizationId, membershipId, removed: true,
958
+ signInRequired: true }`. Sign in again to an available workspace afterwards.
959
+ Saved credentials and unrelated profiles remain unchanged; credentials for the
960
+ left membership no longer grant access. A lost or invalid response raises
961
+ `RemoteWorkspaceLeaveUnconfirmedError`: inspect available memberships before any
962
+ new action. Never automatically retry or substitute another membership ID.
package/bin/index.js CHANGED
@@ -36860,7 +36860,7 @@ var package_default;
36860
36860
  var init_package = __esm(() => {
36861
36861
  package_default = {
36862
36862
  name: "@hasna/skills",
36863
- version: "0.5.1",
36863
+ version: "0.5.2",
36864
36864
  description: "Skills library for AI coding agents",
36865
36865
  type: "module",
36866
36866
  bin: {
@@ -49235,6 +49235,75 @@ var init_remote_workspace = __esm(() => {
49235
49235
  };
49236
49236
  });
49237
49237
 
49238
+ // src/lib/remote-workspace-leave.ts
49239
+ function workspaceLeaveInput(context, input) {
49240
+ const target = workspaceContext(context);
49241
+ if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).sort().join(",") !== "confirm,expectedRole" || input.confirm !== true)
49242
+ throw new WorkspaceLeaveInputError;
49243
+ const captured = workspaceMemberRemovalInput(target.membershipId, { expectedRole: input.expectedRole });
49244
+ return {
49245
+ context: target,
49246
+ input: { expectedRole: captured.body.expectedRole, confirm: true },
49247
+ body: { membershipId: target.membershipId, expectedRole: captured.body.expectedRole }
49248
+ };
49249
+ }
49250
+ function workspaceLeaveFailure(value, status) {
49251
+ if (!value || typeof value !== "object" || Array.isArray(value))
49252
+ return null;
49253
+ const code = value.code;
49254
+ return typeof code === "string" && Object.hasOwn(workspaceLeaveFailures, code) && workspaceLeaveFailures[code][0] === status ? code : null;
49255
+ }
49256
+ function parseWorkspaceLeaveResult(value, membershipId, organizationId) {
49257
+ const row = value;
49258
+ if (!row || typeof row !== "object" || Array.isArray(row) || row.membershipId !== membershipId || row.organizationId !== organizationId || row.removed !== true || row.signInRequired !== true)
49259
+ throw new RemoteWorkspaceLeaveUnconfirmedError;
49260
+ return { membershipId, organizationId, removed: true, signInRequired: true };
49261
+ }
49262
+ function workspaceLeaveProfileContext(membershipId, userId, profile) {
49263
+ const observed = profile === undefined ? undefined : workspaceContext(profile);
49264
+ const target = workspaceContext({ userId: userId ?? observed?.userId, membershipId });
49265
+ if (observed && (observed.userId !== target.userId || observed.membershipId !== target.membershipId))
49266
+ throw new WorkspaceIdentityMismatchError;
49267
+ return target;
49268
+ }
49269
+ var WorkspaceLeaveInputError, workspaceLeaveFailures, RemoteWorkspaceLeaveError, RemoteWorkspaceLeaveUnconfirmedError;
49270
+ var init_remote_workspace_leave = __esm(() => {
49271
+ init_remote_workspace_selection();
49272
+ init_remote_workspace();
49273
+ WorkspaceLeaveInputError = class WorkspaceLeaveInputError extends Error {
49274
+ constructor() {
49275
+ super("Confirm leaving the exact observed user and membership with its expected role.");
49276
+ this.name = "WorkspaceLeaveInputError";
49277
+ }
49278
+ };
49279
+ workspaceLeaveFailures = {
49280
+ INVALID_REQUEST: [400, "Provide the exact current membership and expected role."],
49281
+ ACCOUNT_UNAVAILABLE: [403, "Account is unavailable."],
49282
+ INTERACTIVE_SESSION_REQUIRED: [403, "Fresh interactive sign-in is required to leave a workspace."],
49283
+ MEMBERSHIP_ROLE_CHANGED: [409, "Your role changed. Sign in and inspect the workspace before leaving."],
49284
+ LAST_OWNER_REQUIRED: [409, "The workspace must retain another active owner."],
49285
+ LAST_WORKSPACE_REQUIRED: [409, "Another available workspace is required before leaving."],
49286
+ MEMBERSHIP_BUSY: [503, "Membership is busy. Inspect the workspace before another leave action."]
49287
+ };
49288
+ RemoteWorkspaceLeaveError = class RemoteWorkspaceLeaveError extends Error {
49289
+ code;
49290
+ status;
49291
+ constructor(code) {
49292
+ super(workspaceLeaveFailures[code][1]);
49293
+ this.code = code;
49294
+ this.name = "RemoteWorkspaceLeaveError";
49295
+ this.status = workspaceLeaveFailures[code][0];
49296
+ }
49297
+ };
49298
+ RemoteWorkspaceLeaveUnconfirmedError = class RemoteWorkspaceLeaveUnconfirmedError extends Error {
49299
+ code = "WORKSPACE_LEAVE_UNCONFIRMED";
49300
+ constructor() {
49301
+ super("The leave outcome is unconfirmed. Sign in again and inspect available memberships before another action. Do not retry automatically; saved credentials are unchanged.");
49302
+ this.name = "RemoteWorkspaceLeaveUnconfirmedError";
49303
+ }
49304
+ };
49305
+ });
49306
+
49238
49307
  // src/lib/auth-store.ts
49239
49308
  import { chmodSync, existsSync as existsSync17, mkdirSync as mkdirSync7, readFileSync as readFileSync12, renameSync as renameSync4, statSync as statSync9, unlinkSync, writeFileSync as writeFileSync7 } from "fs";
49240
49309
  import { basename as basename4, dirname as dirname6, join as join18 } from "path";
@@ -49888,6 +49957,30 @@ class RemoteSkillsClient {
49888
49957
  }
49889
49958
  return value;
49890
49959
  }
49960
+ async leaveWorkspace(context, input) {
49961
+ const captured = workspaceLeaveInput(context, input);
49962
+ const connection = new RemoteSkillsClient(this.apiKey, this.apiUrl);
49963
+ const value = await connection.requestWorkspaceSelection("/api/auth/whoami");
49964
+ if (!value || typeof value !== "object" || value.authMethod !== "jwt")
49965
+ throw new RemoteWorkspaceLeaveError("INTERACTIVE_SESSION_REQUIRED");
49966
+ const identity2 = parseWorkspaceIdentity(value, captured.context.userId);
49967
+ if (identity2.user.membershipId !== captured.context.membershipId)
49968
+ throw new WorkspaceIdentityMismatchError;
49969
+ let response, body;
49970
+ try {
49971
+ response = await connection.request("/api/v1/account/workspaces/leave", { method: "POST", body: JSON.stringify(captured.body) });
49972
+ body = JSON.parse(new TextDecoder().decode(await readBoundedResponse(response, 4096)));
49973
+ } catch {
49974
+ throw new RemoteWorkspaceLeaveUnconfirmedError;
49975
+ }
49976
+ if (!response.ok) {
49977
+ const code = workspaceLeaveFailure(body, response.status);
49978
+ if (code)
49979
+ throw new RemoteWorkspaceLeaveError(code);
49980
+ throw new RemoteWorkspaceLeaveUnconfirmedError;
49981
+ }
49982
+ return parseWorkspaceLeaveResult(body, captured.context.membershipId, identity2.organization.id);
49983
+ }
49891
49984
  async listApiKeys() {
49892
49985
  return this.arrayResponse("/api/auth/keys");
49893
49986
  }
@@ -50277,6 +50370,7 @@ function createRemoteSkillsClientReadOnly(env3 = process.env) {
50277
50370
  }
50278
50371
  var RemoteRouteUnsupportedError, RemoteRequestError, RemoteWorkspaceMemberError, RemoteWorkspaceSelectionError, RemoteCapabilityUnavailableError, INVALID_SKILL_VERSION_RESPONSE = "Remote skill version payload did not match the expected contract.";
50279
50372
  var init_remote_client = __esm(() => {
50373
+ init_remote_workspace_leave();
50280
50374
  init_remote_workspace_selection();
50281
50375
  init_remote_workspace();
50282
50376
  init_remote_workspace();
@@ -73694,6 +73788,10 @@ class RemoteSkillsAuthClient {
73694
73788
  const captured = workspaceMemberRoleInput(membershipId, input);
73695
73789
  return (await this.sessionClient(email2, code, context)).setWorkspaceMemberRole(captured.membershipId, captured.body);
73696
73790
  }
73791
+ async leaveWorkspace(email2, code, context, input) {
73792
+ const captured = workspaceLeaveInput(context, input);
73793
+ return (await this.sessionClient(email2, code, captured.context)).leaveWorkspace(captured.context, captured.input);
73794
+ }
73697
73795
  async removeWorkspaceMember(email2, code, membershipId, input, context) {
73698
73796
  const captured = workspaceMemberRemovalInput(membershipId, input);
73699
73797
  return (await this.sessionClient(email2, code, context)).removeWorkspaceMember(captured.membershipId, captured.body);
@@ -73706,6 +73804,7 @@ class RemoteSkillsAuthClient {
73706
73804
  }
73707
73805
  var MAX_ERROR_DETAIL_LENGTH = 200, HostedApiError;
73708
73806
  var init_remote_auth = __esm(() => {
73807
+ init_remote_workspace_leave();
73709
73808
  init_remote_workspace_selection();
73710
73809
  init_remote_files();
73711
73810
  init_remote_workspace();
@@ -73957,6 +74056,18 @@ function registerRemoteCustomerTools(server) {
73957
74056
  email: exports_external.string().email(),
73958
74057
  code: exports_external.string().regex(/^\d{6}$/)
73959
74058
  };
74059
+ server.registerTool("leave_workspace", {
74060
+ title: "Leave Current Workspace",
74061
+ description: "Leave exactly the observed user and membership with confirm=true and fresh verification. The server enforces last-owner and last-workspace safeguards. Sign in again afterwards; no automatic retry or saved-profile deletion.",
74062
+ annotations: { destructiveHint: true, idempotentHint: false, readOnlyHint: false },
74063
+ inputSchema: exports_external.object({ ...memberInput, userId: memberInput.membershipId, confirm: exports_external.literal(true) }).strict()
74064
+ }, async ({ membershipId, userId, expectedRole, email: email2, code, confirm }) => {
74065
+ try {
74066
+ return mcpJson(await freshAccount("Leave workspace", (client, context) => client.leaveWorkspace(email2, code, workspaceLeaveProfileContext(membershipId, userId, context), { expectedRole, confirm })));
74067
+ } catch (error2) {
74068
+ return error2 instanceof RemoteWorkspaceLeaveError || error2 instanceof RemoteWorkspaceLeaveUnconfirmedError ? mcpError(error2.code, error2.message) : mcpError("WORKSPACE_LEAVE_UNCONFIRMED", "Leaving could not be confirmed. Check the selected profile and exact membership, sign in again and inspect available workspaces before another action. Do not retry automatically; saved credentials are unchanged.");
74069
+ }
74070
+ });
73960
74071
  server.registerTool("set_workspace_member_role", {
73961
74072
  title: "Set Current Workspace Member Role",
73962
74073
  description: "Change exactly this membership incarnation with its observed expectedRole and fresh verification. The server enforces owner/admin policy. No automatic refresh or retry; saved credentials stay unchanged.",
@@ -74094,6 +74205,7 @@ var init_remote_customer_tools = __esm(() => {
74094
74205
  init_workspace_profile();
74095
74206
  init_remote_customer_operations();
74096
74207
  init_remote_client();
74208
+ init_remote_workspace_leave();
74097
74209
  init_helpers();
74098
74210
  });
74099
74211
 
@@ -78485,6 +78597,53 @@ var init_auth = __esm(() => {
78485
78597
  CONFIG_HINT_STATUSES = new Set([401, 403, 404, 405, 501]);
78486
78598
  });
78487
78599
 
78600
+ // src/cli/commands/workspace-leave.ts
78601
+ function registerWorkspaceLeaveCommand(workspace) {
78602
+ workspace.command("leave <membership-id>").allowExcessArguments(false).description("Leave exactly this membership after fresh verification; saved profiles stay unchanged").requiredOption("--expected-role <role>", "Observed role: owner, admin, member or viewer").requiredOption("--email <email>", "Account email for fresh verification").requiredOption("--confirm", "Confirm losing access through this membership and signing in again").option("--user-id <id>", "Observed user ID; required without a named workspace profile").option("--code-stdin", "Read a previously requested six-digit verification code from stdin").option("--json", "Output the confirmed result as JSON").action(async (membershipId, options) => {
78603
+ try {
78604
+ if (options.confirm !== true)
78605
+ throw new WorkspaceLeaveInputError;
78606
+ const pending = prepareProfileWorkspace("Leave workspace");
78607
+ const target = await pending.resolve();
78608
+ const captured = workspaceLeaveInput(workspaceLeaveProfileContext(membershipId, options.userId, target.context), { expectedRole: options.expectedRole, confirm: true });
78609
+ if (!options.email.includes("@"))
78610
+ throw new NameInputError("Provide the verified account email.");
78611
+ if (!options.codeStdin && (options.json || !process.stdin.isTTY || !process.stderr.isTTY))
78612
+ throw new NameInputError("Use --code-stdin with a fresh verification code for noninteractive leave actions.");
78613
+ const client = new RemoteSkillsAuthClient(target.origin);
78614
+ let code;
78615
+ if (options.codeStdin)
78616
+ code = await readCode();
78617
+ else {
78618
+ await client.requestCode(options.email);
78619
+ code = await promptCode();
78620
+ }
78621
+ if (code === null)
78622
+ return;
78623
+ target.unchanged();
78624
+ const result2 = await client.leaveWorkspace(options.email, code, captured.context, captured.input);
78625
+ if (options.json)
78626
+ console.log(JSON.stringify(result2));
78627
+ else
78628
+ console.log("Workspace membership left. Sign in again to an available workspace. Saved credentials are unchanged; this membership's credentials no longer grant access.");
78629
+ } catch (error2) {
78630
+ const known = error2 instanceof RemoteWorkspaceLeaveError || error2 instanceof RemoteWorkspaceLeaveUnconfirmedError;
78631
+ const message = known || error2 instanceof WorkspaceLeaveInputError || error2 instanceof NameInputError ? error2.message : "Leaving could not be confirmed. Check the selected profile and exact membership, sign in again and inspect available workspaces before another action. Do not retry automatically; saved credentials are unchanged.";
78632
+ if (options.json)
78633
+ console.log(JSON.stringify({ error: message, ...known ? { code: error2.code } : {} }));
78634
+ else
78635
+ console.error(message);
78636
+ process.exitCode = 1;
78637
+ }
78638
+ });
78639
+ }
78640
+ var init_workspace_leave = __esm(() => {
78641
+ init_workspace_profile();
78642
+ init_remote_auth();
78643
+ init_remote_workspace_leave();
78644
+ init_customer_verification();
78645
+ });
78646
+
78488
78647
  // src/cli/commands/workspace-members.ts
78489
78648
  function registerWorkspaceMembersCommand(workspace) {
78490
78649
  workspace.command("members").allowExcessArguments(false).description("List the current workspace roster with fresh owner/admin verification").requiredOption("--email <email>", "Account email for fresh verification").option("--code-stdin", "Read a previously requested six-digit verification code from stdin").option("--limit <count>", "Page size from 1 to 100 (server default: 50)").option("--cursor <cursor>", "Unchanged nextCursor from the preceding page").option("--json", "Output the complete page as JSON").action(async (options) => {
@@ -78609,6 +78768,7 @@ function registerCustomerProfileCommands(program2) {
78609
78768
  registerWorkspaceListCommand(workspace);
78610
78769
  registerWorkspaceMembersCommand(workspace);
78611
78770
  registerWorkspaceMemberMutationCommands(workspace);
78771
+ registerWorkspaceLeaveCommand(workspace);
78612
78772
  const commands = [
78613
78773
  { kind: "account", command: account.command("update") },
78614
78774
  { kind: "workspace", command: workspace.command("update") }
@@ -78655,6 +78815,7 @@ var init_customer_profile = __esm(() => {
78655
78815
  init_workspace_selection();
78656
78816
  init_remote_auth();
78657
78817
  init_customer_verification();
78818
+ init_workspace_leave();
78658
78819
  init_workspace_members();
78659
78820
  init_workspace_member_mutations();
78660
78821
  });
package/bin/mcp.js CHANGED
@@ -8490,6 +8490,75 @@ var init_remote_workspace = __esm(() => {
8490
8490
  };
8491
8491
  });
8492
8492
 
8493
+ // src/lib/remote-workspace-leave.ts
8494
+ function workspaceLeaveInput(context, input) {
8495
+ const target = workspaceContext(context);
8496
+ if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).sort().join(",") !== "confirm,expectedRole" || input.confirm !== true)
8497
+ throw new WorkspaceLeaveInputError;
8498
+ const captured = workspaceMemberRemovalInput(target.membershipId, { expectedRole: input.expectedRole });
8499
+ return {
8500
+ context: target,
8501
+ input: { expectedRole: captured.body.expectedRole, confirm: true },
8502
+ body: { membershipId: target.membershipId, expectedRole: captured.body.expectedRole }
8503
+ };
8504
+ }
8505
+ function workspaceLeaveFailure(value, status) {
8506
+ if (!value || typeof value !== "object" || Array.isArray(value))
8507
+ return null;
8508
+ const code = value.code;
8509
+ return typeof code === "string" && Object.hasOwn(workspaceLeaveFailures, code) && workspaceLeaveFailures[code][0] === status ? code : null;
8510
+ }
8511
+ function parseWorkspaceLeaveResult(value, membershipId, organizationId) {
8512
+ const row = value;
8513
+ if (!row || typeof row !== "object" || Array.isArray(row) || row.membershipId !== membershipId || row.organizationId !== organizationId || row.removed !== true || row.signInRequired !== true)
8514
+ throw new RemoteWorkspaceLeaveUnconfirmedError;
8515
+ return { membershipId, organizationId, removed: true, signInRequired: true };
8516
+ }
8517
+ function workspaceLeaveProfileContext(membershipId, userId, profile) {
8518
+ const observed = profile === undefined ? undefined : workspaceContext(profile);
8519
+ const target = workspaceContext({ userId: userId ?? observed?.userId, membershipId });
8520
+ if (observed && (observed.userId !== target.userId || observed.membershipId !== target.membershipId))
8521
+ throw new WorkspaceIdentityMismatchError;
8522
+ return target;
8523
+ }
8524
+ var WorkspaceLeaveInputError, workspaceLeaveFailures, RemoteWorkspaceLeaveError, RemoteWorkspaceLeaveUnconfirmedError;
8525
+ var init_remote_workspace_leave = __esm(() => {
8526
+ init_remote_workspace_selection();
8527
+ init_remote_workspace();
8528
+ WorkspaceLeaveInputError = class WorkspaceLeaveInputError extends Error {
8529
+ constructor() {
8530
+ super("Confirm leaving the exact observed user and membership with its expected role.");
8531
+ this.name = "WorkspaceLeaveInputError";
8532
+ }
8533
+ };
8534
+ workspaceLeaveFailures = {
8535
+ INVALID_REQUEST: [400, "Provide the exact current membership and expected role."],
8536
+ ACCOUNT_UNAVAILABLE: [403, "Account is unavailable."],
8537
+ INTERACTIVE_SESSION_REQUIRED: [403, "Fresh interactive sign-in is required to leave a workspace."],
8538
+ MEMBERSHIP_ROLE_CHANGED: [409, "Your role changed. Sign in and inspect the workspace before leaving."],
8539
+ LAST_OWNER_REQUIRED: [409, "The workspace must retain another active owner."],
8540
+ LAST_WORKSPACE_REQUIRED: [409, "Another available workspace is required before leaving."],
8541
+ MEMBERSHIP_BUSY: [503, "Membership is busy. Inspect the workspace before another leave action."]
8542
+ };
8543
+ RemoteWorkspaceLeaveError = class RemoteWorkspaceLeaveError extends Error {
8544
+ code;
8545
+ status;
8546
+ constructor(code) {
8547
+ super(workspaceLeaveFailures[code][1]);
8548
+ this.code = code;
8549
+ this.name = "RemoteWorkspaceLeaveError";
8550
+ this.status = workspaceLeaveFailures[code][0];
8551
+ }
8552
+ };
8553
+ RemoteWorkspaceLeaveUnconfirmedError = class RemoteWorkspaceLeaveUnconfirmedError extends Error {
8554
+ code = "WORKSPACE_LEAVE_UNCONFIRMED";
8555
+ constructor() {
8556
+ super("The leave outcome is unconfirmed. Sign in again and inspect available memberships before another action. Do not retry automatically; saved credentials are unchanged.");
8557
+ this.name = "RemoteWorkspaceLeaveUnconfirmedError";
8558
+ }
8559
+ };
8560
+ });
8561
+
8493
8562
  // src/lib/remote-account.ts
8494
8563
  function creditCount(value) {
8495
8564
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 || value > 2147483647) {
@@ -8843,6 +8912,30 @@ class RemoteSkillsClient {
8843
8912
  }
8844
8913
  return value;
8845
8914
  }
8915
+ async leaveWorkspace(context, input) {
8916
+ const captured = workspaceLeaveInput(context, input);
8917
+ const connection = new RemoteSkillsClient(this.apiKey, this.apiUrl);
8918
+ const value = await connection.requestWorkspaceSelection("/api/auth/whoami");
8919
+ if (!value || typeof value !== "object" || value.authMethod !== "jwt")
8920
+ throw new RemoteWorkspaceLeaveError("INTERACTIVE_SESSION_REQUIRED");
8921
+ const identity = parseWorkspaceIdentity(value, captured.context.userId);
8922
+ if (identity.user.membershipId !== captured.context.membershipId)
8923
+ throw new WorkspaceIdentityMismatchError;
8924
+ let response, body;
8925
+ try {
8926
+ response = await connection.request("/api/v1/account/workspaces/leave", { method: "POST", body: JSON.stringify(captured.body) });
8927
+ body = JSON.parse(new TextDecoder().decode(await readBoundedResponse(response, 4096)));
8928
+ } catch {
8929
+ throw new RemoteWorkspaceLeaveUnconfirmedError;
8930
+ }
8931
+ if (!response.ok) {
8932
+ const code = workspaceLeaveFailure(body, response.status);
8933
+ if (code)
8934
+ throw new RemoteWorkspaceLeaveError(code);
8935
+ throw new RemoteWorkspaceLeaveUnconfirmedError;
8936
+ }
8937
+ return parseWorkspaceLeaveResult(body, captured.context.membershipId, identity.organization.id);
8938
+ }
8846
8939
  async listApiKeys() {
8847
8940
  return this.arrayResponse("/api/auth/keys");
8848
8941
  }
@@ -9232,6 +9325,7 @@ function createRemoteSkillsClientReadOnly(env = process.env) {
9232
9325
  }
9233
9326
  var RemoteRouteUnsupportedError, RemoteRequestError, RemoteWorkspaceMemberError, RemoteWorkspaceSelectionError, RemoteCapabilityUnavailableError, INVALID_SKILL_VERSION_RESPONSE = "Remote skill version payload did not match the expected contract.";
9234
9327
  var init_remote_client = __esm(() => {
9328
+ init_remote_workspace_leave();
9235
9329
  init_remote_workspace_selection();
9236
9330
  init_remote_workspace();
9237
9331
  init_remote_workspace();
@@ -14805,7 +14899,7 @@ class StdioServerTransport {
14805
14899
  // package.json
14806
14900
  var package_default = {
14807
14901
  name: "@hasna/skills",
14808
- version: "0.5.1",
14902
+ version: "0.5.2",
14809
14903
  description: "Skills library for AI coding agents",
14810
14904
  type: "module",
14811
14905
  bin: {
@@ -29557,6 +29651,7 @@ function registerStorageTools(server) {
29557
29651
  }
29558
29652
 
29559
29653
  // src/lib/remote-auth.ts
29654
+ init_remote_workspace_leave();
29560
29655
  init_remote_workspace_selection();
29561
29656
  init_remote_files();
29562
29657
  init_remote_workspace();
@@ -29732,6 +29827,10 @@ class RemoteSkillsAuthClient {
29732
29827
  const captured = workspaceMemberRoleInput(membershipId, input);
29733
29828
  return (await this.sessionClient(email2, code, context)).setWorkspaceMemberRole(captured.membershipId, captured.body);
29734
29829
  }
29830
+ async leaveWorkspace(email2, code, context, input) {
29831
+ const captured = workspaceLeaveInput(context, input);
29832
+ return (await this.sessionClient(email2, code, captured.context)).leaveWorkspace(captured.context, captured.input);
29833
+ }
29735
29834
  async removeWorkspaceMember(email2, code, membershipId, input, context) {
29736
29835
  const captured = workspaceMemberRemovalInput(membershipId, input);
29737
29836
  return (await this.sessionClient(email2, code, context)).removeWorkspaceMember(captured.membershipId, captured.body);
@@ -29825,6 +29924,7 @@ async function captureProfileWorkspace(action, source = process.env) {
29825
29924
 
29826
29925
  // src/mcp/remote-customer-tools.ts
29827
29926
  init_remote_client();
29927
+ init_remote_workspace_leave();
29828
29928
  function registerRemoteCustomerTools(server) {
29829
29929
  const memberRole = exports_external.enum(["owner", "admin", "member", "viewer"]);
29830
29930
  const memberInput = {
@@ -29833,6 +29933,18 @@ function registerRemoteCustomerTools(server) {
29833
29933
  email: exports_external.string().email(),
29834
29934
  code: exports_external.string().regex(/^\d{6}$/)
29835
29935
  };
29936
+ server.registerTool("leave_workspace", {
29937
+ title: "Leave Current Workspace",
29938
+ description: "Leave exactly the observed user and membership with confirm=true and fresh verification. The server enforces last-owner and last-workspace safeguards. Sign in again afterwards; no automatic retry or saved-profile deletion.",
29939
+ annotations: { destructiveHint: true, idempotentHint: false, readOnlyHint: false },
29940
+ inputSchema: exports_external.object({ ...memberInput, userId: memberInput.membershipId, confirm: exports_external.literal(true) }).strict()
29941
+ }, async ({ membershipId, userId, expectedRole, email: email2, code, confirm }) => {
29942
+ try {
29943
+ return mcpJson(await freshAccount("Leave workspace", (client, context) => client.leaveWorkspace(email2, code, workspaceLeaveProfileContext(membershipId, userId, context), { expectedRole, confirm })));
29944
+ } catch (error2) {
29945
+ return error2 instanceof RemoteWorkspaceLeaveError || error2 instanceof RemoteWorkspaceLeaveUnconfirmedError ? mcpError(error2.code, error2.message) : mcpError("WORKSPACE_LEAVE_UNCONFIRMED", "Leaving could not be confirmed. Check the selected profile and exact membership, sign in again and inspect available workspaces before another action. Do not retry automatically; saved credentials are unchanged.");
29946
+ }
29947
+ });
29836
29948
  server.registerTool("set_workspace_member_role", {
29837
29949
  title: "Set Current Workspace Member Role",
29838
29950
  description: "Change exactly this membership incarnation with its observed expectedRole and fresh verification. The server enforces owner/admin policy. No automatic refresh or retry; saved credentials stay unchanged.",
package/bin/migrate.js CHANGED
@@ -7,7 +7,7 @@ import { join as join6 } from "path";
7
7
  // package.json
8
8
  var package_default = {
9
9
  name: "@hasna/skills",
10
- version: "0.5.1",
10
+ version: "0.5.2",
11
11
  description: "Skills library for AI coding agents",
12
12
  type: "module",
13
13
  bin: {
package/bin/server.js CHANGED
@@ -23100,7 +23100,7 @@ var init_dist_es9 = __esm(() => {
23100
23100
  // package.json
23101
23101
  var package_default = {
23102
23102
  name: "@hasna/skills",
23103
- version: "0.5.1",
23103
+ version: "0.5.2",
23104
23104
  description: "Skills library for AI coding agents",
23105
23105
  type: "module",
23106
23106
  bin: {
package/bin/worker.js CHANGED
@@ -23103,7 +23103,7 @@ import { randomUUID as randomUUID4 } from "crypto";
23103
23103
  // package.json
23104
23104
  var package_default = {
23105
23105
  name: "@hasna/skills",
23106
- version: "0.5.1",
23106
+ version: "0.5.2",
23107
23107
  description: "Skills library for AI coding agents",
23108
23108
  type: "module",
23109
23109
  bin: {
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerWorkspaceLeaveCommand(workspace: Command): void;
package/dist/index.d.ts CHANGED
@@ -46,3 +46,5 @@ export type { RemoteCustomerRole, RemoteCustomerProfile, RemoteCurrentWorkspace,
46
46
  export type { RemoteWorkspaceContext, RemoteAccountWorkspace, RemoteAccountWorkspaces, RemoteWorkspaceIdentity, RemoteWorkspaceSession, RemoteAccountWorkspaceDiscovery, RemoteWorkspaceSelectionErrorCode } from "./lib/remote-workspace-selection.js";
47
47
  export { WorkspaceContextInputError, WorkspaceIdentityMismatchError } from "./lib/remote-workspace-selection.js";
48
48
  export { RemoteWorkspaceSelectionError } from "./lib/remote-client.js";
49
+ export type { LeaveRemoteWorkspace, RemoteWorkspaceLeaveResult, RemoteWorkspaceLeaveErrorCode } from "./lib/remote-workspace-leave.js";
50
+ export { WorkspaceLeaveInputError, RemoteWorkspaceLeaveError, RemoteWorkspaceLeaveUnconfirmedError } from "./lib/remote-workspace-leave.js";
package/dist/index.js CHANGED
@@ -10450,6 +10450,65 @@ function parseWorkspaceMembersPage(value) {
10450
10450
  return fail();
10451
10451
  return { organizationId: value.organizationId, members, nextCursor: value.nextCursor };
10452
10452
  }
10453
+
10454
+ // src/lib/remote-workspace-leave.ts
10455
+ class WorkspaceLeaveInputError extends Error {
10456
+ constructor() {
10457
+ super("Confirm leaving the exact observed user and membership with its expected role.");
10458
+ this.name = "WorkspaceLeaveInputError";
10459
+ }
10460
+ }
10461
+ function workspaceLeaveInput(context, input) {
10462
+ const target = workspaceContext(context);
10463
+ if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).sort().join(",") !== "confirm,expectedRole" || input.confirm !== true)
10464
+ throw new WorkspaceLeaveInputError;
10465
+ const captured = workspaceMemberRemovalInput(target.membershipId, { expectedRole: input.expectedRole });
10466
+ return {
10467
+ context: target,
10468
+ input: { expectedRole: captured.body.expectedRole, confirm: true },
10469
+ body: { membershipId: target.membershipId, expectedRole: captured.body.expectedRole }
10470
+ };
10471
+ }
10472
+ var workspaceLeaveFailures = {
10473
+ INVALID_REQUEST: [400, "Provide the exact current membership and expected role."],
10474
+ ACCOUNT_UNAVAILABLE: [403, "Account is unavailable."],
10475
+ INTERACTIVE_SESSION_REQUIRED: [403, "Fresh interactive sign-in is required to leave a workspace."],
10476
+ MEMBERSHIP_ROLE_CHANGED: [409, "Your role changed. Sign in and inspect the workspace before leaving."],
10477
+ LAST_OWNER_REQUIRED: [409, "The workspace must retain another active owner."],
10478
+ LAST_WORKSPACE_REQUIRED: [409, "Another available workspace is required before leaving."],
10479
+ MEMBERSHIP_BUSY: [503, "Membership is busy. Inspect the workspace before another leave action."]
10480
+ };
10481
+
10482
+ class RemoteWorkspaceLeaveError extends Error {
10483
+ code;
10484
+ status;
10485
+ constructor(code) {
10486
+ super(workspaceLeaveFailures[code][1]);
10487
+ this.code = code;
10488
+ this.name = "RemoteWorkspaceLeaveError";
10489
+ this.status = workspaceLeaveFailures[code][0];
10490
+ }
10491
+ }
10492
+
10493
+ class RemoteWorkspaceLeaveUnconfirmedError extends Error {
10494
+ code = "WORKSPACE_LEAVE_UNCONFIRMED";
10495
+ constructor() {
10496
+ super("The leave outcome is unconfirmed. Sign in again and inspect available memberships before another action. Do not retry automatically; saved credentials are unchanged.");
10497
+ this.name = "RemoteWorkspaceLeaveUnconfirmedError";
10498
+ }
10499
+ }
10500
+ function workspaceLeaveFailure(value, status) {
10501
+ if (!value || typeof value !== "object" || Array.isArray(value))
10502
+ return null;
10503
+ const code = value.code;
10504
+ return typeof code === "string" && Object.hasOwn(workspaceLeaveFailures, code) && workspaceLeaveFailures[code][0] === status ? code : null;
10505
+ }
10506
+ function parseWorkspaceLeaveResult(value, membershipId, organizationId) {
10507
+ const row = value;
10508
+ if (!row || typeof row !== "object" || Array.isArray(row) || row.membershipId !== membershipId || row.organizationId !== organizationId || row.removed !== true || row.signInRequired !== true)
10509
+ throw new RemoteWorkspaceLeaveUnconfirmedError;
10510
+ return { membershipId, organizationId, removed: true, signInRequired: true };
10511
+ }
10453
10512
  // src/lib/auth-store.ts
10454
10513
  function getApiUrl(action, env = process.env, options = {}) {
10455
10514
  return requireSkillsApiOrigin(action, env, options);
@@ -10949,6 +11008,30 @@ class RemoteSkillsClient {
10949
11008
  }
10950
11009
  return value;
10951
11010
  }
11011
+ async leaveWorkspace(context, input) {
11012
+ const captured = workspaceLeaveInput(context, input);
11013
+ const connection = new RemoteSkillsClient(this.apiKey, this.apiUrl);
11014
+ const value = await connection.requestWorkspaceSelection("/api/auth/whoami");
11015
+ if (!value || typeof value !== "object" || value.authMethod !== "jwt")
11016
+ throw new RemoteWorkspaceLeaveError("INTERACTIVE_SESSION_REQUIRED");
11017
+ const identity = parseWorkspaceIdentity(value, captured.context.userId);
11018
+ if (identity.user.membershipId !== captured.context.membershipId)
11019
+ throw new WorkspaceIdentityMismatchError;
11020
+ let response, body;
11021
+ try {
11022
+ response = await connection.request("/api/v1/account/workspaces/leave", { method: "POST", body: JSON.stringify(captured.body) });
11023
+ body = JSON.parse(new TextDecoder().decode(await readBoundedResponse(response, 4096)));
11024
+ } catch {
11025
+ throw new RemoteWorkspaceLeaveUnconfirmedError;
11026
+ }
11027
+ if (!response.ok) {
11028
+ const code = workspaceLeaveFailure(body, response.status);
11029
+ if (code)
11030
+ throw new RemoteWorkspaceLeaveError(code);
11031
+ throw new RemoteWorkspaceLeaveUnconfirmedError;
11032
+ }
11033
+ return parseWorkspaceLeaveResult(body, captured.context.membershipId, identity.organization.id);
11034
+ }
10952
11035
  async listApiKeys() {
10953
11036
  return this.arrayResponse("/api/auth/keys");
10954
11037
  }
@@ -12441,7 +12524,7 @@ import { dirname as dirname7, relative as relative4 } from "path";
12441
12524
  // package.json
12442
12525
  var package_default = {
12443
12526
  name: "@hasna/skills",
12444
- version: "0.5.1",
12527
+ version: "0.5.2",
12445
12528
  description: "Skills library for AI coding agents",
12446
12529
  type: "module",
12447
12530
  bin: {
@@ -15081,6 +15164,10 @@ class RemoteSkillsAuthClient {
15081
15164
  const captured = workspaceMemberRoleInput(membershipId, input);
15082
15165
  return (await this.sessionClient(email, code, context)).setWorkspaceMemberRole(captured.membershipId, captured.body);
15083
15166
  }
15167
+ async leaveWorkspace(email, code, context, input) {
15168
+ const captured = workspaceLeaveInput(context, input);
15169
+ return (await this.sessionClient(email, code, captured.context)).leaveWorkspace(captured.context, captured.input);
15170
+ }
15084
15171
  async removeWorkspaceMember(email, code, membershipId, input, context) {
15085
15172
  const captured = workspaceMemberRemovalInput(membershipId, input);
15086
15173
  return (await this.sessionClient(email, code, context)).removeWorkspaceMember(captured.membershipId, captured.body);
@@ -15273,6 +15360,7 @@ export {
15273
15360
  agentGlobalSkillsDir,
15274
15361
  addSchedule,
15275
15362
  adaptSkillMdForAgent,
15363
+ WorkspaceLeaveInputError,
15276
15364
  WorkspaceIdentityMismatchError,
15277
15365
  WorkspaceContextInputError,
15278
15366
  TOOL_PRIMITIVE_SCHEMA_VERSION,
@@ -15308,6 +15396,8 @@ export {
15308
15396
  SKILLS,
15309
15397
  RemoteWorkspaceSelectionError,
15310
15398
  RemoteWorkspaceMemberError,
15399
+ RemoteWorkspaceLeaveUnconfirmedError,
15400
+ RemoteWorkspaceLeaveError,
15311
15401
  RemoteSkillsClient,
15312
15402
  RemoteSkillsAuthClient,
15313
15403
  RemoteRouteUnsupportedError,
@@ -1,3 +1,4 @@
1
+ import { type LeaveRemoteWorkspace } from "./remote-workspace-leave.js";
1
2
  import { type RemoteWorkspaceContext, type RemoteWorkspaceSession, type RemoteAccountWorkspaceDiscovery } from "./remote-workspace-selection.js";
2
3
  import { type RemoteWorkspaceMembersOptions } from "./remote-workspace.js";
3
4
  import { type SetRemoteWorkspaceMemberRole, type RemoveRemoteWorkspaceMember } from "./remote-workspace.js";
@@ -46,6 +47,8 @@ export declare class RemoteSkillsAuthClient {
46
47
  /** Fresh owner/admin session; explicit context survives default-workspace OTP selection. */
47
48
  listWorkspaceMembers(email: string, code: string, options?: RemoteWorkspaceMembersOptions, context?: RemoteWorkspaceContext): Promise<import("./remote-workspace.js").RemoteWorkspaceMembersPage>;
48
49
  setWorkspaceMemberRole(email: string, code: string, membershipId: string, input: SetRemoteWorkspaceMemberRole, context?: RemoteWorkspaceContext): Promise<import("./remote-workspace.js").RemoteWorkspaceMemberRoleResult>;
50
+ /** Fresh verification binds the exact observed incarnation before a single confirmed leave. */
51
+ leaveWorkspace(email: string, code: string, context: RemoteWorkspaceContext, input: LeaveRemoteWorkspace): Promise<import("./remote-workspace-leave.js").RemoteWorkspaceLeaveResult>;
49
52
  removeWorkspaceMember(email: string, code: string, membershipId: string, input: RemoveRemoteWorkspaceMember, context?: RemoteWorkspaceContext): Promise<import("./remote-workspace.js").RemoteWorkspaceMemberRemovalResult>;
50
53
  /** Common auth transport used by CLI login, preserving the selected instance through awaits. */
51
54
  request(path: string, options?: RequestInit): Promise<any>;
@@ -1,3 +1,4 @@
1
+ import { type LeaveRemoteWorkspace, type RemoteWorkspaceLeaveResult } from "./remote-workspace-leave.js";
1
2
  import { type RemoteWorkspaceContext, type RemoteAccountWorkspaces, type RemoteWorkspaceSession, type RemoteWorkspaceSelectionErrorCode } from "./remote-workspace-selection.js";
2
3
  import { type RemoteWorkspaceMembersOptions, type RemoteWorkspaceMembersPage } from "./remote-workspace.js";
3
4
  import { type RemoteWorkspaceMemberErrorCode, type SetRemoteWorkspaceMemberRole, type RemoveRemoteWorkspaceMember, type RemoteWorkspaceMemberRoleResult, type RemoteWorkspaceMemberRemovalResult } from "./remote-workspace.js";
@@ -145,6 +146,8 @@ export declare class RemoteSkillsClient {
145
146
  /** Removes only this incarnation. A successful tombstone replay is returned unchanged. */
146
147
  removeWorkspaceMember(membershipId: string, input: RemoveRemoteWorkspaceMember): Promise<RemoteWorkspaceMemberRemovalResult>;
147
148
  private requestWorkspaceMember;
149
+ /** Leave only the explicitly confirmed current incarnation, once. No credential writes or retries. */
150
+ leaveWorkspace(context: RemoteWorkspaceContext, input: LeaveRemoteWorkspace): Promise<RemoteWorkspaceLeaveResult>;
148
151
  listApiKeys(): Promise<Record<string, unknown>[]>;
149
152
  createApiKey(name: string, scopes?: string[]): Promise<{
150
153
  key: string;
@@ -0,0 +1,52 @@
1
+ import { type RemoteWorkspaceContext } from "./remote-workspace-selection.js";
2
+ import type { RemoteCustomerRole } from "./remote-profile.js";
3
+ export type LeaveRemoteWorkspace = Readonly<{
4
+ expectedRole: RemoteCustomerRole;
5
+ confirm: true;
6
+ }>;
7
+ export type RemoteWorkspaceLeaveResult = {
8
+ organizationId: string;
9
+ membershipId: string;
10
+ removed: true;
11
+ signInRequired: true;
12
+ };
13
+ export declare class WorkspaceLeaveInputError extends Error {
14
+ constructor();
15
+ }
16
+ export declare function workspaceLeaveInput(context: RemoteWorkspaceContext, input: LeaveRemoteWorkspace): {
17
+ context: Readonly<{
18
+ userId: string;
19
+ membershipId: string;
20
+ }>;
21
+ input: {
22
+ expectedRole: RemoteCustomerRole;
23
+ confirm: true;
24
+ };
25
+ body: {
26
+ membershipId: string;
27
+ expectedRole: RemoteCustomerRole;
28
+ };
29
+ };
30
+ export declare const workspaceLeaveFailures: {
31
+ readonly INVALID_REQUEST: readonly [400, "Provide the exact current membership and expected role."];
32
+ readonly ACCOUNT_UNAVAILABLE: readonly [403, "Account is unavailable."];
33
+ readonly INTERACTIVE_SESSION_REQUIRED: readonly [403, "Fresh interactive sign-in is required to leave a workspace."];
34
+ readonly MEMBERSHIP_ROLE_CHANGED: readonly [409, "Your role changed. Sign in and inspect the workspace before leaving."];
35
+ readonly LAST_OWNER_REQUIRED: readonly [409, "The workspace must retain another active owner."];
36
+ readonly LAST_WORKSPACE_REQUIRED: readonly [409, "Another available workspace is required before leaving."];
37
+ readonly MEMBERSHIP_BUSY: readonly [503, "Membership is busy. Inspect the workspace before another leave action."];
38
+ };
39
+ export type RemoteWorkspaceLeaveErrorCode = keyof typeof workspaceLeaveFailures;
40
+ export declare class RemoteWorkspaceLeaveError extends Error {
41
+ readonly code: RemoteWorkspaceLeaveErrorCode;
42
+ readonly status: number;
43
+ constructor(code: RemoteWorkspaceLeaveErrorCode);
44
+ }
45
+ export declare class RemoteWorkspaceLeaveUnconfirmedError extends Error {
46
+ readonly code = "WORKSPACE_LEAVE_UNCONFIRMED";
47
+ constructor();
48
+ }
49
+ export declare function workspaceLeaveFailure(value: unknown, status: number): RemoteWorkspaceLeaveErrorCode | null;
50
+ export declare function parseWorkspaceLeaveResult(value: unknown, membershipId: string, organizationId: string): RemoteWorkspaceLeaveResult;
51
+ /** A configured profile may constrain an explicit leave, never retarget it. */
52
+ export declare function workspaceLeaveProfileContext(membershipId: string, userId: string | undefined, profile?: RemoteWorkspaceContext): RemoteWorkspaceContext;
@@ -40,3 +40,5 @@ export type { RemoteCustomerRole, RemoteCustomerProfile, RemoteCurrentWorkspace,
40
40
  export type { RemoteWorkspaceContext, RemoteAccountWorkspace, RemoteAccountWorkspaces, RemoteWorkspaceIdentity, RemoteWorkspaceSession, RemoteAccountWorkspaceDiscovery, RemoteWorkspaceSelectionErrorCode } from "../lib/remote-workspace-selection.js";
41
41
  export { WorkspaceContextInputError, WorkspaceIdentityMismatchError } from "../lib/remote-workspace-selection.js";
42
42
  export { RemoteWorkspaceSelectionError } from "../lib/remote-client.js";
43
+ export type { LeaveRemoteWorkspace, RemoteWorkspaceLeaveResult, RemoteWorkspaceLeaveErrorCode } from "../lib/remote-workspace-leave.js";
44
+ export { WorkspaceLeaveInputError, RemoteWorkspaceLeaveError, RemoteWorkspaceLeaveUnconfirmedError } from "../lib/remote-workspace-leave.js";
package/dist/sdk/index.js CHANGED
@@ -25462,7 +25462,7 @@ class MissingSkillsFleetError extends Error {
25462
25462
  // package.json
25463
25463
  var package_default = {
25464
25464
  name: "@hasna/skills",
25465
- version: "0.5.1",
25465
+ version: "0.5.2",
25466
25466
  description: "Skills library for AI coding agents",
25467
25467
  type: "module",
25468
25468
  bin: {
@@ -52360,6 +52360,65 @@ function parseWorkspaceMembersPage(value) {
52360
52360
  return fail();
52361
52361
  return { organizationId: value.organizationId, members, nextCursor: value.nextCursor };
52362
52362
  }
52363
+
52364
+ // src/lib/remote-workspace-leave.ts
52365
+ class WorkspaceLeaveInputError extends Error {
52366
+ constructor() {
52367
+ super("Confirm leaving the exact observed user and membership with its expected role.");
52368
+ this.name = "WorkspaceLeaveInputError";
52369
+ }
52370
+ }
52371
+ function workspaceLeaveInput(context, input) {
52372
+ const target = workspaceContext(context);
52373
+ if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).sort().join(",") !== "confirm,expectedRole" || input.confirm !== true)
52374
+ throw new WorkspaceLeaveInputError;
52375
+ const captured = workspaceMemberRemovalInput(target.membershipId, { expectedRole: input.expectedRole });
52376
+ return {
52377
+ context: target,
52378
+ input: { expectedRole: captured.body.expectedRole, confirm: true },
52379
+ body: { membershipId: target.membershipId, expectedRole: captured.body.expectedRole }
52380
+ };
52381
+ }
52382
+ var workspaceLeaveFailures = {
52383
+ INVALID_REQUEST: [400, "Provide the exact current membership and expected role."],
52384
+ ACCOUNT_UNAVAILABLE: [403, "Account is unavailable."],
52385
+ INTERACTIVE_SESSION_REQUIRED: [403, "Fresh interactive sign-in is required to leave a workspace."],
52386
+ MEMBERSHIP_ROLE_CHANGED: [409, "Your role changed. Sign in and inspect the workspace before leaving."],
52387
+ LAST_OWNER_REQUIRED: [409, "The workspace must retain another active owner."],
52388
+ LAST_WORKSPACE_REQUIRED: [409, "Another available workspace is required before leaving."],
52389
+ MEMBERSHIP_BUSY: [503, "Membership is busy. Inspect the workspace before another leave action."]
52390
+ };
52391
+
52392
+ class RemoteWorkspaceLeaveError extends Error {
52393
+ code;
52394
+ status;
52395
+ constructor(code) {
52396
+ super(workspaceLeaveFailures[code][1]);
52397
+ this.code = code;
52398
+ this.name = "RemoteWorkspaceLeaveError";
52399
+ this.status = workspaceLeaveFailures[code][0];
52400
+ }
52401
+ }
52402
+
52403
+ class RemoteWorkspaceLeaveUnconfirmedError extends Error {
52404
+ code = "WORKSPACE_LEAVE_UNCONFIRMED";
52405
+ constructor() {
52406
+ super("The leave outcome is unconfirmed. Sign in again and inspect available memberships before another action. Do not retry automatically; saved credentials are unchanged.");
52407
+ this.name = "RemoteWorkspaceLeaveUnconfirmedError";
52408
+ }
52409
+ }
52410
+ function workspaceLeaveFailure(value, status) {
52411
+ if (!value || typeof value !== "object" || Array.isArray(value))
52412
+ return null;
52413
+ const code = value.code;
52414
+ return typeof code === "string" && Object.hasOwn(workspaceLeaveFailures, code) && workspaceLeaveFailures[code][0] === status ? code : null;
52415
+ }
52416
+ function parseWorkspaceLeaveResult(value, membershipId, organizationId) {
52417
+ const row = value;
52418
+ if (!row || typeof row !== "object" || Array.isArray(row) || row.membershipId !== membershipId || row.organizationId !== organizationId || row.removed !== true || row.signInRequired !== true)
52419
+ throw new RemoteWorkspaceLeaveUnconfirmedError;
52420
+ return { membershipId, organizationId, removed: true, signInRequired: true };
52421
+ }
52363
52422
  // src/lib/auth-store.ts
52364
52423
  function getApiUrl(action, env = process.env, options = {}) {
52365
52424
  return requireSkillsApiOrigin(action, env, options);
@@ -52815,6 +52874,30 @@ class RemoteSkillsClient {
52815
52874
  }
52816
52875
  return value;
52817
52876
  }
52877
+ async leaveWorkspace(context, input) {
52878
+ const captured = workspaceLeaveInput(context, input);
52879
+ const connection = new RemoteSkillsClient(this.apiKey, this.apiUrl);
52880
+ const value = await connection.requestWorkspaceSelection("/api/auth/whoami");
52881
+ if (!value || typeof value !== "object" || value.authMethod !== "jwt")
52882
+ throw new RemoteWorkspaceLeaveError("INTERACTIVE_SESSION_REQUIRED");
52883
+ const identity = parseWorkspaceIdentity(value, captured.context.userId);
52884
+ if (identity.user.membershipId !== captured.context.membershipId)
52885
+ throw new WorkspaceIdentityMismatchError;
52886
+ let response, body;
52887
+ try {
52888
+ response = await connection.request("/api/v1/account/workspaces/leave", { method: "POST", body: JSON.stringify(captured.body) });
52889
+ body = JSON.parse(new TextDecoder().decode(await readBoundedResponse(response, 4096)));
52890
+ } catch {
52891
+ throw new RemoteWorkspaceLeaveUnconfirmedError;
52892
+ }
52893
+ if (!response.ok) {
52894
+ const code = workspaceLeaveFailure(body, response.status);
52895
+ if (code)
52896
+ throw new RemoteWorkspaceLeaveError(code);
52897
+ throw new RemoteWorkspaceLeaveUnconfirmedError;
52898
+ }
52899
+ return parseWorkspaceLeaveResult(body, captured.context.membershipId, identity.organization.id);
52900
+ }
52818
52901
  async listApiKeys() {
52819
52902
  return this.arrayResponse("/api/auth/keys");
52820
52903
  }
@@ -53370,6 +53453,10 @@ class RemoteSkillsAuthClient {
53370
53453
  const captured = workspaceMemberRoleInput(membershipId, input);
53371
53454
  return (await this.sessionClient(email, code, context)).setWorkspaceMemberRole(captured.membershipId, captured.body);
53372
53455
  }
53456
+ async leaveWorkspace(email, code, context, input) {
53457
+ const captured = workspaceLeaveInput(context, input);
53458
+ return (await this.sessionClient(email, code, captured.context)).leaveWorkspace(captured.context, captured.input);
53459
+ }
53373
53460
  async removeWorkspaceMember(email, code, membershipId, input, context) {
53374
53461
  const captured = workspaceMemberRemovalInput(membershipId, input);
53375
53462
  return (await this.sessionClient(email, code, context)).removeWorkspaceMember(captured.membershipId, captured.body);
@@ -53453,6 +53540,7 @@ export {
53453
53540
  assertDurableTarget,
53454
53541
  assertDurableStore,
53455
53542
  artifactStorageSeam,
53543
+ WorkspaceLeaveInputError,
53456
53544
  WorkspaceIdentityMismatchError,
53457
53545
  WorkspaceContextInputError,
53458
53546
  SqliteSkillsStore,
@@ -53465,6 +53553,8 @@ export {
53465
53553
  SKILLS_API_KEY_ENV,
53466
53554
  RemoteWorkspaceSelectionError,
53467
53555
  RemoteWorkspaceMemberError,
53556
+ RemoteWorkspaceLeaveUnconfirmedError,
53557
+ RemoteWorkspaceLeaveError,
53468
53558
  RemoteSkillsClient,
53469
53559
  RemoteSkillsAuthClient,
53470
53560
  RemoteRouteUnsupportedError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {