teamclaws 1.0.1 → 1.0.3

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,10 +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`. 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
+ When supported, they also include the server description. Older brokers may
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.
58
62
  Older brokers may omit the count; the CLI reports it as null and never infers
59
63
  an empty account from an empty filtered list.
60
64
 
@@ -67,6 +71,28 @@ purchase a workspace. Existing owners whose machines are being prepared do not
67
71
  need another purchase. Targetless pairing requires explicit selection even for
68
72
  one result; agents present aliases and pass only user-selected stable IDs.
69
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
+
70
96
  ## Commands
71
97
 
72
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`, {
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.1";
2
+ export const PACKAGE_VERSION = "1.0.3";
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
@@ -107,9 +107,36 @@ export class Output {
107
107
  }
108
108
  servers.forEach((server, index) => {
109
109
  this.stdout.write(`${index + 1}. ${server.alias} — ${server.server_id} — ${server.workspace_url} — ${server.pairing_ready ? "ready to pair" : "not ready to pair"}\n`);
110
+ if (server.description)
111
+ this.stdout.write(` ${server.description}\n`);
110
112
  });
111
113
  }
112
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
+ }
113
140
  pairingComplete(serverId, origin, displayName, externalChatHandoff, capabilities, generatedAt) {
114
141
  const workspace = {
115
142
  server_id: serverId,
@@ -353,15 +353,35 @@ export function parseServersResponse(value) {
353
353
  exitCode: 9,
354
354
  });
355
355
  }
356
- assertExactKeys(server, ["id", "alias", "workspace_url", "status", "pairing_ready"], [], "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
+ if (server.description !== undefined &&
363
+ typeof server.description !== "string") {
364
+ throw new TeamClawsError("invalid_response", "TeamClaws returned an invalid server description.", { exitCode: 9 });
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
+ }
362
373
  return {
363
374
  server_id: normalizeUuid(server.id),
364
375
  alias: requireString(server.alias, "server alias", 200),
376
+ ...(typeof server.description === "string"
377
+ ? { description: server.description }
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
+ : {}),
365
385
  workspace_url: canonicalWorkspaceUrl(server.workspace_url),
366
386
  status: requireString(server.status, "server status", 40),
367
387
  pairing_ready: server.pairing_ready,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamclaws",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
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",
@@ -36,6 +36,15 @@ Run `teamclaws servers list --json`. Use only a successful `servers_list` event;
36
36
  errors are not empty inventories. Display aliases, retaining `server_id` for
37
37
  commands. Explain readiness separately from ownership.
38
38
 
39
+ If discovery emits `server_metadata_prompt`, show its question and explain that
40
+ the owner can always change the server's name and description later. Ask for the
41
+ desired values if the user wants to edit them; run `teamclaws servers update
42
+ <server-id> --name <text> --description <text> --json` only after their answer.
43
+ Either flag may be omitted to keep that field unchanged. This question is
44
+ remembered once per owner and server across devices, including a decline, so
45
+ deliver it when received. Never offer this edit for a shared server whose
46
+ `can_edit_metadata` is false, and never infer ownership from an alias or role.
47
+
39
48
  - `discovery_state: no_workspaces` means the broker confirmed zero workspaces
40
49
  accessible for pairing, including owned workspaces still being prepared.
41
50
  Direct the user to https://teamclaws.com/app/ to purchase a workspace, then