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 +27 -1
- package/dist/app.js +80 -2
- package/dist/broker.js +65 -2
- package/dist/constants.js +1 -1
- package/dist/output.js +27 -0
- package/dist/validation.js +21 -1
- package/package.json +1 -1
- package/skills/teamclaws-onboard/SKILL.md +9 -0
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
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,
|
package/dist/validation.js
CHANGED
|
@@ -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
|
@@ -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
|