teamclaws 2.0.3 → 2.0.6

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
@@ -172,6 +172,7 @@ npx teamclaws pair status <server-identifier-or-canonical-workspace-address>
172
172
  npx teamclaws pair
173
173
  npx teamclaws pair <server-identifier-or-canonical-workspace-address> [--no-open]
174
174
  npx teamclaws shell <target> --command <text> [--json]
175
+ npx teamclaws users add <target> <email> --role <user|staff|superuser> [--json]
175
176
  npx teamclaws chat start <target> --message <text>
176
177
  npx teamclaws chat send <target> --session-id <uuid> --message <text>
177
178
  npx teamclaws chat watch <target> --session-id <uuid> --request-id <uuid>
@@ -213,6 +214,22 @@ Python helper. Use `--client-request-id <uuid>` to safely repeat an uncertain
213
214
  create request, and use the watch cursors printed in JSON events to resume
214
215
  monitoring without creating another turn.
215
216
 
217
+ ## Add a workspace user directly
218
+
219
+ `teamclaws users add <target> <email> --role user --json` sends a direct request
220
+ using the saved paired machine credential and its bound authorization. The role
221
+ is required; supported roles are `user`, `staff`, and `superuser`. The server
222
+ verifies current owner authority and the shared six-calendar-month machine
223
+ authorization deadline. Initial CLI login and pairing remain the normal setup.
224
+
225
+ This operation requires no agent, AI provider, chat, shell, skill, or browser
226
+ session. Missing or inactive credentials return an error without opening a browser.
227
+ The result's `requested_role` is the requested access, not a change to an existing
228
+ member's role. `invited` means an invitation was sent; access starts after
229
+ acceptance. Existing memberships and pending invitations are preserved without
230
+ promotion or resend. If a request has an uncertain network outcome, inspect the
231
+ invitation status before trying again; the command does not blindly retry it.
232
+
216
233
  ## Read chat history without a browser
217
234
 
