teamclaws 1.0.2 → 1.0.4

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
@@ -51,12 +51,14 @@ Existing event names and schema version remain unchanged. The
51
51
  login keeps running. Signed-out `auth_status` includes the non-executing
52
52
  `login_command: "teamclaws login --json"`; status never starts device authorization.
53
53
 
54
- `servers list` requests `GET /api/cli/v1/servers/?include_inventory=1&include_description=1`. Updated
54
+ `servers list` requests `GET /api/cli/v1/servers/?include_inventory=1&include_description=1&include_editable_metadata=1`. Updated
55
55
  brokers return `servers` and `workspace_count`: the count of all non-deleted
56
56
  owned and eligible shared workspaces, including those without a hostname yet.
57
57
  Individual server entries retain aliases, stable IDs, status and `pairing_ready`.
58
58
  When supported, they also include the server description. Older brokers may
59
59
  omit descriptions; nonempty descriptions appear under each readable list entry.
60
+ The editing opt-in adds `custom_alias` (the owner's saved name, distinct from a
61
+ fallback display name) and `can_edit_metadata`. Older brokers may omit these.
60
62
  Older brokers may omit the count; the CLI reports it as null and never infers
61
63
  an empty account from an empty filtered list.
62
64
 
@@ -69,6 +71,28 @@ purchase a workspace. Existing owners whose machines are being prepared do not
69
71
  need another purchase. Targetless pairing requires explicit selection even for
70
72
  one result; agents present aliases and pass only user-selected stable IDs.
71
73
 
74
+ Owners can edit their server's user-defined name and description:
75
+
76
+ ```text
77
+ teamclaws servers update <server-id> --name "My server" --description "What this server is for"
78
+ ```
79
+
80
+ Provide either flag or both. Omitted fields stay unchanged; an empty string
81
+ clears a field. The name updates the server alias, preserving its system name
82
+ and workspace address. Names are limited to 100 characters and new descriptions
83
+ to 2000. The API enforces ownership; shared access, staff, and superuser roles
84
+ do not grant editing rights to someone else's server.
85
+
86
+ When `servers list` or targetless pairing first discovers an owned server with
87
+ no user-defined name or description, the CLI asks once whether to set them and
88
+ explains the command to edit them later. This prompt is remembered per owner and
89
+ server across devices, including a decline. In `--json` mode, the CLI emits a
90
+ `server_metadata_prompt` event for the agent to show the user, followed by the
91
+ normal result. Agents must ask the question and wait for the user's answer
92
+ before running the edit command. Plain-text commands without an interactive
93
+ terminal do not consume the prompt. The backend atomically claims the prompt;
94
+ a crash after that claim may consume it before it is displayed.
95
+
72
96
  ## Commands
73
97
 
