commonswarm 0.1.22 → 0.1.23

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 (2) hide show
  1. package/cswarm.cjs +142 -44
  2. package/package.json +1 -1
package/cswarm.cjs CHANGED
@@ -13511,6 +13511,7 @@ __export(cli_exports, {
13511
13511
  listenerPermissionMode: () => listenerPermissionMode,
13512
13512
  listenerStatusJson: () => listenerStatusJson,
13513
13513
  renderRoster: () => renderRoster,
13514
+ replyRefusalHint: () => replyRefusalHint,
13514
13515
  resolveDetachedClaudeExecutable: () => resolveDetachedClaudeExecutable,
13515
13516
  resolveDetachedCodexExecutable: () => resolveDetachedCodexExecutable,
13516
13517
  resolveTurnBudgetOrDefer: () => resolveTurnBudgetOrDefer
@@ -22162,15 +22163,8 @@ function createWorkspaceError(status, body) {
22162
22163
  return new CreateWorkspaceError(
22163
22164
  status,
22164
22165
  code,
22165
- /* D-067/D-075. This used to end "Archiving a workspace frees its slot; the CLI cannot archive
22166
- * one yet, so ask whoever operates this deployment." Both halves were dead ends. Archiving
22167
- * is unreachable from every surface — `archived_at` exists and nothing writes it — and on a
22168
- * self-serve deployment the reader IS the operator, so it named a person who does not exist
22169
- * to perform an action that does not exist.
22170
- *
22171
- * It now states the limit, does not offer a remedy that is unimplemented, and names the one
22172
- * route that does work: someone else's invitation, which is not capped. */
22173
- `${limit === null ? "You have already created as many workspaces as this account allows." : `You have already created ${limit} workspaces, which is the limit for one account.`} Workspaces cannot be removed yet, so this limit is a ceiling rather than a queue. Workspaces you were invited to do not count against it \u2014 a collaborator can still add you to theirs.`
22166
+ /* Closing one live workspace now frees a slot; name the exact confirmed command. */
22167
+ `${limit === null ? "You have already created as many workspaces as this account allows." : `You have already created ${limit} workspaces, which is the limit for one account.`} Close one with cswarm workspace close <full-id|exact-name> --confirm <same-selector>, then try again. Workspaces you were invited to do not count against it.`
22174
22168
  );
22175
22169
  }
22176
22170
  if (status === 403) {
@@ -22797,6 +22791,12 @@ var CONTENT_TYPES = /* @__PURE__ */ new Map([
22797
22791
  [".md", "text/markdown"],
22798
22792
  [".txt", "text/plain"],
22799
22793
  [".csv", "text/csv"],
22794
+ // .html/.htm: a web/marketing team's deliverables (Fastio feedback 2026-08-19). Every
22795
+ // download is served Content-Disposition: attachment (§5), never rendered inline, so HTML
22796
+ // is no more dangerous than the .svg already permitted — the spec treats all downloads as
22797
+ // untrusted attachments the consumer must not execute.
22798
+ [".html", "text/html"],
22799
+ [".htm", "text/html"],
22800
22800
  [".json", "application/json"],
22801
22801
  [".yaml", "application/yaml"],
22802
22802
  [".yml", "application/yaml"],
@@ -27730,6 +27730,7 @@ var DEFAULT_MEMBERSHIP_REVOKED = {
27730
27730
  message: "Your previously selected workspace is no longer available to this account. CommonSwarm cleared that saved selection."
27731
27731
  };
27732
27732
  var PROJECT_NOT_AVAILABLE = "That workspace is not available to this account. Run cswarm workspaces to see workspaces you can select.";
27733
+ var ARCHIVED_PROJECT_NOT_AVAILABLE = "That workspace is closed and cannot be selected. Run cswarm workspaces to see live workspaces you can select.";
27733
27734
  function compareText(left, right) {
27734
27735
  return left < right ? -1 : left > right ? 1 : 0;
27735
27736
  }
@@ -27767,8 +27768,8 @@ var WorkspaceResolutionError = class extends WorkspaceCliError {
27767
27768
  };
27768
27769
  var WorkspaceUnavailableError = class extends WorkspaceCliError {
27769
27770
  code = "project_not_available";
27770
- constructor() {
27771
- super(PROJECT_NOT_AVAILABLE);
27771
+ constructor(message = PROJECT_NOT_AVAILABLE) {
27772
+ super(message);
27772
27773
  this.name = "WorkspaceUnavailableError";
27773
27774
  }
27774
27775
  structured() {
@@ -27872,41 +27873,41 @@ function cloudWorkspaceDirectory(target2, fetcher = fetch) {
27872
27873
  "workspaces",
27873
27874
  {
27874
27875
  select: "workspace_id,name,archived_at",
27876
+ archived_at: "is.null",
27875
27877
  order: "workspace_id.asc"
27876
27878
  },
27877
27879
  fetcher
27878
27880
  )
27879
27881
  ]);
27880
- const names = /* @__PURE__ */ new Map();
27882
+ const roles = /* @__PURE__ */ new Map();
27883
+ for (const row of membershipRows) {
27884
+ const workspaceId2 = checkedUuid(row.workspace_id, "workspace_id");
27885
+ roles.set(workspaceId2, checkedRole(row.role));
27886
+ }
27887
+ const result = [];
27881
27888
  for (const row of workspaceRows) {
27882
27889
  const workspaceId2 = checkedUuid(row.workspace_id, "workspace_id");
27883
27890
  const archivedAt = checkedNullableTimestamp(
27884
27891
  row.archived_at,
27885
27892
  "archived_at"
27886
27893
  );
27887
- names.set(workspaceId2, {
27894
+ if (archivedAt !== null) continue;
27895
+ const role = roles.get(workspaceId2);
27896
+ if (!role) {
27897
+ throw new Error(
27898
+ "workspace read omitted the current user's live membership"
27899
+ );
27900
+ }
27901
+ result.push({
27902
+ workspace_id: workspaceId2,
27888
27903
  name: sanitizeDisplayLabel(
27889
27904
  checkedString(row.name, "workspace name"),
27890
27905
  "Unnamed workspace"
27891
27906
  ),
27892
- archived: archivedAt !== null
27907
+ role,
27908
+ archived: false
27893
27909
  });
27894
27910
  }
27895
- const result = membershipRows.map((row) => {
27896
- const workspaceId2 = checkedUuid(row.workspace_id, "workspace_id");
27897
- const project = names.get(workspaceId2);
27898
- if (!project) {
27899
- throw new Error(
27900
- "workspace read omitted a workspace for a live membership"
27901
- );
27902
- }
27903
- return {
27904
- workspace_id: workspaceId2,
27905
- name: project.name,
27906
- role: checkedRole(row.role),
27907
- archived: project.archived
27908
- };
27909
- });
27910
27911
  return sortWorkspaces(result);
27911
27912
  },
27912
27913
  async status(session, workspaceId2) {
@@ -27961,6 +27962,9 @@ function cloudWorkspaceDirectory(target2, fetcher = fetch) {
27961
27962
  you: userId === session.userId
27962
27963
  };
27963
27964
  });
27965
+ if (!members.some((member) => member.you)) {
27966
+ throw new WorkspaceUnavailableError();
27967
+ }
27964
27968
  const memberNames = new Map(
27965
27969
  members.map((member) => [member.user_id, member.name])
27966
27970
  );
@@ -28086,6 +28090,32 @@ async function clearWorkspaceDefault(store2, userId, expectedWorkspaceId) {
28086
28090
  return true;
28087
28091
  });
28088
28092
  }
28093
+ async function updateWorkspaceDefaultAfterClose(store2, userId, closedWorkspaceId, workspaces) {
28094
+ return await store2.withLock(async () => {
28095
+ const current = await store2.readProfile();
28096
+ if (current.userId !== userId || current.workspaceId !== closedWorkspaceId) {
28097
+ return {
28098
+ closedWasSelected: false,
28099
+ nextWorkspace: null,
28100
+ selectedWorkspaceId: current.userId === userId ? current.workspaceId : null
28101
+ };
28102
+ }
28103
+ const nextWorkspace = sortWorkspaces(workspaces).find(
28104
+ (workspace) => workspace.workspace_id !== closedWorkspaceId && !workspace.archived
28105
+ ) ?? null;
28106
+ await store2.writeProfile({
28107
+ ...current,
28108
+ workspaceId: nextWorkspace?.workspace_id ?? null,
28109
+ principalId: null,
28110
+ principalName: null
28111
+ });
28112
+ return {
28113
+ closedWasSelected: true,
28114
+ nextWorkspace,
28115
+ selectedWorkspaceId: nextWorkspace?.workspace_id ?? null
28116
+ };
28117
+ });
28118
+ }
28089
28119
  function workspaceOverride(explicit, environmental) {
28090
28120
  if (explicit !== void 0) {
28091
28121
  if (!UUID_RE6.test(explicit)) {
@@ -28144,6 +28174,14 @@ async function resolveWorkspace(options) {
28144
28174
  throw new WorkspaceResolutionError(workspaces);
28145
28175
  }
28146
28176
  async function selectWorkspace(selector, workspaces, store2, userId) {
28177
+ const selected = resolveWorkspaceSelector(selector, workspaces);
28178
+ if (selected.archived) {
28179
+ throw new WorkspaceUnavailableError(ARCHIVED_PROJECT_NOT_AVAILABLE);
28180
+ }
28181
+ await writeWorkspaceDefault(store2, userId, selected.workspace_id);
28182
+ return selected;
28183
+ }
28184
+ function resolveWorkspaceSelector(selector, workspaces) {
28147
28185
  const sorted = sortWorkspaces(workspaces);
28148
28186
  let selected;
28149
28187
  if (UUID_RE6.test(selector)) {
@@ -28162,7 +28200,6 @@ async function selectWorkspace(selector, workspaces, store2, userId) {
28162
28200
  selected = matches[0];
28163
28201
  }
28164
28202
  if (!selected) throw new WorkspaceUnavailableError();
28165
- await writeWorkspaceDefault(store2, userId, selected.workspace_id);
28166
28203
  return selected;
28167
28204
  }
28168
28205
  function holderLabel(holder) {
@@ -28181,13 +28218,8 @@ function relativeExpiry(expiry, now = Date.now()) {
28181
28218
  const amount = relativeMagnitude(remaining);
28182
28219
  return remaining >= 0 ? `expires in ${amount}` : `expired ${amount} ago`;
28183
28220
  }
28184
- var ARCHIVE_NOT_ENFORCED_CODE = "workspace_archive_not_enforced";
28185
- var ARCHIVE_NOT_ENFORCED_MESSAGE = "Archiving a workspace does not restrict what members or their agents can do in it: an archived workspace stays selectable, and commands against it still succeed while your membership is live. Removing a workspace from this list means ending your membership, which this CLI cannot do \u2014 ask whoever runs the workspace.";
28186
28221
  function archiveKnownGaps() {
28187
- return [{
28188
- code: ARCHIVE_NOT_ENFORCED_CODE,
28189
- message: ARCHIVE_NOT_ENFORCED_MESSAGE
28190
- }];
28222
+ return [];
28191
28223
  }
28192
28224
  function renderWorkspaces(workspaces, currentWorkspaceId) {
28193
28225
  if (workspaces.length === 0) {
@@ -28211,9 +28243,6 @@ function renderWorkspaces(workspaces, currentWorkspaceId) {
28211
28243
  "No workspace is selected. Run cswarm use <full-id|exact-name>."
28212
28244
  );
28213
28245
  }
28214
- if (workspaces.some((workspace) => workspace.archived)) {
28215
- lines.push(ARCHIVE_NOT_ENFORCED_MESSAGE);
28216
- }
28217
28246
  return lines.join("\n");
28218
28247
  }
28219
28248
  function renderStatus(options) {
@@ -36933,8 +36962,8 @@ var ACCEPTED_AGENT_CREDENTIAL_MESSAGES = [
36933
36962
  AGENT_CREDENTIAL_MESSAGE_D088
36934
36963
  ];
36935
36964
  function packageVersion() {
36936
- if ("0.1.22".length > 0) {
36937
- return "0.1.22";
36965
+ if ("0.1.23".length > 0) {
36966
+ return "0.1.23";
36938
36967
  }
36939
36968
  try {
36940
36969
  const value = JSON.parse(
@@ -37072,6 +37101,7 @@ Usage:
37072
37101
  cswarm invite [--url <url> --anon-key <key>] [--workspace-id <uuid>] --email <email>
37073
37102
  cswarm invite revoke [--url <url> --anon-key <key>] [--workspace-id <uuid>] --invitation-id <uuid> [--json]
37074
37103
  cswarm member remove <full-user-id|exact-name> --confirm <same-selector> [--url <url> --anon-key <key>] [--workspace-id <uuid>] [--json]
37104
+ cswarm workspace close <full-id|exact-name> --confirm <same-selector> [--url <url> --anon-key <key>] [--json]
37075
37105
  cswarm accept --link-stdin [--name <name>] [--no-browser] [--json]
37076
37106
  cswarm accept <https://...#invite=...|cswarm://accept/...> [--name <name>] [--no-browser] [--json] # unsafe: shell history/process list
37077
37107
  cswarm accept --invitation-token-stdin [--url <url> --anon-key <key>]
@@ -37100,6 +37130,7 @@ Credential selection for command/dogfood:
37100
37130
  feedback command only, nothing persisted -- either form
37101
37131
  listen start persists durable state, rotates -- needs expires_at
37102
37132
  token revoke names what it revokes -- needs token_id
37133
+ workspace close is human-session-only; it never accepts an agent token
37103
37134
 
37104
37135
  Found a bug or missing feature in cswarm itself? cswarm feedback sends it to the
37105
37136
  deployment's operators \u2014 agents are encouraged to report friction they hit.
@@ -37108,6 +37139,8 @@ Signals (intention sharing) accept the same credential selection. Agent mode
37108
37139
  never opens a browser or infers a human's saved workspace. Durations use a whole
37109
37140
  number plus m, h, or d (for example 90m, 24h, or 7d) and are capped at 30d.
37110
37141
  Place -- before signal text that itself begins with -- to stop option parsing.
37142
+ Signal text is at most 2000 characters and --about at most 500; a longer body is
37143
+ refused locally before any network call, so compose within the limit.
37111
37144
 
37112
37145
  listen start --turn-budget bounds ONE worker prompt turn (default 10m): how long
37113
37146
  the worker may think and use tools on a single message before the turn times out
@@ -37120,7 +37153,7 @@ TTL); a turn that lands just before a rotation can be clamped to the ~5m
37120
37153
  renewal lead, and if it times out there, durable delivery retries it on the
37121
37154
  fresh credential.
37122
37155
 
37123
- Invite, legacy token accept, principal create/revoke, human token mint/revoke, link, and new require a
37156
+ Invite, legacy token accept, principal create/revoke, human token mint/revoke, link, new, and workspace close require a
37124
37157
  stored human login. Agent self-surrender of a token uses --agent-token-stdin and never takes the secret on argv. Invite-link accept signs in when needed, then accepts and
37125
37158
  registers one principal. Invitation links, agent credentials, and capability links
37126
37159
  appear only in fresh success responses.
@@ -37815,6 +37848,53 @@ async function runMember(args) {
37815
37848
  `);
37816
37849
  }
37817
37850
  }
37851
+ async function runWorkspace(args) {
37852
+ args.assertShape([...TARGET_FLAGS, "confirm", "json"], 3);
37853
+ if (args.positionals[1] !== "close") {
37854
+ throw new UsageError(
37855
+ `unknown workspace command: ${args.positionals[1] ?? "(missing)"}`
37856
+ );
37857
+ }
37858
+ const selector = args.positionals[2];
37859
+ if (args.required("confirm") !== selector) {
37860
+ throw new Error(
37861
+ "--confirm must exactly repeat the workspace selector; no request was sent"
37862
+ );
37863
+ }
37864
+ const cloud = await target(args);
37865
+ const human = await humanCredential(args, cloud);
37866
+ const directory = cloudWorkspaceDirectory(cloud);
37867
+ const projects = await directory.list(human);
37868
+ const selected = resolveWorkspaceSelector(selector, projects);
37869
+ const response = (await sendConnectWithPending(
37870
+ new ThinCommandClient(cloud),
37871
+ human,
37872
+ selected.workspace_id,
37873
+ { kind: "archive_workspace" }
37874
+ )).response;
37875
+ if (response.status !== "accepted") {
37876
+ throw new Error(
37877
+ `Workspace close was rejected: ${response.reason ?? "domain rejection"}. The workspace is still open.`
37878
+ );
37879
+ }
37880
+ const { closedWasSelected, nextWorkspace, selectedWorkspaceId } = await updateWorkspaceDefaultAfterClose(
37881
+ human.store,
37882
+ human.userId,
37883
+ selected.workspace_id,
37884
+ projects
37885
+ );
37886
+ const message = nextWorkspace ? `Closed workspace ${selected.name} (${selected.workspace_id}). It is hidden for everyone, and ${nextWorkspace.name} (${nextWorkspace.workspace_id}) is now selected.` : closedWasSelected ? `Closed workspace ${selected.name} (${selected.workspace_id}). It is hidden for everyone. No live workspace remains, so the selected workspace was cleared.` : `Closed workspace ${selected.name} (${selected.workspace_id}). It is hidden for everyone. Your selected workspace was not changed.`;
37887
+ const output = {
37888
+ message,
37889
+ status: response.status,
37890
+ workspace_id: selected.workspace_id,
37891
+ selected_workspace_id: selectedWorkspaceId,
37892
+ command_event_ids: response.event_ids
37893
+ };
37894
+ if (args.has("json")) printJson(output);
37895
+ else process.stdout.write(`${message}
37896
+ `);
37897
+ }
37818
37898
  async function runLegacyAccept(args) {
37819
37899
  const invitationToken = await invitationCredential(args);
37820
37900
  const cloud = await target(args);
@@ -38507,8 +38587,10 @@ function resolveTurnBudgetOrDefer(configuredBudgetMs, credentialExpiresAt, nowMs
38507
38587
  }
38508
38588
  return clampTurnBudgetToCredential(configuredBudgetMs, credentialExpiresAt, nowMs);
38509
38589
  }
38590
+ var SIGNAL_BODY_MAX = 2e3;
38591
+ var SIGNAL_ABOUT_MAX = 500;
38510
38592
  function signalText(value, label) {
38511
- const maximum = label === "body" ? 2e3 : 500;
38593
+ const maximum = label === "body" ? SIGNAL_BODY_MAX : SIGNAL_ABOUT_MAX;
38512
38594
  if (value.length < (label === "body" ? 1 : 0) || value.length > maximum) {
38513
38595
  throw new Error(
38514
38596
  `${label === "body" ? "signal text" : "--about"} must be ${label === "body" ? "1.." : "at most "}${maximum} characters`
@@ -38767,6 +38849,10 @@ Check whether you are about to do the same work.
38767
38849
  `}`
38768
38850
  );
38769
38851
  }
38852
+ function replyRefusalHint(error) {
38853
+ if (!(error instanceof CommandHttpError) || error.status !== 403) return null;
38854
+ return "reply was refused (403). The most common cause is that the signal was not addressed to you \u2014 you cannot reply to your own ask; reply to the other party's signal, reach someone directly with cswarm ask --to <agent>, or post a channel-visible cswarm note. If you did receive that signal, the refusal is an authorization one instead: the credential may be revoked or expired, or it may not be a member of this workspace.";
38855
+ }
38770
38856
  async function runReply(args) {
38771
38857
  args.assertShape([
38772
38858
  ...TARGET_FLAGS,
@@ -38798,7 +38884,14 @@ async function runReply(args) {
38798
38884
  about: null,
38799
38885
  ...untilMs2 === void 0 ? {} : { until_ms: untilMs2 }
38800
38886
  };
38801
- const result = await postSignalCommand(cloud, credential, command2);
38887
+ let result;
38888
+ try {
38889
+ result = await postSignalCommand(cloud, credential, command2);
38890
+ } catch (error) {
38891
+ const hint = replyRefusalHint(error);
38892
+ if (hint !== null) throw new Error(hint);
38893
+ throw error;
38894
+ }
38802
38895
  const signal = result.response.signal;
38803
38896
  if (args.has("json")) {
38804
38897
  printJson({
@@ -40355,6 +40448,10 @@ async function main() {
40355
40448
  await runMember(args);
40356
40449
  return;
40357
40450
  }
40451
+ if (verb === "workspace") {
40452
+ await runWorkspace(args);
40453
+ return;
40454
+ }
40358
40455
  if (verb === "target") {
40359
40456
  await runTarget(args);
40360
40457
  return;
@@ -40500,6 +40597,7 @@ ${usage()}
40500
40597
  listenerPermissionMode,
40501
40598
  listenerStatusJson,
40502
40599
  renderRoster,
40600
+ replyRefusalHint,
40503
40601
  resolveDetachedClaudeExecutable,
40504
40602
  resolveDetachedCodexExecutable,
40505
40603
  resolveTurnBudgetOrDefer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "commonswarm",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
4
4
  "description": "CommonSwarm CLI — coordination for teams where people and AI agents work side by side.",
5
5
  "bin": {
6
6
  "cswarm": "cswarm.cjs"