218
235
  ```text
package/dist/app.js CHANGED
@@ -11,6 +11,7 @@ import { CredentialStore } from "./storage.js";
11
11
  import { TargetAuthorization } from "./target-authorization.js";
12
12
  import { installRelease } from "./update.js";
13
13
  import { normalizeUuid, parsePairingTarget } from "./validation.js";
14
+ import { runUsersAdd } from "./users.js";
14
15
  export async function run(arguments_, dependencies = {}, signal) {
15
16
  let parsed;
16
17
  try {
@@ -70,6 +71,19 @@ export async function run(arguments_, dependencies = {}, signal) {
70
71
  }
71
72
  const store = dependencies.store ?? new CredentialStore();
72
73
  const targetAuthorization = new TargetAuthorization(store, dependencies.fetcher);
74
+ if (parsed.action === "users_add") {
75
+ try {
76
+ await targetAuthorization.requireGrant();
77
+ const result = await runUsersAdd(store, dependencies.fetcher ?? fetch, parsed.target, parsed.email, parsed.role, targetAuthorization, signal);
78
+ output.usersAddResult(result);
79
+ return 0;
80
+ }
81
+ catch (error) {
82
+ const safe = safeError(error);
83
+ output.error(safe);
84
+ return safe.exitCode;
85
+ }
86
+ }
73
87
  const auth = new AuthService({
74
88
  store,
75
89
  output,
@@ -217,6 +231,26 @@ export function parseArguments(arguments_) {
217
231
  }
218
232
  const json = arguments_.includes("--json");
219
233
  const args = arguments_.filter((argument) => argument !== "--json");
234
+ if (args[0] === "users" && args[1] === "add") {
235
+ const target = parsePairingTarget(requiredPositional(args, 2));
236
+ const email = requiredPositional(args, 3).trim().toLowerCase();
237
+ const options = parseNamedOptions(args.slice(4), {
238
+ values: ["--role"],
239
+ booleans: [],
240
+ });
241
+ const role = options.values["--role"];
242
+ if (!email ||
243
+ email.length > 254 ||
244
+ !["user", "staff", "superuser"].includes(role ?? ""))
245
+ throw usageError();
246
+ return {
247
+ action: "users_add",
248
+ json,
249
+ target,
250
+ email,
251
+ role: role,
252
+ };
253
+ }
220
254
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h")
221
255
  return { action: "help", json };
222
256
  if (args[0] === "--version" || args[0] === "-v")
@@ -498,5 +532,5 @@ function usageError() {
498
532
  });
499
533
  }
500
534
  export function helpText() {
501
- return `TeamClaws ${PACKAGE_VERSION}\n\nUsage:\n teamclaws update [--json]\n teamclaws shell <target> --command <text> [--json]\n teamclaws login [email] [--json]\n teamclaws auth status [--json]\n teamclaws logout [--revoke] [--json]\n teamclaws servers list [--json]\n teamclaws servers update <server-id> [--name <text>] [--description <text>] [--json]\n teamclaws pair status <server-identifier-or-canonical-workspace-address> [--json]\n teamclaws pair [server-identifier-or-canonical-workspace-address] [--no-open] [--json]\n teamclaws chat list <target> [--json]\n teamclaws chat history <target> [--json]\n teamclaws chat messages <target> --chat-id <uuid> [--after-message-id <n>] [--json]\n teamclaws chat capabilities <target> [--json]\n teamclaws chat start <target> --message <text> [selection options] [--json]\n teamclaws chat send <target> --session-id <uuid> --message <text> [--json]\n teamclaws chat watch <target> --session-id <uuid> --request-id <uuid> [resume cursors] [--json]\n\nPair status performs one bounded direct verification and stops. Pairing grants the standard host-issued credential. Chat commands use that credential directly against the selected workspace.\n`;
535
+ return `TeamClaws ${PACKAGE_VERSION}\n\nUsage:\n teamclaws update [--json]\n teamclaws shell <target> --command <text> [--json]\n teamclaws users add <target> <email> --role <user|staff|superuser> [--json]\n teamclaws login [email] [--json]\n teamclaws auth status [--json]\n teamclaws logout [--revoke] [--json]\n teamclaws servers list [--json]\n teamclaws servers update <server-id> [--name <text>] [--description <text>] [--json]\n teamclaws pair status <server-identifier-or-canonical-workspace-address> [--json]\n teamclaws pair [server-identifier-or-canonical-workspace-address] [--no-open] [--json]\n teamclaws chat list <target> [--json]\n teamclaws chat history <target> [--json]\n teamclaws chat messages <target> --chat-id <uuid> [--after-message-id <n>] [--json]\n teamclaws chat capabilities <target> [--json]\n teamclaws chat start <target> --message <text> [selection options] [--json]\n teamclaws chat send <target> --session-id <uuid> --message <text> [--json]\n teamclaws chat watch <target> --session-id <uuid> --request-id <uuid> [resume cursors] [--json]\n\nPair status performs one bounded direct verification and stops. Pairing grants the standard host-issued credential. Chat commands use that credential directly against the selected workspace.\n`;
502
536
  }
package/dist/broker.js CHANGED
@@ -94,9 +94,6 @@ export class BrokerClient {
94
94
  throw errorFromResponse(response, "pairing_start_failed");
95
95
  }
96
96
  const pending = parsePendingPairing(response.body);
97
- if ("server_id" in target && pending.server_id !== target.server_id) {
98
- throw new TeamClawsError("target_mismatch", "TeamClaws resolved a different workspace than the one requested.", { exitCode: 9 });
99
- }
100
97
  if ("workspace_url" in target &&
101
98
  pending.target.origin !== new URL(target.workspace_url).origin) {
102
99
  throw new TeamClawsError("target_mismatch", "TeamClaws resolved a different workspace than the one requested.", { exitCode: 9 });
package/dist/constants.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const PACKAGE_NAME = "teamclaws";
2
- export const PACKAGE_VERSION = "2.0.3";
2
+ export const PACKAGE_VERSION = "2.0.6";
3
3
  export const CLIENT_NAME = "teamclaws-cli";
4
4
  export const CONTRACT_VERSION = 1;
5
5
  export const OUTPUT_SCHEMA_VERSION = 1;
@@ -63,7 +63,7 @@ export class ExternalChatClient {
63
63
  const remainingAttempt = deadline - this.now();
64
64
  if (remainingAttempt < 1)
65
65
  break;
66
- const timeoutMs = Math.max(1, Math.floor(Math.min(15_000, remainingAttempt)));
66
+ const timeoutMs = Math.max(1, Math.floor(Math.min(60_000, remainingAttempt)));
67
67
  const timeout = AbortSignal.timeout(timeoutMs);
68
68
  const attemptSignal = signal
69
69
  ? AbortSignal.any([timeout, signal])
@@ -297,7 +297,7 @@ export class ExternalChatClient {
297
297
  capabilities: parseCapabilities(result.external_chat_capabilities, true),
298
298
  };
299
299
  }
300
- const capabilityResponse = await this.request(new URL(handoff.capabilities_url, `${credential.origin}/`).toString(), credential, {}, [200], signal);
300
+ const capabilityResponse = await this.request(new URL(handoff.capabilities_url, `${credential.origin}/`).toString(), credential, { timeoutMs: 60_000 }, [200], signal);
301
301
  return {
302
302
  credential,
303
303
  capabilities: parseCapabilities(capabilityResponse.body),
@@ -375,6 +375,7 @@ export class ExternalChatClient {
375
375
  for (let attempt = 0; attempt < 3; attempt += 1) {
376
376
  try {
377
377
  const response = await requestTargetJson(this.fetcher, this.authorization, url, credential, {
378
+ ...(options.method === "POST" ? { timeoutMs: 60_000 } : {}),
378
379
  ...options,
379
380
  headers: {
380
381
  ...options.headers,
package/dist/output.js CHANGED
@@ -389,6 +389,22 @@ export class Output {
389
389
  else
390
390
  this.stderr.write(`Error: ${error.message}\n`);
391
391
  }
392
+ usersAddResult(result) {
393
+ this.event({
394
+ schema_version: OUTPUT_SCHEMA_VERSION,
395
+ event: "users_add_result",
396
+ ...result,
397
+ });
398
+ if (!this.json) {
399
+ const messages = {
400
+ invited: `Invitation sent for ${result.requested_role} access. Access begins after acceptance.`,
401
+ already_pending: "An invitation is already pending; it was not resent.",
402
+ already_member: "Already a member; access unchanged.",
403
+ already_owner: "Already the owner; access unchanged.",
404
+ };
405
+ this.stdout.write(`${messages[result.code]}\n`);
406
+ }
407
+ }
392
408
  event(event) {
393
409
  if (this.json)
394
410
  this.stdout.write(`${JSON.stringify(event)}\n`);
package/dist/pairing.js CHANGED
@@ -84,7 +84,6 @@ export class PairingClient {
84
84
  credential = await this.poll(pending, signal);
85
85
  const grant = await this.authorization.requireGrant();
86
86
  if (credential.pairing_id !== credential.device_id ||
87
- credential.server_id !== pending.server_id ||
88
87
  credential.origin !== pending.target.origin ||
89
88
  credential.requester_sub !== grant.sub ||
90
89
  credential.authorization_id !== grant.authorization_id ||
package/dist/users.js ADDED
@@ -0,0 +1,78 @@
1
+ import { TeamClawsError } from "./errors.js";
2
+ import { requestTargetJson, } from "./target-authorization.js";
3
+ import { isObject } from "./validation.js";
4
+ const failureMessages = {
5
+ invalid_actor_token: "The machine authorization is invalid, expired, paused, or revoked.",
6
+ unauthorized: "The workspace rejected the paired machine credential.",
7
+ target_proof_expired: "The request proof expired before user addition.",
8
+ forbidden: "The paired identity is not authorized to add users.",
9
+ owner_required: "Only the current machine owner can add users.",
10
+ actor_machine_mismatch: "The authorization does not match this machine or its paired identity.",
11
+ actor_authorization_unavailable: "Machine authorization could not be verified. Try again later.",
12
+ authorization_unavailable: "Machine authorization could not be verified. Try again later.",
13
+ machine_auth_failed: "The workspace's machine authentication was rejected.",
14
+ machine_ip_rejected: "The workspace's machine address was rejected.",
15
+ mfa_required: "The owner must meet the existing MFA policy before adding users.",
16
+ invalid_email: "Enter a valid email address.",
17
+ invalid_role: "Choose user, staff, or superuser access.",
18
+ invalid_request: "The workspace rejected the add-user request.",
19
+ invite_delivery_failed: "The invitation could not be delivered. Check its status before retrying.",
20
+ outcome_unverified: "The invitation outcome could not be verified. Check its status before retrying.",
21
+ };
22
+ export async function runUsersAdd(store, fetcher, target, email, role, authorization, signal) {
23
+ const credential = await store.findWorkspace(target);
24
+ if (!credential)
25
+ throw new TeamClawsError("re_pair_required", "No saved pairing was found for this workspace. Pair it first.", { exitCode: 3 });
26
+ let response;
27
+ try {
28
+ response = await requestTargetJson(fetcher, authorization, `${credential.origin}/teamclaws-admin/api/device-users/v1/add/`, credential, {
29
+ method: "POST",
30
+ body: { email, role },
31
+ timeoutMs: 60_000,
32
+ ...(signal ? { signal } : {}),
33
+ });
34
+ }
35
+ catch (error) {
36
+ if (error instanceof TeamClawsError &&
37
+ ["network_error", "network_timeout"].includes(error.code))
38
+ throw new TeamClawsError("outcome_unverified", failureMessages.outcome_unverified);
39
+ throw error;
40
+ }
41
+ const body = response.body;
42
+ if (response.status < 200 ||
43
+ response.status >= 300 ||
44
+ (isObject(body) && body.ok === false)) {
45
+ const rawCode = isObject(body)
46
+ ? (body.code ?? (isObject(body.error) ? body.error.code : body.error))
47
+ : undefined;
48
+ const code = typeof rawCode === "string" && Object.hasOwn(failureMessages, rawCode)
49
+ ? rawCode
50
+ : response.status === 404 || response.status === 405
51
+ ? "users_add_unavailable"
52
+ : "outcome_unverified";
53
+ throw new TeamClawsError(code, failureMessages[code] ??
54
+ (code === "users_add_unavailable"
55
+ ? "Direct user addition is unavailable on this workspace."
56
+ : "The workspace could not verify the add-user result."), {
57
+ exitCode: response.status === 401 ? 3 : response.status === 429 ? 5 : 1,
58
+ });
59
+ }
60
+ if (!isObject(body) ||
61
+ body.ok !== true ||
62
+ body.action !== "add" ||
63
+ body.scope !== "access_group" ||
64
+ body.email !== email ||
65
+ body.requested_role !== role ||
66
+ !((response.status === 201 && body.code === "invited") ||
67
+ (response.status === 200 &&
68
+ ["already_pending", "already_member", "already_owner"].includes(String(body.code)))))
69
+ throw new TeamClawsError("invalid_response", "The workspace returned an invalid add-user result. Check the invitation status before retrying.", { exitCode: 9 });
70
+ return {
71
+ ok: true,
72
+ code: body.code,
73
+ action: "add",
74
+ scope: "access_group",
75
+ email,
76
+ requested_role: role,
77
+ };
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamclaws",
3
- "version": "2.0.3",
3
+ "version": "2.0.6",
4
4
  "description": "Official TeamClaws command-line client and agent skills",
5
5
  "license": "PolyForm-Shield-1.0.0",
6
6
  "homepage": "https://teamclaws.com",