74
98
  ```text
package/dist/app.js CHANGED
@@ -9,7 +9,7 @@ import { PairingClient } from "./pairing.js";
9
9
  import { runShell, shellFetch } from "./shell.js";
10
10
  import { CredentialStore } from "./storage.js";
11
11
  import { installRelease } from "./update.js";
12
- import { parsePairingTarget } from "./validation.js";
12
+ import { normalizeUuid, parsePairingTarget } from "./validation.js";
13
13
  export async function run(arguments_, dependencies = {}, signal) {
14
14
  let parsed;
15
15
  try {
@@ -120,9 +120,15 @@ export async function run(arguments_, dependencies = {}, signal) {
120
120
  }
121
121
  case "servers_list": {
122
122
  const inventory = await broker.servers(token, signal);
123
+ await offerServerMetadata(inventory.servers, broker, token, parsed.json, output, dependencies.io, signal);
123
124
  output.servers(inventory.servers, inventory.workspace_count);
124
125
  return 0;
125
126
  }
127
+ case "servers_update": {
128
+ const server = await broker.updateServerMetadata(token, parsed.serverId, parsed.updates, signal);
129
+ output.serverMetadataUpdated(server);
130
+ return 0;
131
+ }
126
132
  case "pair": {
127
133
  const target = parsed.target
128
134
  ? parsePairingTarget(parsed.target)
@@ -154,6 +160,34 @@ export async function run(arguments_, dependencies = {}, signal) {
154
160
  }
155
161
  }
156
162
  export function parseArguments(arguments_) {
163
+ if (arguments_[0] === "servers" && arguments_[1] === "update") {
164
+ const serverId = normalizeUuid(requiredPositional(arguments_, 2));
165
+ const updates = {};
166
+ let json = false;
167
+ for (let index = 3; index < arguments_.length; index += 1) {
168
+ const option = arguments_[index];
169
+ if (option === "--name" && updates.alias === undefined) {
170
+ const value = arguments_[++index];
171
+ if (value === undefined)
172
+ throw usageError();
173
+ updates.alias = value;
174
+ }
175
+ else if (option === "--description" &&
176
+ updates.description === undefined) {
177
+ const value = arguments_[++index];
178
+ if (value === undefined)
179
+ throw usageError();
180
+ updates.description = value;
181
+ }
182
+ else if (option === "--json" && !json)
183
+ json = true;
184
+ else
185
+ throw usageError();
186
+ }
187
+ if (Object.keys(updates).length === 0)
188
+ throw usageError();
189
+ return { action: "servers_update", serverId, updates, json };
190
+ }
157
191
  if (arguments_[0] === "shell") {
158
192
  const target = requiredPositional(arguments_, 1);
159
193
  let command;
@@ -350,6 +384,7 @@ function nonNegativeInteger(value) {
350
384
  }
351
385
  async function discoverTarget(broker, token, json, output, io, signal) {
352
386
  const inventory = await broker.servers(token, signal);
387
+ await offerServerMetadata(inventory.servers, broker, token, json, output, io, signal);
353
388
  const servers = inventory.servers.filter((server) => server.pairing_ready);
354
389
  if (servers.length === 0) {
355
390
  throw new TeamClawsError("no_pairing_ready_workspaces", inventory.workspace_count === 0
@@ -364,6 +399,49 @@ async function discoverTarget(broker, token, json, output, io, signal) {
364
399
  const selected = await promptForServer(servers, io?.input ?? process.stdin, io?.stdout ?? process.stdout);
365
400
  return { server_id: selected.server_id };
366
401
  }
402
+ async function offerServerMetadata(servers, broker, token, json, output, io, signal) {
403
+ const interactive = io?.interactive ?? (process.stdin.isTTY && process.stdout.isTTY);
404
+ if (!json && !interactive)
405
+ return;
406
+ for (const server of servers) {
407
+ if (server.can_edit_metadata !== true ||
408
+ server.custom_alias === undefined ||
409
+ server.description === undefined ||
410
+ (server.custom_alias.trim() !== "" && server.description.trim() !== ""))
411
+ continue;
412
+ const claimed = await broker.claimMetadataPrompt(token, server.server_id, signal);
413
+ Object.assign(server, claimed.server);
414
+ if (!claimed.should_prompt)
415
+ continue;
416
+ output.serverMetadataPrompt(server);
417
+ if (json)
418
+ continue;
419
+ const terminal = createInterface({
420
+ input: io?.input ?? process.stdin,
421
+ output: (io?.stdout ?? process.stdout),
422
+ });
423
+ try {
424
+ const answer = await terminal.question("Set the name and description now? [y/N] ");
425
+ if (!/^y(?:es)?$/iu.test(answer.trim()))
426
+ continue;
427
+ const name = await terminal.question(`Name (Enter to keep ${JSON.stringify(server.custom_alias || server.alias)}): `);
428
+ const description = await terminal.question("Description (Enter to keep the current description): ");
429
+ const updates = {};
430
+ if (name.trim())
431
+ updates.alias = name;
432
+ if (description.trim())
433
+ updates.description = description;
434
+ if (Object.keys(updates).length > 0) {
435
+ const updated = await broker.updateServerMetadata(token, server.server_id, updates, signal);
436
+ Object.assign(server, updated);
437
+ output.serverMetadataUpdated(updated);
438
+ }
439
+ }
440
+ finally {
441
+ terminal.close();
442
+ }
443
+ }
444
+ }
367
445
  async function promptForServer(servers, input, output) {
368
446
  const terminal = createInterface({
369
447
  input,
@@ -389,5 +467,5 @@ function usageError() {
389
467
  });
390
468
  }
391
469
  export function helpText() {
392
- 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 [--json]\n teamclaws servers list [--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 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`;
470
+ 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 [--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 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`;
393
471
  }
package/dist/broker.js CHANGED
@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
3
3
  import { API_BASE, CLIENT_NAME, CONTRACT_VERSION, MAX_CLIENT_LABEL_LENGTH, MAX_DEVICE_NAME_LENGTH, PACKAGE_VERSION, } from "./constants.js";
4
4
  import { TeamClawsError } from "./errors.js";
5
5
  import { errorFromResponse, requestJson } from "./http.js";
6
- import { parsePendingPairing, parseWorkspaceInventory } from "./validation.js";
6
+ import { assertExactKeys, isObject, normalizeUuid, parsePendingPairing, parseWorkspaceInventory, requireString, } from "./validation.js";
7
7
  export class BrokerClient {
8
8
  fetcher;
9
9
  deviceName;
@@ -15,7 +15,7 @@ export class BrokerClient {
15
15
  return normalizedDeviceName(this.deviceName());
16
16
  }
17
17
  async servers(accessToken, signal) {
18
- const response = await requestJson(this.fetcher, `${API_BASE}servers/?include_inventory=1&include_description=1`, {
18
+ const response = await requestJson(this.fetcher, `${API_BASE}servers/?include_inventory=1&include_description=1&include_editable_metadata=1`, {
19
19
  headers: requestHeaders(accessToken),
20
20
  ...(signal ? { signal } : {}),
21
21
  });
@@ -29,6 +29,47 @@ export class BrokerClient {
29
29
  }
30
30
  return parseWorkspaceInventory(response.body);
31
31
  }
32
+ async updateServerMetadata(accessToken, serverId, updates, signal) {
33
+ if (Object.keys(updates).length === 0 ||
34
+ (updates.alias !== undefined && [...updates.alias.trim()].length > 100) ||
35
+ (updates.description !== undefined &&
36
+ [...updates.description.trim()].length > 2_000)) {
37
+ throw new TeamClawsError("invalid_server_metadata", "Provide a name of 100 characters or fewer and/or a description of 2000 characters or fewer.", { exitCode: 2 });
38
+ }
39
+ const id = normalizeUuid(serverId);
40
+ const response = await requestJson(this.fetcher, `${API_BASE}servers/${id}/metadata/`, {
41
+ method: "POST",
42
+ headers: requestHeaders(accessToken),
43
+ body: updates,
44
+ ...(signal ? { signal } : {}),
45
+ });
46
+ if (response.status < 200 || response.status >= 300)
47
+ throw errorFromResponse(response, "server_metadata_update_failed");
48
+ if (!isObject(response.body))
49
+ throw invalidMetadataResponse();
50
+ assertExactKeys(response.body, ["server"], [], "server metadata response");
51
+ return editableServer(response.body.server, id);
52
+ }
53
+ async claimMetadataPrompt(accessToken, serverId, signal) {
54
+ const id = normalizeUuid(serverId);
55
+ const response = await requestJson(this.fetcher, `${API_BASE}servers/${id}/metadata-prompt/claim/`, {
56
+ method: "POST",
57
+ headers: requestHeaders(accessToken),
58
+ body: {},
59
+ ...(signal ? { signal } : {}),
60
+ });
61
+ if (response.status < 200 || response.status >= 300)
62
+ throw errorFromResponse(response, "server_metadata_prompt_failed");
63
+ if (!isObject(response.body))
64
+ throw invalidMetadataResponse();
65
+ assertExactKeys(response.body, ["should_prompt", "server"], [], "server metadata prompt response");
66
+ if (typeof response.body.should_prompt !== "boolean")
67
+ throw invalidMetadataResponse();
68
+ return {
69
+ should_prompt: response.body.should_prompt,
70
+ server: editableServer(response.body.server, id),
71
+ };
72
+ }
32
73
  async startPairing(target, accessToken, signal) {
33
74
  const request = pairingStartRequest(target, this.currentDeviceName());
34
75
  const response = await requestJson(this.fetcher, `${API_BASE}pairing/start/`, {
@@ -51,6 +92,28 @@ export class BrokerClient {
51
92
  return pending;
52
93
  }
53
94
  }
95
+ function invalidMetadataResponse() {
96
+ return new TeamClawsError("invalid_response", "TeamClaws returned invalid server metadata.", { exitCode: 9 });
97
+ }
98
+ function editableServer(value, expectedId) {
99
+ if (!isObject(value))
100
+ throw invalidMetadataResponse();
101
+ assertExactKeys(value, ["id", "alias", "description", "custom_alias", "can_edit_metadata"], [], "server metadata");
102
+ const id = normalizeUuid(value.id);
103
+ if (id !== expectedId ||
104
+ value.can_edit_metadata !== true ||
105
+ typeof value.custom_alias !== "string" ||
106
+ [...value.custom_alias].length > 100 ||
107
+ typeof value.description !== "string")
108
+ throw invalidMetadataResponse();
109
+ return {
110
+ server_id: id,
111
+ alias: requireString(value.alias, "server alias", 200),
112
+ description: value.description,
113
+ custom_alias: value.custom_alias,
114
+ can_edit_metadata: true,
115
+ };
116
+ }
54
117
  export function pairingStartRequest(target, deviceName) {
55
118
  return {
56
119
  contract_version: CONTRACT_VERSION,
package/dist/constants.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const PACKAGE_NAME = "teamclaws";
2
- export const PACKAGE_VERSION = "1.0.2";
2
+ export const PACKAGE_VERSION = "1.0.4";
3
3
  export const CLIENT_NAME = "teamclaws-cli";
4
4
  export const CONTRACT_VERSION = 1;
5
5
  export const OUTPUT_SCHEMA_VERSION = 1;
package/dist/output.js CHANGED
@@ -112,6 +112,31 @@ export class Output {
112
112
  });
113
113
  }
114
114
  }
115
+ serverMetadataPrompt(server) {
116
+ const updateCommand = `teamclaws servers update ${server.server_id} --name "Your server name" --description "What this server is for"`;
117
+ const guidance = `You can always change the name and description later with: ${updateCommand}`;
118
+ const question = "Would you like to set the server name and description now?";
119
+ this.event({
120
+ schema_version: OUTPUT_SCHEMA_VERSION,
121
+ event: "server_metadata_prompt",
122
+ server,
123
+ question,
124
+ message: `Your server ${JSON.stringify(server.alias)} is missing a user-defined name or description. ${question} ${guidance}`,
125
+ update_command: updateCommand,
126
+ });
127
+ if (!this.json)
128
+ this.stdout.write(`Your server ${JSON.stringify(server.alias)} is missing a user-defined name or description.\n${guidance}\n`);
129
+ }
130
+ serverMetadataUpdated(server) {
131
+ this.event({
132
+ schema_version: OUTPUT_SCHEMA_VERSION,
133
+ event: "server_metadata_updated",
134
+ status: "success",
135
+ server,
136
+ });
137
+ if (!this.json)
138
+ this.stdout.write(`Updated server details for ${server.alias}.\n`);
139
+ }
115
140
  pairingComplete(serverId, origin, displayName, externalChatHandoff, capabilities, generatedAt) {
116
141
  const workspace = {
117
142
  server_id: serverId,
@@ -353,23 +353,35 @@ export function parseServersResponse(value) {
353
353
  exitCode: 9,
354
354
  });
355
355
  }
356
- assertExactKeys(server, ["id", "alias", "workspace_url", "status", "pairing_ready"], ["description"], "server");
356
+ assertExactKeys(server, ["id", "alias", "workspace_url", "status", "pairing_ready"], ["description", "custom_alias", "can_edit_metadata"], "server");
357
357
  if (typeof server.pairing_ready !== "boolean") {
358
358
  throw new TeamClawsError("invalid_response", "TeamClaws returned an invalid server.", {
359
359
  exitCode: 9,
360
360
  });
361
361
  }
362
362
  if (server.description !== undefined &&
363
- (typeof server.description !== "string" ||
364
- server.description.length > 2_000)) {
363
+ typeof server.description !== "string") {
365
364
  throw new TeamClawsError("invalid_response", "TeamClaws returned an invalid server description.", { exitCode: 9 });
366
365
  }
366
+ if ((server.custom_alias !== undefined &&
367
+ (typeof server.custom_alias !== "string" ||
368
+ [...server.custom_alias].length > 100)) ||
369
+ (server.can_edit_metadata !== undefined &&
370
+ typeof server.can_edit_metadata !== "boolean")) {
371
+ throw new TeamClawsError("invalid_response", "TeamClaws returned invalid server editing permissions.", { exitCode: 9 });
372
+ }
367
373
  return {
368
374
  server_id: normalizeUuid(server.id),
369
375
  alias: requireString(server.alias, "server alias", 200),
370
376
  ...(typeof server.description === "string"
371
377
  ? { description: server.description }
372
378
  : {}),
379
+ ...(typeof server.custom_alias === "string"
380
+ ? { custom_alias: server.custom_alias }
381
+ : {}),
382
+ ...(typeof server.can_edit_metadata === "boolean"
383
+ ? { can_edit_metadata: server.can_edit_metadata }
384
+ : {}),
373
385
  workspace_url: canonicalWorkspaceUrl(server.workspace_url),
374
386
  status: requireString(server.status, "server status", 40),
375
387
  pairing_ready: server.pairing_ready,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamclaws",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
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",
@@ -33,8 +33,19 @@ failures; report the actual result without claiming success.
33
33
  ## Choose workspaces
34
34
 
35
35
  Run `teamclaws servers list --json`. Use only a successful `servers_list` event;
36
- errors are not empty inventories. Display aliases, retaining `server_id` for
37
- commands. Explain readiness separately from ownership.
36
+ errors are not empty inventories. Show each server's display name (`alias`) and
37
+ returned `description` to the user, retaining `server_id` for commands. If a
38
+ description is empty or absent, say "No description set." Explain readiness
39
+ separately from ownership.
40
+
41
+ If discovery emits `server_metadata_prompt`, show its question and explain that
42
+ the owner can always change the server's name and description later. Ask for the
43
+ desired values if the user wants to edit them; run `teamclaws servers update
44
+ <server-id> --name <text> --description <text> --json` only after their answer.
45
+ Either flag may be omitted to keep that field unchanged. This question is
46
+ remembered once per owner and server across devices, including a decline, so
47
+ deliver it when received. Never offer this edit for a shared server whose
48
+ `can_edit_metadata` is false, and never infer ownership from an alias or role.
38
49
 
39
50
  - `discovery_state: no_workspaces` means the broker confirmed zero workspaces
40
51
  accessible for pairing, including owned workspaces still being prepared.
@@ -46,10 +57,11 @@ commands. Explain readiness separately from ownership.
46
57
  list does not prove an empty account. Ask the user to check their workspaces at
47
58
  https://teamclaws.com/app/; only if they confirm they have none, direct them to
48
59
  purchase there. Do not claim inventory support is deployed from local code alone.
49
- - With one pairing-ready workspace, name its alias and ask, “Would you like to
50
- pair with **[alias]** now?” Never automatically pair it.
51
- - With several, list aliases and ask, “Would you like to pair with any of these
52
- now?” If aliases repeat, distinguish them using their workspace addresses.
60
+ - With one pairing-ready workspace, show its alias and description and ask,
61
+ “Would you like to pair with **[alias]** now?” Never automatically pair it.
62
+ - With several, list each alias and description and ask, “Would you like to pair
63
+ with any of these now?” If aliases repeat, distinguish them using their
64
+ workspace addresses.
53
65
 
54
66
  For each selected workspace, follow [teamclaws-pair](../teamclaws-pair/SKILL.md)
55
67
  with its stable identifier. A prior explicit selection already authorizes that
@@ -55,9 +55,11 @@ Use `--no-open` only when automatic browser opening is undesirable. The
55
55
  `pairing_browser_required` event still provides the safe approval address.
56
56
 
57
57
  When no selected stable identifier is known, run `teamclaws servers list --json`,
58
- show aliases and readiness, and ask which workspace the user wants to pair—even
59
- when only one is available. Pass the selected `server_id`; aliases are display
60
- labels, not unique command targets. Targetless `teamclaws pair --json` returns
58
+ show each server's display name (`alias`), returned `description`, and readiness,
59
+ and ask which workspace the user wants to pair—even when only one is available.
60
+ If a description is empty or absent, say "No description set." Pass the selected
61
+ `server_id`; aliases are display labels, not unique command targets.
62
+ Targetless `teamclaws pair --json` returns
61
63
  `server_selection_required` for any nonempty pairing-ready list and never
62
64
  creates approval automatically. Interactive targetless pairing also requires a
63
65
  selection. An empty or not-ready list is not proof that the account owns no