teamclaws 1.0.0 → 1.0.2

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
@@ -12,6 +12,63 @@ package.
12
12
  GNU/Linux distribution with glibc 2.28 or newer
13
13
  - x64 or ARM64
14
14
 
15
+ ## Installation
16
+
17
+ Install the published CLI:
18
+
19
+ ```text
20
+ npm install -g teamclaws@latest
21
+ teamclaws --version
22
+ ```
23
+
24
+ To install a downloaded release tarball:
25
+
26
+ ```text
27
+ npm install -g ./teamclaws-1.0.1.tgz
28
+ teamclaws --version
29
+ ```
30
+
31
+ GitHub source ZIPs must be built before use; they are not npm installers.
32
+
33
+ ## Agent onboarding
34
+
35
+ > Install teamclaws@latest, read its agent skills, and help me connect to my cloud workspace.
36
+
37
+ Start with [teamclaws-onboard](skills/teamclaws-onboard/SKILL.md). For a global
38
+ installation, `npm root -g` locates the package root; read
39
+ `teamclaws/skills/teamclaws-onboard/SKILL.md` beneath it. The package contains one
40
+ shared workflow for login, alias-based user-selected pairing (including a single
41
+ workspace), additional pairings, project transfer, and optional public/private
42
+ live hosting. Login and pairing mechanics remain in their respective skills;
43
+ [teamclaws-project](skills/teamclaws-project/SKILL.md) covers project work through
44
+ the existing paired shell. Status-only requests do not start onboarding.
45
+
46
+ ### Onboarding output contract
47
+
48
+ Existing event names and schema version remain unchanged. The
49
+ `login_browser_required` event provides the safe `url`, a `label` of
50
+ `Sign in to TeamClaws`, and `account_guidance`. Display these immediately while
51
+ login keeps running. Signed-out `auth_status` includes the non-executing
52
+ `login_command: "teamclaws login --json"`; status never starts device authorization.
53
+
54
+ `servers list` requests `GET /api/cli/v1/servers/?include_inventory=1&include_description=1`. Updated
55
+ brokers return `servers` and `workspace_count`: the count of all non-deleted
56
+ owned and eligible shared workspaces, including those without a hostname yet.
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
+ Older brokers may omit the count; the CLI reports it as null and never infers
61
+ an empty account from an empty filtered list.
62
+
63
+ The `servers_list` event adds `workspace_count` and `discovery_state`:
64
+ `no_workspaces` only for a verified count of zero, `ready` when any listed
65
+ workspace is pairing-ready, `not_ready` when workspaces exist but none is ready,
66
+ and `unknown` for an empty legacy response without a count. Discovery errors
67
+ remain error events. A verified empty account uses https://teamclaws.com/app/ to
68
+ purchase a workspace. Existing owners whose machines are being prepared do not
69
+ need another purchase. Targetless pairing requires explicit selection even for
70
+ one result; agents present aliases and pass only user-selected stable IDs.
71
+
15
72
  ## Commands
16
73
 
