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 +163 -11
- package/dist/app.js +87 -21
- package/dist/auth.js +13 -3
- package/dist/broker.js +4 -5
- package/dist/browser.js +0 -1
- package/dist/cli.js +2 -3
- package/dist/constants.js +1 -2
- package/dist/errors.js +0 -1
- package/dist/external-chat.js +34 -4
- package/dist/http.js +15 -8
- package/dist/output.js +58 -6
- package/dist/pairing.js +0 -1
- package/dist/shell.js +69 -0
- package/dist/startup.js +27 -0
- package/dist/storage.js +131 -6
- package/dist/types.js +0 -1
- package/dist/update.js +230 -0
- package/dist/validation.js +19 -3
- package/package.json +8 -8
- package/skills/teamclaws-chat/SKILL.md +45 -2
- package/skills/teamclaws-login/SKILL.md +10 -2
- package/skills/teamclaws-onboard/SKILL.md +73 -0
- package/skills/teamclaws-pair/SKILL.md +19 -10
- package/skills/teamclaws-project/SKILL.md +106 -0
- package/dist/app.js.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/broker.js.map +0 -1
- package/dist/browser.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/constants.js.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/external-chat.js.map +0 -1
- package/dist/http.js.map +0 -1
- package/dist/output.js.map +0 -1
- package/dist/pairing.js.map +0 -1
- package/dist/storage.js.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/validation.js.map +0 -1
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`
|
|
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
|
|
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
|
|
72
|
-
|
|
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
|
-
|
|
279
|
-
|
|
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",
|
|
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", "
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
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
|
|
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.
|
|
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
package/dist/external-chat.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
6
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|