teamclaws 1.0.0 → 1.0.1
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 +161 -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 +56 -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 +10 -2
- 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/dist/output.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { OUTPUT_SCHEMA_VERSION } from "./constants.js";
|
|
2
|
+
const LOGIN_ACCOUNT_GUIDANCE = "Use your TeamClaws account. If you already have a workspace, use the account that has access to it. This may use a different email from your AI assistant account.";
|
|
2
3
|
export class Output {
|
|
3
4
|
json;
|
|
4
5
|
stdout;
|
|
@@ -13,9 +14,11 @@ export class Output {
|
|
|
13
14
|
schema_version: OUTPUT_SCHEMA_VERSION,
|
|
14
15
|
event: "login_browser_required",
|
|
15
16
|
url,
|
|
17
|
+
label: "Sign in to TeamClaws",
|
|
18
|
+
account_guidance: LOGIN_ACCOUNT_GUIDANCE,
|
|
16
19
|
});
|
|
17
20
|
if (!this.json)
|
|
18
|
-
this.stdout.write(
|
|
21
|
+
this.stdout.write(`Sign in to TeamClaws: ${url}\n${LOGIN_ACCOUNT_GUIDANCE}\n`);
|
|
19
22
|
}
|
|
20
23
|
pairingBrowser(url, userCode, agentAction) {
|
|
21
24
|
const event = {
|
|
@@ -55,6 +58,8 @@ export class Output {
|
|
|
55
58
|
event: "auth_status",
|
|
56
59
|
status,
|
|
57
60
|
};
|
|
61
|
+
if (status === "signed_out")
|
|
62
|
+
event.login_command = "teamclaws login --json";
|
|
58
63
|
if (user)
|
|
59
64
|
event.user = user;
|
|
60
65
|
if (expiresAt)
|
|
@@ -63,7 +68,7 @@ export class Output {
|
|
|
63
68
|
if (!this.json) {
|
|
64
69
|
this.stdout.write(status === "signed_in" && user
|
|
65
70
|
? `Signed in as ${user.display_name} (${user.email}).\n`
|
|
66
|
-
: "Not signed in to TeamClaws.\n");
|
|
71
|
+
: "Not signed in to TeamClaws. Run `teamclaws login` to sign in.\n");
|
|
67
72
|
}
|
|
68
73
|
}
|
|
69
74
|
logoutComplete(remoteConfirmed) {
|
|
@@ -78,20 +83,30 @@ export class Output {
|
|
|
78
83
|
: "Signed out on this computer. TeamClaws could not confirm remote revocation.\n");
|
|
79
84
|
}
|
|
80
85
|
}
|
|
81
|
-
servers(servers) {
|
|
86
|
+
servers(servers, workspaceCount) {
|
|
82
87
|
this.event({
|
|
83
88
|
schema_version: OUTPUT_SCHEMA_VERSION,
|
|
84
89
|
event: "servers_list",
|
|
85
90
|
status: "success",
|
|
86
91
|
servers,
|
|
92
|
+
workspace_count: workspaceCount ?? null,
|
|
93
|
+
discovery_state: workspaceCount === 0
|
|
94
|
+
? "no_workspaces"
|
|
95
|
+
: servers.some((server) => server.pairing_ready)
|
|
96
|
+
? "ready"
|
|
97
|
+
: workspaceCount === undefined && servers.length === 0
|
|
98
|
+
? "unknown"
|
|
99
|
+
: "not_ready",
|
|
87
100
|
});
|
|
88
101
|
if (!this.json) {
|
|
89
102
|
if (servers.length === 0) {
|
|
90
|
-
this.stdout.write(
|
|
103
|
+
this.stdout.write(workspaceCount === 0
|
|
104
|
+
? "No TeamClaws workspaces are available to this account. Purchase a workspace at https://teamclaws.com/app/.\n"
|
|
105
|
+
: "No pairing-ready TeamClaws workspaces are listed. Check existing workspaces at https://teamclaws.com/app/ before considering a purchase.\n");
|
|
91
106
|
return;
|
|
92
107
|
}
|
|
93
108
|
servers.forEach((server, index) => {
|
|
94
|
-
this.stdout.write(`${index + 1}. ${server.alias} — ${server.server_id} — ${server.workspace_url}\n`);
|
|
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`);
|
|
95
110
|
});
|
|
96
111
|
}
|
|
97
112
|
}
|
|
@@ -191,6 +206,42 @@ export class Output {
|
|
|
191
206
|
? `Snapshot generated: ${generatedAt}.\n`
|
|
192
207
|
: "Generation time is not supplied by the capabilities endpoint.\n");
|
|
193
208
|
}
|
|
209
|
+
chatSessionsList(workspace, records) {
|
|
210
|
+
const sessions = records.map((record) => ({
|
|
211
|
+
session_id: record.session_id,
|
|
212
|
+
request_id: record.request_id,
|
|
213
|
+
owner_conversation_url: record.owner_conversation_url,
|
|
214
|
+
revision: record.revision,
|
|
215
|
+
response_cursor: record.response_cursor,
|
|
216
|
+
updated_at: record.updated_at,
|
|
217
|
+
}));
|
|
218
|
+
this.event({
|
|
219
|
+
schema_version: OUTPUT_SCHEMA_VERSION,
|
|
220
|
+
event: "chat_sessions_list",
|
|
221
|
+
status: "success",
|
|
222
|
+
source: "local",
|
|
223
|
+
workspace: { server_id: workspace.server_id, origin: workspace.origin },
|
|
224
|
+
sessions,
|
|
225
|
+
});
|
|
226
|
+
if (!this.json) {
|
|
227
|
+
this.stdout.write(sessions.length
|
|
228
|
+
? "Locally recorded conversations:\n"
|
|
229
|
+
: "No locally recorded conversations for this workspace and pairing. This is not the workspace conversation inventory.\n");
|
|
230
|
+
for (const item of sessions)
|
|
231
|
+
this.stdout.write(`${item.session_id} — request ${item.request_id} — revision ${item.revision}, response cursor ${item.response_cursor} — ${item.owner_conversation_url}\n`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
chatReferenceRecovery(sessionId, requestId, ownerUrl, revision, responseCursor) {
|
|
235
|
+
this.externalChatEvent("chat_reference_recovery", {
|
|
236
|
+
session_id: sessionId,
|
|
237
|
+
request_id: requestId,
|
|
238
|
+
owner_conversation_url: ownerUrl,
|
|
239
|
+
revision,
|
|
240
|
+
response_cursor: responseCursor,
|
|
241
|
+
});
|
|
242
|
+
if (!this.json)
|
|
243
|
+
this.stdout.write(`Retain session ${sessionId}, request ${requestId}, revision ${revision}, response cursor ${responseCursor}: ${ownerUrl}\n`);
|
|
244
|
+
}
|
|
194
245
|
chatCapabilities(selection) {
|
|
195
246
|
this.externalChatEvent("capabilities_resolved", {
|
|
196
247
|
provider_key: selection.provider_key,
|
|
@@ -295,4 +346,3 @@ export class Output {
|
|
|
295
346
|
});
|
|
296
347
|
}
|
|
297
348
|
}
|
|
298
|
-
//# sourceMappingURL=output.js.map
|
package/dist/pairing.js
CHANGED
package/dist/shell.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { request as httpRequest } from "node:http";
|
|
2
|
+
import { request as httpsRequest } from "node:https";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
import { TeamClawsError } from "./errors.js";
|
|
5
|
+
import { errorFromResponse, requestJson } from "./http.js";
|
|
6
|
+
import { isObject } from "./validation.js";
|
|
7
|
+
export async function runShell(store, fetcher, target, command, signal) {
|
|
8
|
+
const credential = await store.findWorkspace(target);
|
|
9
|
+
if (!credential) {
|
|
10
|
+
throw new TeamClawsError("re_pair_required", "No saved pairing was found for this workspace. Pair it first.", { exitCode: 3 });
|
|
11
|
+
}
|
|
12
|
+
const response = await requestJson(fetcher, `${credential.origin}/teamclaws-admin/api/shell/`, {
|
|
13
|
+
method: "POST",
|
|
14
|
+
headers: {
|
|
15
|
+
Authorization: `Bearer ${credential.device_id}:${credential.device_token}`,
|
|
16
|
+
},
|
|
17
|
+
textBody: command,
|
|
18
|
+
timeoutMs: null,
|
|
19
|
+
responseLimitBytes: null,
|
|
20
|
+
...(signal ? { signal } : {}),
|
|
21
|
+
});
|
|
22
|
+
if (response.status !== 200)
|
|
23
|
+
throw errorFromResponse(response, "shell_failed");
|
|
24
|
+
const result = response.body;
|
|
25
|
+
if (!isObject(result) ||
|
|
26
|
+
typeof result.stdout !== "string" ||
|
|
27
|
+
typeof result.stderr !== "string" ||
|
|
28
|
+
!Number.isSafeInteger(result.exit_code)) {
|
|
29
|
+
throw new TeamClawsError("invalid_response", "Invalid shell result.", {
|
|
30
|
+
exitCode: 9,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
stdout: result.stdout,
|
|
35
|
+
stderr: result.stderr,
|
|
36
|
+
exit_code: result.exit_code,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// Native fetch imposes transport deadlines. This single-use connection has no
|
|
40
|
+
// request deadline, redirects, retries, or response body limit.
|
|
41
|
+
export const shellFetch = async (input, init) => {
|
|
42
|
+
const url = new URL(String(input));
|
|
43
|
+
return await new Promise((resolve, reject) => {
|
|
44
|
+
const request = (url.protocol === "https:" ? httpsRequest : httpRequest)(url, {
|
|
45
|
+
method: init?.method,
|
|
46
|
+
headers: Object.fromEntries(new Headers(init?.headers)),
|
|
47
|
+
agent: false,
|
|
48
|
+
timeout: 0,
|
|
49
|
+
...(init?.signal ? { signal: init.signal } : {}),
|
|
50
|
+
}, (response) => {
|
|
51
|
+
const status = response.statusCode ?? 500;
|
|
52
|
+
const headers = new Headers();
|
|
53
|
+
for (let index = 0; index < response.rawHeaders.length; index += 2) {
|
|
54
|
+
headers.append(response.rawHeaders[index], response.rawHeaders[index + 1]);
|
|
55
|
+
}
|
|
56
|
+
if (status >= 300 && status < 400) {
|
|
57
|
+
response.resume();
|
|
58
|
+
reject(new Error("Shell redirects are not followed."));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const body = Readable.toWeb(response);
|
|
62
|
+
if (status === 204 || status === 205 || status === 304)
|
|
63
|
+
response.resume();
|
|
64
|
+
resolve(new Response(status === 204 || status === 205 || status === 304 ? null : body, { status, headers }));
|
|
65
|
+
});
|
|
66
|
+
request.on("error", reject);
|
|
67
|
+
request.end(init?.body);
|
|
68
|
+
});
|
|
69
|
+
};
|
package/dist/startup.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { parseArguments, run } from "./app.js";
|
|
2
|
+
import { checkRelease, releaseNotice } from "./update.js";
|
|
3
|
+
export async function runCli(args, dependencies = {}, signal, check = checkRelease) {
|
|
4
|
+
// No persistent cache: every executable invocation checks before dispatch.
|
|
5
|
+
let releaseCheck;
|
|
6
|
+
try {
|
|
7
|
+
releaseCheck = await check(signal);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
releaseCheck = { status: "unavailable" };
|
|
11
|
+
}
|
|
12
|
+
if (releaseCheck.status === "available") {
|
|
13
|
+
let json = args.includes("--json");
|
|
14
|
+
try {
|
|
15
|
+
json = parseArguments(args).json;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
/* Match usage errors. */
|
|
19
|
+
}
|
|
20
|
+
const { release } = releaseCheck;
|
|
21
|
+
const message = releaseNotice(release);
|
|
22
|
+
(dependencies.io?.stderr ?? process.stderr).write(json
|
|
23
|
+
? `${JSON.stringify({ schema_version: 1, event: "update_available", version: release.version, published_at: release.publishedAt, available_date: release.date, update_command: "teamclaws update", message })}\n`
|
|
24
|
+
: `${message}\n`);
|
|
25
|
+
}
|
|
26
|
+
return run(args, { ...dependencies, releaseCheck }, signal);
|
|
27
|
+
}
|
package/dist/storage.js
CHANGED
|
@@ -5,7 +5,7 @@ import { homedir, tmpdir } from "node:os";
|
|
|
5
5
|
import { basename, dirname, join } from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { TeamClawsError } from "./errors.js";
|
|
8
|
-
import { normalizeUuid, parseLoginCredential, parseStatusWorkspaceCredential, parseWorkspaceCredential, } from "./validation.js";
|
|
8
|
+
import { isObject, requireHttpsUrl, requireInteger, normalizeUuid, parseLoginCredential, parseStatusWorkspaceCredential, parseWorkspaceCredential, } from "./validation.js";
|
|
9
9
|
const execFile = promisify(execFileCallback);
|
|
10
10
|
const SYSTEM_SID = "S-1-5-18";
|
|
11
11
|
const MAX_WORKSPACE_CREDENTIAL_FILES = 256;
|
|
@@ -91,6 +91,76 @@ export class CredentialStore {
|
|
|
91
91
|
throw storageError();
|
|
92
92
|
});
|
|
93
93
|
}
|
|
94
|
+
conversationDirectory(credential) {
|
|
95
|
+
return join(this.root, "chat-references", normalizeUuid(credential.server_id), normalizeUuid(credential.device_id));
|
|
96
|
+
}
|
|
97
|
+
async checkConversationDirectory(credential) {
|
|
98
|
+
const directory = this.conversationDirectory(credential);
|
|
99
|
+
for (const path of [
|
|
100
|
+
this.root,
|
|
101
|
+
join(this.root, "chat-references"),
|
|
102
|
+
dirname(directory),
|
|
103
|
+
directory,
|
|
104
|
+
]) {
|
|
105
|
+
await this.assertPrivateDirectory(path);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async listConversations(credential) {
|
|
109
|
+
try {
|
|
110
|
+
const directory = this.conversationDirectory(credential);
|
|
111
|
+
try {
|
|
112
|
+
await this.checkConversationDirectory(credential);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (error.code === "ENOENT")
|
|
116
|
+
return [];
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
const records = [];
|
|
120
|
+
for (const name of (await readdir(directory)).sort()) {
|
|
121
|
+
if (!UUID_CREDENTIAL_NAME.test(name))
|
|
122
|
+
continue;
|
|
123
|
+
const path = join(directory, name);
|
|
124
|
+
// A single reference is small; reject unexpectedly large local files.
|
|
125
|
+
if ((await lstat(path)).size > 16_384)
|
|
126
|
+
throw conversationStorageError();
|
|
127
|
+
const value = await this.readJsonIfPresent(path);
|
|
128
|
+
if (value === undefined)
|
|
129
|
+
continue;
|
|
130
|
+
const record = parseConversationReference(value, credential);
|
|
131
|
+
if (name !== `${record.session_id}.json`)
|
|
132
|
+
throw conversationStorageError();
|
|
133
|
+
records.push(record);
|
|
134
|
+
}
|
|
135
|
+
return records.sort((a, b) => b.updated_at.localeCompare(a.updated_at));
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
throw conversationStorageError();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async upsertConversation(credential, reference) {
|
|
142
|
+
try {
|
|
143
|
+
const record = parseConversationReference(reference, credential);
|
|
144
|
+
const directory = this.conversationDirectory(credential);
|
|
145
|
+
await this.ensurePrivateDirectory(directory);
|
|
146
|
+
const path = join(directory, `${record.session_id}.json`);
|
|
147
|
+
const existing = await this.readJsonIfPresent(path);
|
|
148
|
+
if (existing !== undefined) {
|
|
149
|
+
const prior = parseConversationReference(existing, credential);
|
|
150
|
+
if (prior.session_id !== record.session_id)
|
|
151
|
+
throw conversationStorageError();
|
|
152
|
+
// Resuming an earlier cursor must not regress this request's saved progress.
|
|
153
|
+
if (prior.request_id === record.request_id) {
|
|
154
|
+
record.revision = Math.max(prior.revision, record.revision);
|
|
155
|
+
record.response_cursor = Math.max(prior.response_cursor, record.response_cursor);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
await this.writePrivateJson(path, record);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
throw conversationStorageError();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
94
164
|
async readJsonIfPresent(path) {
|
|
95
165
|
try {
|
|
96
166
|
await this.assertRegularPrivateFile(path);
|
|
@@ -254,6 +324,20 @@ async function verifyWindowsPath(path, knownUserSid) {
|
|
|
254
324
|
throw storageError();
|
|
255
325
|
}
|
|
256
326
|
}
|
|
327
|
+
let localAdministratorSid;
|
|
328
|
+
function resolveLocalAdministratorSid() {
|
|
329
|
+
return (localAdministratorSid ??= execFile("powershell.exe", [
|
|
330
|
+
"-NoProfile",
|
|
331
|
+
"-NonInteractive",
|
|
332
|
+
"-Command",
|
|
333
|
+
"[Security.AccessControl.RawSecurityDescriptor]::new('D:(A;;FA;;;LA)').DiscretionaryAcl[0].SecurityIdentifier.Value",
|
|
334
|
+
], { encoding: "utf8" }).then(({ stdout }) => {
|
|
335
|
+
const sid = stdout.trim();
|
|
336
|
+
if (!/^S-1-5-21-\d+-\d+-\d+-500$/u.test(sid))
|
|
337
|
+
throw storageError();
|
|
338
|
+
return sid;
|
|
339
|
+
}));
|
|
340
|
+
}
|
|
257
341
|
export function isPrivateWindowsAcl(acl, userSid) {
|
|
258
342
|
const allowed = new Set([userSid, SYSTEM_SID]);
|
|
259
343
|
return (acl.protected &&
|
|
@@ -266,14 +350,20 @@ async function readWindowsAcl(path) {
|
|
|
266
350
|
await execFile("icacls.exe", [path, "/save", aclPath, "/c", "/q"]);
|
|
267
351
|
const buffer = await readFile(aclPath);
|
|
268
352
|
const text = decodeWindowsText(buffer);
|
|
269
|
-
|
|
353
|
+
const acl = parseWindowsAclText(text);
|
|
354
|
+
if (acl.trustees.includes("LA")) {
|
|
355
|
+
const administratorSid = await resolveLocalAdministratorSid();
|
|
356
|
+
acl.trustees = acl.trustees.map((sid) => sid === "LA" ? administratorSid : sid);
|
|
357
|
+
}
|
|
358
|
+
return acl;
|
|
270
359
|
}
|
|
271
360
|
finally {
|
|
272
361
|
await rm(aclPath, { force: true }).catch(() => undefined);
|
|
273
362
|
}
|
|
274
363
|
}
|
|
275
364
|
export function parseWindowsAclText(text) {
|
|
276
|
-
const descriptor = text.split(/\r?\n/u).find((line) =>
|
|
365
|
+
const descriptor = text.split(/\r?\n/u).find((line) => /D:(?:P|AI|AR)*(?:\(|$)/u.test(line)) ??
|
|
366
|
+
"";
|
|
277
367
|
const daclStart = descriptor.indexOf("D:");
|
|
278
368
|
if (daclStart < 0)
|
|
279
369
|
return { protected: false, trustees: [] };
|
|
@@ -282,7 +372,11 @@ export function parseWindowsAclText(text) {
|
|
|
282
372
|
const dacl = descriptor.slice(daclBodyStart, saclStart < 0 ? undefined : saclStart);
|
|
283
373
|
const firstAce = dacl.indexOf("(");
|
|
284
374
|
const controlFlags = dacl.slice(0, firstAce < 0 ? undefined : firstAce);
|
|
285
|
-
const trustees = [...dacl.matchAll(/;;;([^;)]+)\)/gu)].map((match) => match[1] === "SY"
|
|
375
|
+
const trustees = [...dacl.matchAll(/;;;([^;)]+)\)/gu)].map((match) => match[1] === "SY"
|
|
376
|
+
? SYSTEM_SID
|
|
377
|
+
: match[1] === "BA"
|
|
378
|
+
? "S-1-5-32-544"
|
|
379
|
+
: (match[1] ?? ""));
|
|
286
380
|
return {
|
|
287
381
|
protected: controlFlags.includes("P"),
|
|
288
382
|
trustees: [...new Set(trustees)],
|
|
@@ -290,7 +384,7 @@ export function parseWindowsAclText(text) {
|
|
|
290
384
|
}
|
|
291
385
|
async function currentWindowsSid() {
|
|
292
386
|
try {
|
|
293
|
-
const { stdout } = await execFile("whoami.exe", ["/user", "/fo", "csv", "/nh"], {
|
|
387
|
+
const { stdout } = await execFile(join(process.env.SystemRoot ?? "C:\\Windows", "System32", "whoami.exe"), ["/user", "/fo", "csv", "/nh"], {
|
|
294
388
|
encoding: "utf8",
|
|
295
389
|
});
|
|
296
390
|
const match = stdout.match(/"(S-\d(?:-\d+)+)"/u);
|
|
@@ -321,4 +415,35 @@ export function decodeWindowsText(buffer) {
|
|
|
321
415
|
function storageError() {
|
|
322
416
|
return new TeamClawsError("credential_storage_failed", "TeamClaws could not protect the local credential file.", { exitCode: 9 });
|
|
323
417
|
}
|
|
324
|
-
|
|
418
|
+
function parseConversationReference(value, credential) {
|
|
419
|
+
if (!isObject(value))
|
|
420
|
+
throw conversationStorageError();
|
|
421
|
+
const server = normalizeUuid(value.server_id);
|
|
422
|
+
const device = normalizeUuid(value.device_id);
|
|
423
|
+
const origin = requireHttpsUrl(value.origin, "workspace origin").origin;
|
|
424
|
+
const owner = requireHttpsUrl(value.owner_conversation_url, "conversation URL");
|
|
425
|
+
if (server !== normalizeUuid(credential.server_id) ||
|
|
426
|
+
device !== normalizeUuid(credential.device_id) ||
|
|
427
|
+
origin !== credential.origin ||
|
|
428
|
+
value.origin !== origin ||
|
|
429
|
+
owner.origin !== origin ||
|
|
430
|
+
!owner.pathname.startsWith("/teamclaws-admin/") ||
|
|
431
|
+
typeof value.updated_at !== "string" ||
|
|
432
|
+
!Number.isFinite(Date.parse(value.updated_at)) ||
|
|
433
|
+
new Date(value.updated_at).toISOString() !== value.updated_at)
|
|
434
|
+
throw conversationStorageError();
|
|
435
|
+
return {
|
|
436
|
+
server_id: server,
|
|
437
|
+
origin,
|
|
438
|
+
device_id: device,
|
|
439
|
+
session_id: normalizeUuid(value.session_id),
|
|
440
|
+
request_id: normalizeUuid(value.request_id),
|
|
441
|
+
owner_conversation_url: owner.toString(),
|
|
442
|
+
revision: requireInteger(value.revision, "revision", 1, 2 ** 31 - 1),
|
|
443
|
+
response_cursor: requireInteger(value.response_cursor, "response cursor", 0, Number.MAX_SAFE_INTEGER),
|
|
444
|
+
updated_at: value.updated_at,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
function conversationStorageError() {
|
|
448
|
+
return new TeamClawsError("chat_reference_storage_failed", "The local conversation references could not be read or saved safely. Retain any returned session/request identifiers; do not resend the message automatically.", { exitCode: 9 });
|
|
449
|
+
}
|
package/dist/types.js
CHANGED
package/dist/update.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { PACKAGE_NAME, PACKAGE_VERSION } from "./constants.js";
|
|
8
|
+
export const OFFICIAL_REGISTRY = "https://registry.npmjs.org/";
|
|
9
|
+
export const UPDATE_TIMEOUT_MS = 1500;
|
|
10
|
+
const MAX_METADATA_BYTES = 2 * 1024 * 1024;
|
|
11
|
+
// Strict SemVer syntax; arbitrary-size numeric components avoid rounding.
|
|
12
|
+
function semver(value) {
|
|
13
|
+
if (typeof value !== "string" || value.length > 256)
|
|
14
|
+
return null;
|
|
15
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(value);
|
|
16
|
+
return match
|
|
17
|
+
? { core: match.slice(1, 4).map(BigInt), prerelease: !!match[4] }
|
|
18
|
+
: null;
|
|
19
|
+
}
|
|
20
|
+
export function isNewerStable(version, current) {
|
|
21
|
+
const latest = semver(version);
|
|
22
|
+
const installed = semver(current);
|
|
23
|
+
if (!latest || latest.prerelease || !installed)
|
|
24
|
+
return false;
|
|
25
|
+
for (let index = 0; index < 3; index++) {
|
|
26
|
+
if (latest.core[index] !== installed.core[index])
|
|
27
|
+
return latest.core[index] > installed.core[index];
|
|
28
|
+
}
|
|
29
|
+
return installed.prerelease;
|
|
30
|
+
}
|
|
31
|
+
function object(value) {
|
|
32
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
33
|
+
throw new Error("Invalid metadata");
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
export function parseRelease(metadata, now = Date.now()) {
|
|
37
|
+
const packument = object(metadata);
|
|
38
|
+
const version = object(packument["dist-tags"]).latest;
|
|
39
|
+
const parsed = semver(version);
|
|
40
|
+
if (packument.name !== PACKAGE_NAME ||
|
|
41
|
+
typeof version !== "string" ||
|
|
42
|
+
!parsed ||
|
|
43
|
+
parsed.prerelease)
|
|
44
|
+
throw new Error("Invalid release");
|
|
45
|
+
const manifest = object(object(packument.versions)[version]);
|
|
46
|
+
const dist = object(manifest.dist);
|
|
47
|
+
const publishedAt = object(packument.time)[version];
|
|
48
|
+
if (manifest.name !== PACKAGE_NAME ||
|
|
49
|
+
manifest.version !== version ||
|
|
50
|
+
manifest.deprecated ||
|
|
51
|
+
dist.tarball !==
|
|
52
|
+
`${OFFICIAL_REGISTRY}teamclaws/-/teamclaws-${version}.tgz` ||
|
|
53
|
+
typeof dist.integrity !== "string" ||
|
|
54
|
+
!/^sha512-[A-Za-z0-9+/]{86}==$/.test(dist.integrity) ||
|
|
55
|
+
typeof publishedAt !== "string" ||
|
|
56
|
+
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(publishedAt))
|
|
57
|
+
throw new Error("Invalid published release");
|
|
58
|
+
const timestamp = Date.parse(publishedAt);
|
|
59
|
+
if (!Number.isFinite(timestamp) ||
|
|
60
|
+
new Date(timestamp).toISOString() !== publishedAt ||
|
|
61
|
+
timestamp > now)
|
|
62
|
+
throw new Error("Invalid publish time");
|
|
63
|
+
const [year, month, day] = publishedAt.slice(0, 10).split("-");
|
|
64
|
+
return { version, publishedAt, date: `${month}/${day}/${year}` };
|
|
65
|
+
}
|
|
66
|
+
export async function checkRelease(signal, fetcher = fetch, current = PACKAGE_VERSION) {
|
|
67
|
+
const controller = new AbortController();
|
|
68
|
+
const abort = () => controller.abort();
|
|
69
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
70
|
+
if (signal?.aborted)
|
|
71
|
+
abort();
|
|
72
|
+
let timer;
|
|
73
|
+
const timeout = new Promise((_, reject) => {
|
|
74
|
+
timer = setTimeout(() => {
|
|
75
|
+
abort();
|
|
76
|
+
reject(new Error("Registry deadline"));
|
|
77
|
+
}, UPDATE_TIMEOUT_MS);
|
|
78
|
+
});
|
|
79
|
+
try {
|
|
80
|
+
return await Promise.race([
|
|
81
|
+
timeout,
|
|
82
|
+
(async () => {
|
|
83
|
+
const response = await fetcher(`${OFFICIAL_REGISTRY}${PACKAGE_NAME}`, {
|
|
84
|
+
signal: controller.signal,
|
|
85
|
+
redirect: "error",
|
|
86
|
+
credentials: "omit",
|
|
87
|
+
headers: { Accept: "application/json" },
|
|
88
|
+
});
|
|
89
|
+
if (!response.ok || !response.body)
|
|
90
|
+
throw new Error("Registry unavailable");
|
|
91
|
+
const reader = response.body.getReader();
|
|
92
|
+
const chunks = [];
|
|
93
|
+
let size = 0;
|
|
94
|
+
try {
|
|
95
|
+
for (;;) {
|
|
96
|
+
const { done, value } = await reader.read();
|
|
97
|
+
if (done)
|
|
98
|
+
break;
|
|
99
|
+
size += value.byteLength;
|
|
100
|
+
if (size > MAX_METADATA_BYTES)
|
|
101
|
+
throw new Error("Metadata too large");
|
|
102
|
+
chunks.push(value);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
void reader.cancel().catch(() => { });
|
|
107
|
+
}
|
|
108
|
+
const release = parseRelease(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
109
|
+
return isNewerStable(release.version, current)
|
|
110
|
+
? { status: "available", release }
|
|
111
|
+
: { status: "current" };
|
|
112
|
+
})(),
|
|
113
|
+
]);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return { status: "unavailable" };
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
abort();
|
|
121
|
+
signal?.removeEventListener("abort", abort);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export function releaseNotice(release) {
|
|
125
|
+
return `Version ${release.version} Available starting on ${release.date}. Use command teamclaws update to update as soon as possible.`;
|
|
126
|
+
}
|
|
127
|
+
const execute = promisify(execFile);
|
|
128
|
+
// npm's JS entrypoint avoids cmd.exe interpolation and .cmd spawning on Windows.
|
|
129
|
+
export async function npmRunner(nodeExecutable = process.execPath, platform = process.platform) {
|
|
130
|
+
const nodeDirectory = dirname(nodeExecutable);
|
|
131
|
+
const candidates = platform === "win32"
|
|
132
|
+
? [join(nodeDirectory, "node_modules/npm/bin/npm-cli.js")]
|
|
133
|
+
: [
|
|
134
|
+
join(nodeDirectory, "npm"),
|
|
135
|
+
resolve(nodeDirectory, "../lib/node_modules/npm/bin/npm-cli.js"),
|
|
136
|
+
];
|
|
137
|
+
for (const candidate of candidates) {
|
|
138
|
+
try {
|
|
139
|
+
const entry = await realpath(candidate);
|
|
140
|
+
const pkg = JSON.parse(await readFile(resolve(dirname(entry), "../package.json"), "utf8"));
|
|
141
|
+
if (pkg.name !== "npm" || basename(entry) !== "npm-cli.js")
|
|
142
|
+
continue;
|
|
143
|
+
return async (args, timeout, signal) => {
|
|
144
|
+
const env = { ...process.env };
|
|
145
|
+
for (const key of Object.keys(env)) {
|
|
146
|
+
if (/^npm_config_/i.test(key) ||
|
|
147
|
+
/^(NODE_OPTIONS|NODE_PATH)$/i.test(key))
|
|
148
|
+
delete env[key];
|
|
149
|
+
}
|
|
150
|
+
// Distinct empty files avoid npm's rejection of loading the same file
|
|
151
|
+
// as both user and global configuration. Prefix discovery is read-only
|
|
152
|
+
// and retains user config; installation uses only the verified prefix.
|
|
153
|
+
const scratch = args[0] === "install"
|
|
154
|
+
? await mkdtemp(join(tmpdir(), "teamclaws-npm-"))
|
|
155
|
+
: undefined;
|
|
156
|
+
try {
|
|
157
|
+
const config = [];
|
|
158
|
+
if (scratch) {
|
|
159
|
+
for (const name of ["user", "global"]) {
|
|
160
|
+
const path = join(scratch, `${name}.npmrc`);
|
|
161
|
+
await writeFile(path, "");
|
|
162
|
+
config.push(`--${name}config=${path}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const { stdout } = await execute(nodeExecutable, [entry, ...config, ...args], {
|
|
166
|
+
cwd: scratch ?? tmpdir(),
|
|
167
|
+
env,
|
|
168
|
+
timeout,
|
|
169
|
+
maxBuffer: 1024 * 1024,
|
|
170
|
+
windowsHide: true,
|
|
171
|
+
...(signal ? { signal } : {}),
|
|
172
|
+
});
|
|
173
|
+
return stdout;
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
if (scratch)
|
|
177
|
+
await rm(scratch, { recursive: true, force: true });
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
/* Try the other supported npm layout. */
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
throw new Error("npm is unavailable beside the running Node installation.");
|
|
186
|
+
}
|
|
187
|
+
export function installerArguments(version, prefix) {
|
|
188
|
+
if (!isAbsolute(prefix) || /[\r\n\0]/.test(prefix))
|
|
189
|
+
throw new Error("Invalid installation prefix");
|
|
190
|
+
if (!semver(version) || semver(version).prerelease)
|
|
191
|
+
throw new Error("Invalid update version");
|
|
192
|
+
return [
|
|
193
|
+
"install",
|
|
194
|
+
"--global",
|
|
195
|
+
`--prefix=${prefix}`,
|
|
196
|
+
`--registry=${OFFICIAL_REGISTRY}`,
|
|
197
|
+
"--ignore-scripts",
|
|
198
|
+
"--no-audit",
|
|
199
|
+
"--no-fund",
|
|
200
|
+
"--update-notifier=false",
|
|
201
|
+
"--",
|
|
202
|
+
`${PACKAGE_NAME}@${version}`,
|
|
203
|
+
];
|
|
204
|
+
}
|
|
205
|
+
export async function installRelease(release, signal, runner, packageRoot = fileURLToPath(new URL("../", import.meta.url)), platform = process.platform) {
|
|
206
|
+
if (!isNewerStable(release.version, PACKAGE_VERSION))
|
|
207
|
+
throw new Error("Refusing an equal, older, or invalid release.");
|
|
208
|
+
const npm = runner ?? (await npmRunner());
|
|
209
|
+
const prefix = (await npm(["prefix", "--global"], 5000, signal)).trim();
|
|
210
|
+
// Verify the active package is the actual global package under npm's reported
|
|
211
|
+
// prefix. Source, npm link, local/npx, pnpm and mismatched npm installs fail.
|
|
212
|
+
if (!prefix ||
|
|
213
|
+
!/^(?:\/|[A-Za-z]:[\\/])/.test(prefix) ||
|
|
214
|
+
/[\r\n\0]/.test(prefix))
|
|
215
|
+
throw new Error("Invalid npm global prefix.");
|
|
216
|
+
const canonicalPrefix = await realpath(prefix);
|
|
217
|
+
const expected = platform === "win32"
|
|
218
|
+
? join(canonicalPrefix, "node_modules", PACKAGE_NAME)
|
|
219
|
+
: join(canonicalPrefix, "lib/node_modules", PACKAGE_NAME);
|
|
220
|
+
const actual = await realpath(packageRoot);
|
|
221
|
+
if (actual !== resolve(expected) || (await realpath(expected)) !== actual)
|
|
222
|
+
throw new Error("Unsupported installation: teamclaws update requires the active npm global installation. Update local, linked, npx, or managed installations with their original package manager.");
|
|
223
|
+
const pkg = JSON.parse(await readFile(join(actual, "package.json"), "utf8"));
|
|
224
|
+
if (pkg.name !== PACKAGE_NAME || pkg.version !== PACKAGE_VERSION)
|
|
225
|
+
throw new Error("Active package identity does not match.");
|
|
226
|
+
await npm(installerArguments(release.version, prefix), 120000, signal);
|
|
227
|
+
const updated = JSON.parse(await readFile(join(actual, "package.json"), "utf8"));
|
|
228
|
+
if (updated.name !== PACKAGE_NAME || updated.version !== release.version)
|
|
229
|
+
throw new Error("npm finished but the installed version could not be verified.");
|
|
230
|
+
}
|
package/dist/validation.js
CHANGED
|
@@ -336,7 +336,7 @@ export function parseServersResponse(value) {
|
|
|
336
336
|
exitCode: 9,
|
|
337
337
|
});
|
|
338
338
|
}
|
|
339
|
-
assertExactKeys(value, ["servers"], [], "server list");
|
|
339
|
+
assertExactKeys(value, ["servers"], ["workspace_count"], "server list");
|
|
340
340
|
if (!Array.isArray(value.servers)) {
|
|
341
341
|
throw new TeamClawsError("invalid_response", "TeamClaws returned an invalid server list.", {
|
|
342
342
|
exitCode: 9,
|
|
@@ -455,4 +455,12 @@ function requireIsoDate(value, label) {
|
|
|
455
455
|
function invalidCredentialFile() {
|
|
456
456
|
return new TeamClawsError("invalid_credential_file", "The local TeamClaws credential file is invalid.", { exitCode: 9 });
|
|
457
457
|
}
|
|
458
|
-
|
|
458
|
+
export function parseWorkspaceInventory(value) {
|
|
459
|
+
const servers = parseServersResponse(value);
|
|
460
|
+
if (!isObject(value) || value.workspace_count === undefined)
|
|
461
|
+
return { servers };
|
|
462
|
+
return {
|
|
463
|
+
servers,
|
|
464
|
+
workspace_count: requireInteger(value.workspace_count, "workspace count", servers.length, Number.MAX_SAFE_INTEGER),
|
|
465
|
+
};
|
|
466
|
+
}
|