17
74
  ```text
@@ -23,6 +80,7 @@ npx teamclaws servers list
23
80
  npx teamclaws pair status <server-identifier-or-canonical-workspace-address>
24
81
  npx teamclaws pair
25
82
  npx teamclaws pair <server-identifier-or-canonical-workspace-address> [--no-open]
83
+ npx teamclaws shell <target> --command <text> [--json]
26
84
  npx teamclaws chat start <target> --message <text>
27
85
  npx teamclaws chat send <target> --session-id <uuid> --message <text>
28
86
  npx teamclaws chat watch <target> --session-id <uuid> --request-id <uuid>
@@ -31,6 +89,13 @@ npx teamclaws chat watch <target> --session-id <uuid> --request-id <uuid>
31
89
  Human-readable output is the default. Add `--json` for the strict, versioned,
32
90
  newline-delimited JSON event stream intended for automation.
33
91
 
92
+ Pairing (including explicit targets), server listing, pair status, chat, and shell
93
+ require a valid TeamClaws CLI account login. Run `teamclaws login` first. The CLI
94
+ refreshes and verifies that login before contacting the workspace or requesting
95
+ pairing approval. Missing or invalid authentication exits with code 3 in both
96
+ human and JSON modes. Help, version, login, auth status, and logout remain
97
+ available. Saved workspace credentials are preserved when login is required.
98
+
34
99
  Pairing is broad privileged delegation to the approved workspace. The client
35
100
  does not request or select scopes, profiles, capability bundles, roles, user
36
101
  types, or permission labels. TeamClaws, Clawgate, and host policy determine the
@@ -40,8 +105,8 @@ The client never accepts a model or harness access token. An agent that already
40
105
  discovered a workspace passes only the selected stable server identifier to the
41
106
  `pair` command.
42
107
 
43
- `pair status` is side-effect free: it reads only the matching protected local
44
- credential, performs at most one direct workspace verification with a
108
+ After account authentication, `pair status` reads the matching protected local
109
+ workspace credential, performs at most one direct workspace verification with a
45
110
  five-second timeout, reports the result, and stops. It never starts pairing or
46
111
  loads external-chat resources.
47
112
 
@@ -54,6 +119,25 @@ Python helper. Use `--client-request-id <uuid>` to safely repeat an uncertain
54
119
  create request, and use the watch cursors printed in JSON events to resume
55
120
  monitoring without creating another turn.
56
121
 
122
+ ## Host shell commands
123
+
124
+ `teamclaws shell <target> --command <text> [--json]` uses an existing saved
125
+ pairing to execute the exact command text on the workspace host. Quote the
126
+ command as one local shell argument; quotes, pipes and newlines inside that
127
+ argument are preserved. Each request starts in the executing account's home
128
+ directory. The command does not log in, pair, or load chat capabilities.
129
+
130
+ Normal mode forwards stdout and stderr without adding text. JSON mode emits one
131
+ object with `stdout`, `stderr`, and `exit_code`. Both modes return the remote
132
+ process exit code. HTTP/authentication/transport errors use the existing CLI
133
+ error output and failure codes.
134
+
135
+ The client sends one POST to `/teamclaws-admin/api/shell/` with
136
+ `Content-Type: text/plain; charset=utf-8`, never follows redirects, and never
137
+ retries execution. The shell path has no command or HTTP operation deadline,
138
+ command-size limit, or response/output-size limit. A network failure can leave the
139
+ execution outcome unknown; inspect the host before deciding to run it again.
140
+
57
141
  ## Local storage
58
142
 
59
143
  Login credentials are stored in the private TeamClaws user directory. Completed
@@ -63,13 +147,6 @@ directories use mode `0700` and files use mode `0600`; Windows uses
63
147
  inheritance-disabled access-control entries limited to the current user and the
64
148
  Windows system account.
65
149
 
66
- ## Development
67
-
68
- ```text
69
- npm ci
70
- npm run verify
71
- ```
72
-
73
150
  The package is licensed under the PolyForm Shield License 1.0.0. Paid service
74
151
  access is enforced by TeamClaws servers, not by the client license.
75
152
 
@@ -79,7 +156,7 @@ Run `teamclaws chat capabilities <target> [--json]` with an existing saved
79
156
  workspace pairing. It reads the workspace capabilities endpoint with a shared
80
157
  120-second budget, attempts lasting at most 15 seconds, and pauses of up to five
81
158
  seconds between retryable failures or inventories with no model choices. It does not
82
- log in, pair, or start a chat. Without a saved pairing it makes no request.
159
+ log in, pair, or start a chat. Without a saved pairing it makes no workspace request.
83
160
 
84
161
  JSON output is one `chat_capabilities` event with `schema_version: 1`,
85
162
  `status: "success"`, `source: "refresh"`, the safe workspace ID/origin, and
@@ -104,4 +181,79 @@ The timestamp describes snapshot generation, not an authorization lease. The
104
181
  existing refresh endpoint does not supply a generation timestamp. Chat requests
105
182
  still validate current authorization and selection; explicitly refresh if a
106
183
  selection is no longer available. No chat mutation is automatically replayed by
107
- this command. Pair status retains its one-request, terminal behavior.
184
+ this command. Pair status retains its single workspace verification and terminal behavior.
185
+
186
+ ## CLI updates
187
+
188
+ Every command invocation checks `https://registry.npmjs.org/teamclaws` before
189
+ command dispatch, including help and version. The check has a 1.5-second total
190
+ deadline, a 2 MiB metadata limit, no redirects, no credentials, and no persistent
191
+ cache. Registry failure or malformed metadata silently leaves ordinary commands
192
+ available. Nothing is installed automatically.
193
+
194
+ When a newer published stable `latest` release exists, stderr receives:
195
+
196
+ ```text
197
+ Version 1.2.0 Available starting on 09/12/2026. Use command teamclaws update to update as soon as possible.
198
+ ```
199
+
200
+ With `--json`, stderr receives one newline-delimited `update_available` JSON
201
+ object containing `version`, `published_at`, `available_date`, `update_command`,
202
+ and `message`. Agents should capture both streams. Existing stdout results,
203
+ shell result objects, event streams, help/version text, and command exit codes
204
+ retain their contracts. Raw shell stderr follows the advisory unchanged; callers
205
+ requiring only remote stderr must separate the first advisory line. Long-running
206
+ commands receive the notice once at startup, not on every streamed event.
207
+
208
+ Run `teamclaws update` (or `teamclaws update --json`) explicitly to install.
209
+ The command does not require login. It supports only a verified npm global
210
+ installation matching the running package and npm's reported global prefix.
211
+ Local, linked, npx, other package-manager installations, mismatched prefixes,
212
+ and layouts without npm beside the running Node executable are unsupported;
213
+ update those through their original package manager. No prefix is guessed and
214
+ no automatic privilege elevation occurs.
215
+
216
+ The installer uses Node to launch npm's JavaScript entrypoint with literal
217
+ arguments on Windows, Linux, and macOS. It pins the official registry, package,
218
+ and checked version, disables lifecycle scripts, and verifies the installed
219
+ package version afterward. It never accesses the `.teamclaws` credential tree:
220
+ login and workspace credentials remain separate from the installed package.
221
+ Installation failure returns nonzero; an interrupted npm install may require
222
+ repair through npm. The updater does not promise transactional rollback.
223
+
224
+ Release selection uses `dist-tags.latest`, the matching `versions` manifest,
225
+ and `time[version]` in official registry metadata. Prereleases, deprecated
226
+ releases, missing publication records, future timestamps, and invalid package
227
+ or tarball identity are rejected. The displayed date is the publication date in
228
+ UTC, formatted `MM/DD/YYYY`, not the metadata modification date or a private
229
+ commit date. Semantic precedence ignores build metadata and never downgrades
230
+ an equal/newer candidate; a stable version supersedes its same-core prerelease.
231
+ See [npm distribution tags](https://docs.npmjs.com/cli/v11/commands/npm-dist-tag/),
232
+ [npm publication time fields](https://docs.npmjs.com/cli/v11/commands/npm-view/),
233
+ [Semantic Versioning](https://semver.org/), and
234
+ [npm installation layouts](https://docs.npmjs.com/cli/v11/configuring-npm/folders/).
235
+
236
+ ## Find conversations started on this computer
237
+
238
+ Run `teamclaws chat list <target> --json` to recover locally recorded session and
239
+ latest request identifiers for a verified pairing. Login and direct pairing
240
+ verification are required; listing does not fetch capabilities, create a chat,
241
+ send a message, poll a request, or call a remote history endpoint.
242
+
243
+ The `chat_sessions_list` event has `status: success`, `source: local`, the
244
+ selected `workspace`, and `sessions` containing `session_id`, `request_id`,
245
+ `owner_conversation_url`, `revision`, `response_cursor`, and `updated_at`.
246
+ Use the existing send/watch commands with those identifiers. For watch, pass
247
+ revision as `--known-revision` and cursor as `--after-response-message-id`.
248
+ Every subsequent remote operation retains its normal authorization checks.
249
+
250
+ References are saved separately from credentials in private files under
251
+ `~/.teamclaws/chat-references/`, partitioned by workspace and paired device.
252
+ No message text, tokens, headers, or transcripts are saved. Empty results mean
253
+ no locally recorded conversations; other computers' chats and old unsaved
254
+ references are not discoverable. Device identity is not included in list output.
255
+
256
+ If recording fails after a remote response, `chat_reference_recovery` preserves
257
+ the validated identifiers and cursors, followed by `chat_reference_storage_failed`
258
+ (exit 9). Retain those identifiers and resume with watch; never automatically
259
+ repeat the remote mutation. Existing login and pairing credentials are unchanged.
package/dist/app.js CHANGED
@@ -6,7 +6,9 @@ import { TeamClawsError, safeError } from "./errors.js";
6
6
  import { ExternalChatClient, } from "./external-chat.js";
7
7
  import { Output } from "./output.js";
8
8
  import { PairingClient } from "./pairing.js";
9
+ import { runShell, shellFetch } from "./shell.js";
9
10
  import { CredentialStore } from "./storage.js";
11
+ import { installRelease } from "./update.js";
10
12
  import { parsePairingTarget } from "./validation.js";
11
13
  export async function run(arguments_, dependencies = {}, signal) {
12
14
  let parsed;
@@ -31,6 +33,40 @@ export async function run(arguments_, dependencies = {}, signal) {
31
33
  stdout.write(`${PACKAGE_VERSION}\n`);
32
34
  return 0;
33
35
  }
36
+ if (parsed.action === "update") {
37
+ const checked = dependencies.releaseCheck;
38
+ let status;
39
+ let message;
40
+ let code = 0;
41
+ if (!checked || checked.status === "unavailable") {
42
+ status = "unavailable";
43
+ message =
44
+ "Could not verify an official stable release. No update was installed. Try teamclaws update again later.";
45
+ code = 1;
46
+ }
47
+ else if (checked.status === "current") {
48
+ status = "current";
49
+ message = "No newer official stable TeamClaws release is available.";
50
+ }
51
+ else {
52
+ try {
53
+ await (dependencies.installer ?? installRelease)(checked.release, signal);
54
+ status = "updated";
55
+ message = `Updated TeamClaws to ${checked.release.version}.`;
56
+ }
57
+ catch {
58
+ status = "failed";
59
+ message =
60
+ "Update failed. Requires the active npm global installation with write permission and npm beside Node. Local, linked, npx, and managed installs must use their original package manager. No privilege elevation was attempted. Check the installed version before retrying.";
61
+ code = 1;
62
+ }
63
+ }
64
+ if (parsed.json)
65
+ stdout.write(`${JSON.stringify({ schema_version: 1, event: "update_result", status, message })}\n`);
66
+ else
67
+ (code ? stderr : stdout).write(`${message}\n`);
68
+ return code;
69
+ }
34
70
  const store = dependencies.store ?? new CredentialStore();
35
71
  const auth = new AuthService({
36
72
  store,
@@ -67,16 +103,27 @@ export async function run(arguments_, dependencies = {}, signal) {
67
103
  case "logout":
68
104
  await auth.logout(signal);
69
105
  return 0;
106
+ }
107
+ const token = await auth.requireAccessToken(parsed.action === "pair"
108
+ ? "Authenticate in the TeamClaws CLI with `teamclaws login` before making a pairing request."
109
+ : "Authenticate in the TeamClaws CLI with `teamclaws login` before running this command.", signal);
110
+ switch (parsed.action) {
111
+ case "shell": {
112
+ const result = await runShell(store, dependencies.fetcher ?? shellFetch, parsePairingTarget(parsed.target), parsed.command, signal);
113
+ if (parsed.json)
114
+ stdout.write(`${JSON.stringify(result)}\n`);
115
+ else {
116
+ stdout.write(result.stdout);
117
+ stderr.write(result.stderr);
118
+ }
119
+ return result.exit_code;
120
+ }
70
121
  case "servers_list": {
71
- const token = await auth.accessToken(signal);
72
- if (!token)
73
- throw loginRequired();
74
- const servers = await broker.servers(token, signal);
75
- output.servers(servers);
122
+ const inventory = await broker.servers(token, signal);
123
+ output.servers(inventory.servers, inventory.workspace_count);
76
124
  return 0;
77
125
  }
78
126
  case "pair": {
79
- const token = await auth.accessToken(signal);
80
127
  const target = parsed.target
81
128
  ? parsePairingTarget(parsed.target)
82
129
  : await discoverTarget(broker, token, parsed.json, output, dependencies.io, signal);
@@ -88,6 +135,8 @@ export async function run(arguments_, dependencies = {}, signal) {
88
135
  const status = await pairing.status(parsePairingTarget(parsed.target), signal);
89
136
  return status === "paired" ? 0 : status === "unreachable" ? 5 : 1;
90
137
  }
138
+ case "chat_list":
139
+ return await externalChat.list(parsePairingTarget(parsed.target), signal);
91
140
  case "chat_capabilities":
92
141
  return await externalChat.capabilities(parsePairingTarget(parsed.target), signal);
93
142
  case "chat_start":
@@ -105,12 +154,34 @@ export async function run(arguments_, dependencies = {}, signal) {
105
154
  }
106
155
  }
107
156
  export function parseArguments(arguments_) {
157
+ if (arguments_[0] === "shell") {
158
+ const target = requiredPositional(arguments_, 1);
159
+ let command;
160
+ let json = false;
161
+ for (let index = 2; index < arguments_.length; index += 1) {
162
+ const option = arguments_[index];
163
+ if (option === "--command" && command === undefined) {
164
+ command = arguments_[++index];
165
+ if (command === undefined || command.length === 0)
166
+ throw usageError();
167
+ }
168
+ else if (option === "--json" && !json)
169
+ json = true;
170
+ else
171
+ throw usageError();
172
+ }
173
+ if (command === undefined)
174
+ throw usageError();
175
+ return { action: "shell", target, command, json };
176
+ }
108
177
  const json = arguments_.includes("--json");
109
178
  const args = arguments_.filter((argument) => argument !== "--json");
110
179
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h")
111
180
  return { action: "help", json };
112
181
  if (args[0] === "--version" || args[0] === "-v")
113
182
  return { action: "version", json };
183
+ if (args[0] === "update" && args.length === 1)
184
+ return { action: "update", json };
114
185
  if (args[0] === "login") {
115
186
  if (args.length === 1)
116
187
  return { action: "login", json };
@@ -145,6 +216,9 @@ export function parseArguments(arguments_) {
145
216
  const target = positional[0];
146
217
  return { action: "pair", json, ...(target ? { target } : {}), noOpen };
147
218
  }
219
+ if (args[0] === "chat" && args[1] === "list" && args.length === 3) {
220
+ return { action: "chat_list", json, target: requiredPositional(args, 2) };
221
+ }
148
222
  if (args[0] === "chat" && args[1] === "capabilities" && args.length === 3) {
149
223
  return { action: "chat_capabilities", json, target: args[2] };
150
224
  }
@@ -275,18 +349,16 @@ function nonNegativeInteger(value) {
275
349
  return parsed;
276
350
  }
277
351
  async function discoverTarget(broker, token, json, output, io, signal) {
278
- if (!token) {
279
- throw new TeamClawsError("login_or_server_required", "Sign in or provide a stable server identifier or canonical workspace address.", { exitCode: 4 });
280
- }
281
- const servers = (await broker.servers(token, signal)).filter((server) => server.pairing_ready);
352
+ const inventory = await broker.servers(token, signal);
353
+ const servers = inventory.servers.filter((server) => server.pairing_ready);
282
354
  if (servers.length === 0) {
283
- throw new TeamClawsError("no_pairing_ready_workspaces", "No pairing-ready TeamClaws workspaces are available.", { exitCode: 4 });
355
+ throw new TeamClawsError("no_pairing_ready_workspaces", inventory.workspace_count === 0
356
+ ? "No TeamClaws workspaces are available to this account. Purchase a workspace at https://teamclaws.com/app/."
357
+ : "No pairing-ready TeamClaws workspaces are available. Check existing workspaces at https://teamclaws.com/app/; this does not mean you need to purchase another.", { exitCode: 4 });
284
358
  }
285
- if (servers.length === 1)
286
- return { server_id: servers[0].server_id };
287
359
  const interactive = io?.interactive ?? (process.stdin.isTTY && process.stdout.isTTY);
288
360
  if (json || !interactive) {
289
- throw new TeamClawsError("server_selection_required", "More than one workspace is available. Provide the stable server identifier.", { exitCode: 4 });
361
+ throw new TeamClawsError("server_selection_required", "Choose a workspace by alias from `teamclaws servers list`, then provide its stable server identifier after the user agrees to pair.", { exitCode: 4 });
290
362
  }
291
363
  output.servers(servers);
292
364
  const selected = await promptForServer(servers, io?.input ?? process.stdin, io?.stdout ?? process.stdout);
@@ -311,17 +383,11 @@ async function promptForServer(servers, input, output) {
311
383
  terminal.close();
312
384
  }
313
385
  }
314
- function loginRequired() {
315
- return new TeamClawsError("login_required", "Sign in to TeamClaws first.", {
316
- exitCode: 3,
317
- });
318
- }
319
386
  function usageError() {
320
387
  return new TeamClawsError("invalid_command", "Run `teamclaws --help` for supported commands.", {
321
388
  exitCode: 2,
322
389
  });
323
390
  }
324
391
  export function helpText() {
325
- return `TeamClaws ${PACKAGE_VERSION}\n\nUsage:\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 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`;
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`;
326
393
  }
327
- //# sourceMappingURL=app.js.map
package/dist/auth.js CHANGED
@@ -76,6 +76,14 @@ export class AuthService {
76
76
  const active = await this.ensureActive(credential, signal);
77
77
  return active.access_token;
78
78
  }
79
+ async requireAccessToken(message, signal) {
80
+ const token = await this.accessToken(signal);
81
+ if (!token) {
82
+ throw new TeamClawsError("login_required", message, { exitCode: 3 });
83
+ }
84
+ await this.fetchIdentity(token, signal);
85
+ return token;
86
+ }
79
87
  async ensureActive(credential, signal) {
80
88
  const expiresAt = Date.parse(credential.expires_at);
81
89
  if (expiresAt - this.now() > 30_000)
@@ -171,7 +179,7 @@ export class AuthService {
171
179
  ...(signal ? { signal } : {}),
172
180
  });
173
181
  if (response.status === 401) {
174
- throw new TeamClawsError("invalid_authentication", "The TeamClaws login is no longer valid.", {
182
+ throw new TeamClawsError("invalid_authentication", "The TeamClaws login is no longer valid. Run `teamclaws login` to authenticate.", {
175
183
  exitCode: 3,
176
184
  });
177
185
  }
@@ -299,7 +307,10 @@ function oauthError(status, body) {
299
307
  if (error) {
300
308
  code = error === "invalid_grant" ? "invalid_authentication" : error;
301
309
  }
302
- return new TeamClawsError(code, "TeamClaws rejected the login request.", {
310
+ const message = error === "invalid_grant"
311
+ ? "The TeamClaws login is no longer valid. Run `teamclaws login` to authenticate."
312
+ : "TeamClaws rejected the login request.";
313
+ return new TeamClawsError(code, message, {
303
314
  retryable: status >= 500,
304
315
  exitCode: 3,
305
316
  });
@@ -327,4 +338,3 @@ async function waitFor(milliseconds, signal) {
327
338
  signal?.addEventListener("abort", onAbort, { once: true });
328
339
  });
329
340
  }
330
- //# sourceMappingURL=auth.js.map
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, parseServersResponse } from "./validation.js";
6
+ import { parsePendingPairing, parseWorkspaceInventory } from "./validation.js";
7
7
  export class BrokerClient {
8
8
  fetcher;
9
9
  deviceName;
@@ -15,19 +15,19 @@ 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/`, {
18
+ const response = await requestJson(this.fetcher, `${API_BASE}servers/?include_inventory=1&include_description=1`, {
19
19
  headers: requestHeaders(accessToken),
20
20
  ...(signal ? { signal } : {}),
21
21
  });
22
22
  if (response.status === 401) {
23
- throw new TeamClawsError("invalid_authentication", "The TeamClaws login is no longer valid.", {
23
+ throw new TeamClawsError("invalid_authentication", "The TeamClaws login is no longer valid. Run `teamclaws login` to authenticate.", {
24
24
  exitCode: 3,
25
25
  });
26
26
  }
27
27
  if (response.status < 200 || response.status >= 300) {
28
28
  throw errorFromResponse(response, "server_discovery_failed");
29
29
  }
30
- return parseServersResponse(response.body);
30
+ return parseWorkspaceInventory(response.body);
31
31
  }
32
32
  async startPairing(target, accessToken, signal) {
33
33
  const request = pairingStartRequest(target, this.currentDeviceName());
@@ -76,4 +76,3 @@ function requestHeaders(accessToken) {
76
76
  headers.Authorization = `Bearer ${accessToken}`;
77
77
  return headers;
78
78
  }
79
- //# sourceMappingURL=broker.js.map
package/dist/browser.js CHANGED
@@ -30,4 +30,3 @@ export function browserCommand(url, platform = process.platform) {
30
30
  return { executable: "xdg-open", arguments: [url] };
31
31
  return undefined;
32
32
  }
33
- //# sourceMappingURL=browser.js.map
package/dist/cli.js CHANGED
@@ -1,14 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import { run } from "./app.js";
2
+ import { runCli } from "./startup.js";
3
3
  const controller = new AbortController();
4
4
  const interrupt = () => controller.abort();
5
5
  process.once("SIGINT", interrupt);
6
6
  process.once("SIGTERM", interrupt);
7
7
  try {
8
- process.exitCode = await run(process.argv.slice(2), {}, controller.signal);
8
+ process.exitCode = await runCli(process.argv.slice(2), {}, controller.signal);
9
9
  }
10
10
  finally {
11
11
  process.removeListener("SIGINT", interrupt);
12
12
  process.removeListener("SIGTERM", interrupt);
13
13
  }
14
- //# sourceMappingURL=cli.js.map
package/dist/constants.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const PACKAGE_NAME = "teamclaws";
2
- export const PACKAGE_VERSION = "1.0.0";
2
+ export const PACKAGE_VERSION = "1.0.2";
3
3
  export const CLIENT_NAME = "teamclaws-cli";
4
4
  export const CONTRACT_VERSION = 1;
5
5
  export const OUTPUT_SCHEMA_VERSION = 1;
@@ -18,4 +18,3 @@ export const DEFAULT_RESPONSE_LIMIT_BYTES = 1_048_576;
18
18
  export const MAX_DEVICE_NAME_LENGTH = 120;
19
19
  export const MAX_CLIENT_LABEL_LENGTH = 120;
20
20
  export const MAX_ERROR_MESSAGE_LENGTH = 300;
21
- //# sourceMappingURL=constants.js.map
package/dist/errors.js CHANGED
@@ -17,4 +17,3 @@ export function safeError(error) {
17
17
  return error;
18
18
  return new TeamClawsError("unexpected_error", "TeamClaws could not complete the command. Try again.", { exitCode: 1 });
19
19
  }
20
- //# sourceMappingURL=errors.js.map
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { PACKAGE_VERSION } from "./constants.js";
3
3
  import { TeamClawsError } from "./errors.js";
4
4
  import { errorFromResponse, requestJson, } from "./http.js";
5
- import { isObject, normalizeUuid, parseVerifiedWorkspace, requireHttpsUrl, requireInteger, requireString, } from "./validation.js";
5
+ import { isObject, normalizeUuid, parseVerifiedWorkspace, parseVerifiedDevice, requireHttpsUrl, requireInteger, requireString, } from "./validation.js";
6
6
  const REQUIRED_SCOPES = new Set([
7
7
  "external_chat:capabilities:read",
8
8
  "external_chat:sessions:create",
@@ -167,6 +167,37 @@ export class ExternalChatClient {
167
167
  const requestId = validUuid(options.requestId);
168
168
  return this.poll(context, { requestId, sessionId }, options.knownRevision, options.responseCursor, signal);
169
169
  }
170
+ async list(target, signal) {
171
+ const credential = await this.store.findWorkspace(target);
172
+ if (!credential)
173
+ throw new TeamClawsError("re_pair_required", "No saved pairing was found for this workspace. Pair it first.", { exitCode: 3 });
174
+ const response = await this.request(`${credential.origin}/teamclaws-admin/api/device-pairings/me/`, bearer(credential), {}, [200], signal);
175
+ parseVerifiedDevice(response.body, credential.device_id);
176
+ requirePairingScopes(response.body);
177
+ this.output.chatSessionsList(credential, await this.store.listConversations(credential));
178
+ return 0;
179
+ }
180
+ async record(context, state, responseCursor) {
181
+ try {
182
+ await this.store.upsertConversation(context.credential, {
183
+ server_id: context.credential.server_id,
184
+ origin: context.credential.origin,
185
+ device_id: context.credential.device_id,
186
+ session_id: state.sessionId,
187
+ request_id: state.requestId,
188
+ owner_conversation_url: state.ownerUrl,
189
+ revision: state.revision,
190
+ response_cursor: responseCursor,
191
+ updated_at: new Date().toISOString(),
192
+ });
193
+ }
194
+ catch (error) {
195
+ // The remote operation already happened. Preserve its safe references even
196
+ // if local storage is unavailable; never replay the mutation.
197
+ this.output.chatReferenceRecovery(state.sessionId, state.requestId, state.ownerUrl, state.revision, responseCursor);
198
+ throw error;
199
+ }
200
+ }
170
201
  async context(target, signal, starting = false) {
171
202
  const credential = await this.store.findWorkspace(target);
172
203
  if (!credential) {
@@ -238,6 +269,7 @@ export class ExternalChatClient {
238
269
  }
239
270
  knownRevision = state.revision;
240
271
  responseCursor = Math.max(responseCursor, state.responseCursor, ...seenMessageIds);
272
+ await this.record(context, state, responseCursor);
241
273
  if (TERMINAL_STATUSES.has(state.status))
242
274
  break;
243
275
  if (!PENDING_STATUSES.has(state.status))
@@ -341,8 +373,7 @@ function parseCapabilities(value, inventoryOnly = false) {
341
373
  throw invalidChatResponse();
342
374
  }
343
375
  const id = requireString(provider.id, "provider identifier", 120);
344
- if (provider.models.length > 128)
345
- throw invalidChatResponse();
376
+ // Catalogs grow independently of the CLI; HTTP response bytes are bounded.
346
377
  const models = provider.models.map((model) => {
347
378
  if (!isObject(model))
348
379
  throw invalidChatResponse();
@@ -567,4 +598,3 @@ async function waitFor(milliseconds, signal) {
567
598
  signal?.addEventListener("abort", onAbort, { once: true });
568
599
  });
569
600
  }
570
- //# sourceMappingURL=external-chat.js.map
package/dist/http.js CHANGED
@@ -2,16 +2,22 @@ import { DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_RESPONSE_LIMIT_BYTES, MAX_ERROR_MES
2
2
  import { TeamClawsError } from "./errors.js";
3
3
  import { isObject } from "./validation.js";
4
4
  export async function requestJson(fetcher, url, options = {}) {
5
- const timeout = AbortSignal.timeout(options.timeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS);
6
- const signal = options.signal
5
+ const timeout = options.timeoutMs === null
6
+ ? undefined
7
+ : AbortSignal.timeout(options.timeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS);
8
+ const signal = timeout && options.signal
7
9
  ? AbortSignal.any([timeout, options.signal])
8
- : timeout;
10
+ : (timeout ?? options.signal);
9
11
  const headers = new Headers({
10
12
  Accept: "application/json",
11
13
  ...options.headers,
12
14
  });
13
15
  let body;
14
- if (options.form) {
16
+ if (options.textBody !== undefined) {
17
+ headers.set("Content-Type", "text/plain; charset=utf-8");
18
+ body = options.textBody;
19
+ }
20
+ else if (options.form) {
15
21
  headers.set("Content-Type", "application/x-www-form-urlencoded");
16
22
  body = options.form.toString();
17
23
  }
@@ -26,11 +32,11 @@ export async function requestJson(fetcher, url, options = {}) {
26
32
  headers,
27
33
  ...(body === undefined ? {} : { body }),
28
34
  redirect: "error",
29
- signal,
35
+ ...(signal ? { signal } : {}),
30
36
  });
31
37
  }
32
38
  catch (error) {
33
- if (signal.aborted) {
39
+ if (signal?.aborted) {
34
40
  throw new TeamClawsError("network_timeout", "TeamClaws did not respond in time.", {
35
41
  retryable: true,
36
42
  exitCode: 5,
@@ -43,7 +49,9 @@ export async function requestJson(fetcher, url, options = {}) {
43
49
  });
44
50
  }
45
51
  const contentLength = Number(response.headers.get("content-length") ?? 0);
46
- const limit = options.responseLimitBytes ?? DEFAULT_RESPONSE_LIMIT_BYTES;
52
+ const limit = options.responseLimitBytes === null
53
+ ? Infinity
54
+ : (options.responseLimitBytes ?? DEFAULT_RESPONSE_LIMIT_BYTES);
47
55
  if (contentLength > limit) {
48
56
  throw new TeamClawsError("response_too_large", "TeamClaws returned an oversized response.", {
49
57
  exitCode: 9,
@@ -104,4 +112,3 @@ function safeProtocolString(value, fallback, maxLength) {
104
112
  return fallback;
105
113
  return value;
106
114
  }
107
- //# sourceMappingURL=http.js.map