@clawscarf/cli 0.1.0-alpha.5 → 0.1.0-alpha.6
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 +3 -0
- package/package.json +2 -1
- package/pnpm-lock.yaml +37 -0
- package/recipes/team-server/recipe.json +1 -1
- package/release/README.md +19 -0
- package/release/components.json +1 -1
- package/release/telemetry.json +4 -0
- package/runtime/configuration.js +2 -1
- package/runtime/releases/{0.1.0-alpha.5.json → 0.1.0-alpha.6.json} +16 -16
- package/scripts/clawscarf.js +9 -1
- package/scripts/installation/command.js +5 -2
- package/scripts/installation/installer/run.js +2 -1
- package/scripts/telemetry.js +282 -0
package/README.md
CHANGED
|
@@ -80,6 +80,9 @@ For automation, the same configuration choices are available as command-line fla
|
|
|
80
80
|
with `--non-interactive`, and commands support `--json`.
|
|
81
81
|
[CLI reference and examples →](https://github.com/clawscarf/clawscarf/blob/main/deploy/deployment/installation.md#configure-without-prompts)
|
|
82
82
|
|
|
83
|
+
See [CLI telemetry](https://github.com/clawscarf/clawscarf/blob/main/deploy/deployment/installation.md#telemetry) for usage reporting
|
|
84
|
+
and how to disable it.
|
|
85
|
+
|
|
83
86
|
## Your server, your team
|
|
84
87
|
|
|
85
88
|
An installation serves **one trusted team**: separate logins and roles, shared
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.1.0-alpha.
|
|
2
|
+
"version": "0.1.0-alpha.6",
|
|
3
3
|
"engines": {
|
|
4
4
|
"node": ">=24.16.0 <25 || >=26.1.0"
|
|
5
5
|
},
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"node-pg-migrate": "9.0.0",
|
|
12
12
|
"openid-client": "6.8.8",
|
|
13
13
|
"pg": "8.23.0",
|
|
14
|
+
"posthog-node": "5.52.5",
|
|
14
15
|
"proper-lockfile": "4.1.2",
|
|
15
16
|
"undici": "8.10.2",
|
|
16
17
|
"yaml": "2.9.1",
|
package/pnpm-lock.yaml
CHANGED
|
@@ -26,6 +26,9 @@ importers:
|
|
|
26
26
|
pg:
|
|
27
27
|
specifier: 8.23.0
|
|
28
28
|
version: 8.23.0
|
|
29
|
+
posthog-node:
|
|
30
|
+
specifier: 5.52.5
|
|
31
|
+
version: 5.52.5
|
|
29
32
|
proper-lockfile:
|
|
30
33
|
specifier: 4.1.2
|
|
31
34
|
version: 4.1.2
|
|
@@ -556,6 +559,18 @@ packages:
|
|
|
556
559
|
integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==
|
|
557
560
|
}
|
|
558
561
|
|
|
562
|
+
'@posthog/core@1.55.1':
|
|
563
|
+
resolution:
|
|
564
|
+
{
|
|
565
|
+
integrity: sha512-S76bSbCHGVC8oQa1zyue6A6WzkNf1Ue4oRvfvBNLRnFM3UoWnpWnQ4T/4IzrIEtVCQYv1OI7gk8uL8/U6Tuu7w==
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
'@posthog/types@1.412.4':
|
|
569
|
+
resolution:
|
|
570
|
+
{
|
|
571
|
+
integrity: sha512-Q7lV9O9TbLngjOYw1ucm3bS3tn48xb1B4ZQdDy8gv7yfKe99hCTeWYG4xZ36nJM80VxpTF9TriiJt5hngDOkBg==
|
|
572
|
+
}
|
|
573
|
+
|
|
559
574
|
'@seriousme/openapi-schema-validator@2.9.1':
|
|
560
575
|
resolution:
|
|
561
576
|
{
|
|
@@ -1917,6 +1932,18 @@ packages:
|
|
|
1917
1932
|
}
|
|
1918
1933
|
engines: { node: '>=0.10.0' }
|
|
1919
1934
|
|
|
1935
|
+
posthog-node@5.52.5:
|
|
1936
|
+
resolution:
|
|
1937
|
+
{
|
|
1938
|
+
integrity: sha512-r4KRXh2MvcHYh1s3NUca022GqujkJjpmD4qbRzWOLVTu8BfN3fqZj+MyKIxvHPXsS8sKsNVX+MlKk2qzW0Y4GQ==
|
|
1939
|
+
}
|
|
1940
|
+
engines: { node: ^20.20.0 || >=22.22.0 }
|
|
1941
|
+
peerDependencies:
|
|
1942
|
+
rxjs: ^7.0.0
|
|
1943
|
+
peerDependenciesMeta:
|
|
1944
|
+
rxjs:
|
|
1945
|
+
optional: true
|
|
1946
|
+
|
|
1920
1947
|
powershell-utils@0.1.0:
|
|
1921
1948
|
resolution:
|
|
1922
1949
|
{
|
|
@@ -2714,6 +2741,12 @@ snapshots:
|
|
|
2714
2741
|
|
|
2715
2742
|
'@pinojs/redact@0.4.0': {}
|
|
2716
2743
|
|
|
2744
|
+
'@posthog/core@1.55.1':
|
|
2745
|
+
dependencies:
|
|
2746
|
+
'@posthog/types': 1.412.4
|
|
2747
|
+
|
|
2748
|
+
'@posthog/types@1.412.4': {}
|
|
2749
|
+
|
|
2717
2750
|
'@seriousme/openapi-schema-validator@2.9.1':
|
|
2718
2751
|
dependencies:
|
|
2719
2752
|
ajv: 8.20.0
|
|
@@ -3526,6 +3559,10 @@ snapshots:
|
|
|
3526
3559
|
dependencies:
|
|
3527
3560
|
xtend: 4.0.2
|
|
3528
3561
|
|
|
3562
|
+
posthog-node@5.52.5:
|
|
3563
|
+
dependencies:
|
|
3564
|
+
'@posthog/core': 1.55.1
|
|
3565
|
+
|
|
3529
3566
|
powershell-utils@0.1.0: {}
|
|
3530
3567
|
|
|
3531
3568
|
prelude-ls@1.2.1: {}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "team-server",
|
|
3
3
|
"version": "0.1.1",
|
|
4
|
-
"runtime": "../../runtime/releases/0.1.0-alpha.
|
|
4
|
+
"runtime": "../../runtime/releases/0.1.0-alpha.6.json",
|
|
5
5
|
"name": "Team server",
|
|
6
6
|
"description": "A protected OpenClaw server for your team, with hosted login, managed models and optional Connections.",
|
|
7
7
|
"maturity": "supported",
|
package/release/README.md
CHANGED
|
@@ -159,3 +159,22 @@ an existing output directory and removes incomplete output on failure. It does n
|
|
|
159
159
|
build images, copy recipes/packs or publish artifacts. A custom recipe can point to
|
|
160
160
|
the resulting runtime definition. The [component pins](components.json) record
|
|
161
161
|
upstream sources; [operator packaging](operator.md) describes the CLI archive.
|
|
162
|
+
|
|
163
|
+
## CLI telemetry destination
|
|
164
|
+
|
|
165
|
+
[telemetry.json](telemetry.json) is copied unchanged into the compiled operator,
|
|
166
|
+
npm CLI and standalone CLI payload. It selects the ClawScarf project in PostHog EU
|
|
167
|
+
using `host` (the HTTPS ingestion origin) and `projectToken` (the public `phc_`
|
|
168
|
+
project token). Setting the file to `null` disables reporting for that build.
|
|
169
|
+
Never put a personal or project-secret API key in this public file. A loopback
|
|
170
|
+
HTTP origin is accepted for local receiver tests only.
|
|
171
|
+
|
|
172
|
+
The [CLI telemetry guide](../deploy/deployment/installation.md#telemetry) owns event
|
|
173
|
+
fields, user opt-out, local identity and delivery limits. Destination changes need
|
|
174
|
+
a new CLI build/package; they do not change running team servers or cloud-service
|
|
175
|
+
configuration. Automated tests use local receivers. The destination project's
|
|
176
|
+
**Settings → Privacy → Discard client IP data** must stay enabled: a null `$ip`
|
|
177
|
+
property alone does not prevent PostHog from storing the connection IP. The CLI
|
|
178
|
+
also disables GeoIP enrichment. When changing destinations, verify that privacy
|
|
179
|
+
setting and confirm start/finish events and their properties in the target project
|
|
180
|
+
before publishing. An ingestion check does not establish CLI release publication.
|
package/release/components.json
CHANGED
package/runtime/configuration.js
CHANGED
|
@@ -35,6 +35,7 @@ export function initialConfiguration(input) {
|
|
|
35
35
|
bind: "loopback",
|
|
36
36
|
port: 18789,
|
|
37
37
|
terminal: { enabled: false },
|
|
38
|
+
cliAgents: { enabled: false },
|
|
38
39
|
trustedProxies: ["127.0.0.1"],
|
|
39
40
|
controlUi: {
|
|
40
41
|
allowedOrigins: [settings.publicOrigin],
|
|
@@ -63,7 +64,7 @@ export function initialConfiguration(input) {
|
|
|
63
64
|
member: {
|
|
64
65
|
agents: "*",
|
|
65
66
|
scopes: ["operator.read", "operator.write", "operator.talk"],
|
|
66
|
-
sessions: { others: "
|
|
67
|
+
sessions: { others: "view" },
|
|
67
68
|
sandbox: "inherit",
|
|
68
69
|
},
|
|
69
70
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
4
|
-
"sourceRevision": "
|
|
3
|
+
"version": "0.1.0-alpha.6",
|
|
4
|
+
"sourceRevision": "3527b1e4ce8218d5b144f267cafe4115aa160b85",
|
|
5
5
|
"platforms": [
|
|
6
6
|
"darwin-arm64",
|
|
7
7
|
"linux-arm64",
|
|
@@ -9,16 +9,16 @@
|
|
|
9
9
|
],
|
|
10
10
|
"images": {
|
|
11
11
|
"postgres": "postgres@sha256:742f40ea20b9ff2ff31db5458d127452988a2164df9e17441e191f3b72252193",
|
|
12
|
-
"gateway": "ghcr.io/clawscarf/clawscarf/runtime@sha256:
|
|
13
|
-
"companion": "ghcr.io/clawscarf/clawscarf/companion@sha256:
|
|
14
|
-
"openshellClient": "ghcr.io/clawscarf/clawscarf/openshell-client@sha256:
|
|
15
|
-
"relay": "ghcr.io/clawscarf/clawscarf/browser-relay@sha256:
|
|
12
|
+
"gateway": "ghcr.io/clawscarf/clawscarf/runtime@sha256:24a7ef582b70cff88e09aed5d1ef7350e3a2afcfad251590a7740bf7c8b8c28b",
|
|
13
|
+
"companion": "ghcr.io/clawscarf/clawscarf/companion@sha256:5821b3770d19a19cfdb9faf3ee357a47c790fcc79a43d3ba4cf59f6962dc5428",
|
|
14
|
+
"openshellClient": "ghcr.io/clawscarf/clawscarf/openshell-client@sha256:69c6fb88be2b14fa0fd5ac8b5c5274195cdb5182b256aeffe8955bbd501db0db",
|
|
15
|
+
"relay": "ghcr.io/clawscarf/clawscarf/browser-relay@sha256:cf07a39071b22cb362125bc000edef8c4694b9089c075df42fdb4235cd25c12a",
|
|
16
16
|
"models": "ghcr.io/berriai/litellm:v1.100.1@sha256:a3715fa7ad8387941ab697259bd2881d68931657247a41984f90fae6d11c62bf",
|
|
17
17
|
"browser": {
|
|
18
|
-
"chromium": "ghcr.io/clawscarf/clawscarf/browser@sha256:
|
|
19
|
-
"node": "ghcr.io/clawscarf/clawscarf/browser-node@sha256:
|
|
20
|
-
"dns": "ghcr.io/clawscarf/clawscarf/browser-dns@sha256:
|
|
21
|
-
"egress": "ghcr.io/clawscarf/clawscarf/browser-egress@sha256:
|
|
18
|
+
"chromium": "ghcr.io/clawscarf/clawscarf/browser@sha256:6289b18228ff6bd56a59fe4a7974299e64726696c8c7a10f42bff35a919bc9bb",
|
|
19
|
+
"node": "ghcr.io/clawscarf/clawscarf/browser-node@sha256:daf1e454db411836e595c167f924a9c951b4e6cdd40b8891d3fbd0e7a328afbb",
|
|
20
|
+
"dns": "ghcr.io/clawscarf/clawscarf/browser-dns@sha256:dd543e51e8a1c52e2165bda661c2a866bcffaf198bc961f7a537c7af8b90e244",
|
|
21
|
+
"egress": "ghcr.io/clawscarf/clawscarf/browser-egress@sha256:cca3d28d58002f91a03a548032a30ae5da726bcaee1ec65062581bbfb03dcbd9"
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"tools": {
|
|
@@ -28,34 +28,34 @@
|
|
|
28
28
|
"darwin-arm64": {
|
|
29
29
|
"file": "tools/darwin-arm64/openshell",
|
|
30
30
|
"sha256": "0baeeffe0e4c184b08fde910ac9e8815fb0b8cfb808e49c0e634d7db65fb03bb",
|
|
31
|
-
"url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.
|
|
31
|
+
"url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.6/openshell-darwin-arm64"
|
|
32
32
|
},
|
|
33
33
|
"linux-arm64": {
|
|
34
34
|
"file": "tools/linux-arm64/openshell",
|
|
35
35
|
"sha256": "84df9f059b16b66c83748606cc87d9edcda84b57effea19543f5c368aba7eb18",
|
|
36
|
-
"url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.
|
|
36
|
+
"url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.6/openshell-linux-arm64"
|
|
37
37
|
},
|
|
38
38
|
"linux-x64": {
|
|
39
39
|
"file": "tools/linux-x64/openshell",
|
|
40
40
|
"sha256": "24e85062073d512d1951c76cd3890b7bce1ab01cc36a08f88b3b43c067a402da",
|
|
41
|
-
"url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.
|
|
41
|
+
"url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.6/openshell-linux-x64"
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
"gateway": {
|
|
45
45
|
"darwin-arm64": {
|
|
46
46
|
"file": "tools/darwin-arm64/openshell-gateway",
|
|
47
47
|
"sha256": "298706259ef18350f334c222e981e91583558511d50bce22727c689d261203e6",
|
|
48
|
-
"url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.
|
|
48
|
+
"url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.6/openshell-gateway-darwin-arm64"
|
|
49
49
|
},
|
|
50
50
|
"linux-arm64": {
|
|
51
51
|
"file": "tools/linux-arm64/openshell-gateway",
|
|
52
52
|
"sha256": "0e660428e655e7481f34ba8a4b351554400baf791863d32555c98dddb3f09ae2",
|
|
53
|
-
"url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.
|
|
53
|
+
"url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.6/openshell-gateway-linux-arm64"
|
|
54
54
|
},
|
|
55
55
|
"linux-x64": {
|
|
56
56
|
"file": "tools/linux-x64/openshell-gateway",
|
|
57
57
|
"sha256": "780fc7ad871e2163f3fb866a2d0254ee682fc0d83e6261a84b350c8dcdd9f47b",
|
|
58
|
-
"url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.
|
|
58
|
+
"url": "https://github.com/clawscarf/clawscarf/releases/download/v0.1.0-alpha.6/openshell-gateway-linux-x64"
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
}
|
package/scripts/clawscarf.js
CHANGED
|
@@ -8,7 +8,11 @@ import { LocalSetupError } from "./deployment/process.js";
|
|
|
8
8
|
import { LocalDatabaseError } from "./deployment/database.js";
|
|
9
9
|
import { InstallerCancelled } from "./installation/installer/prompts.js";
|
|
10
10
|
import { installationCommand } from "./installation/command.js";
|
|
11
|
-
|
|
11
|
+
import { CliTelemetry } from "./telemetry.js";
|
|
12
|
+
const telemetry = new CliTelemetry();
|
|
13
|
+
const program = installationCommand(telemetry);
|
|
14
|
+
program.hook("preAction", async (_program, command) => telemetry.start(command));
|
|
15
|
+
let failureCode;
|
|
12
16
|
const commands = [program];
|
|
13
17
|
for (const command of commands) {
|
|
14
18
|
command.exitOverride().configureOutput({ outputError: () => { } });
|
|
@@ -85,8 +89,12 @@ catch (error) {
|
|
|
85
89
|
? error.message
|
|
86
90
|
: "Check the configuration, file permissions and local prerequisites. No operation was automatically retried."),
|
|
87
91
|
};
|
|
92
|
+
failureCode = failure.code;
|
|
88
93
|
process.stderr.write((program.opts().json
|
|
89
94
|
? JSON.stringify(failure)
|
|
90
95
|
: `Error: ${failure.detail}`) + "\n");
|
|
91
96
|
process.exitCode = error instanceof InstallerCancelled ? 130 : 1;
|
|
92
97
|
}
|
|
98
|
+
finally {
|
|
99
|
+
await telemetry.finish(process.exitCode, failureCode);
|
|
100
|
+
}
|
|
@@ -20,12 +20,13 @@ import { runConfiguration } from "./installer/run.js";
|
|
|
20
20
|
import { installationOptions } from "./options.js";
|
|
21
21
|
import { InstallationError } from "./errors.js";
|
|
22
22
|
import { administratorSetup } from "./administrator.js";
|
|
23
|
-
export function installationCommand() {
|
|
23
|
+
export function installationCommand(observation) {
|
|
24
24
|
const program = new Command("clawscarf")
|
|
25
25
|
.description("Install and operate a protected OpenClaw team server.")
|
|
26
26
|
.option("--json", "Print machine-readable results; diagnostics go to stderr");
|
|
27
27
|
program.addCommand(peopleCommand(program));
|
|
28
28
|
const output = (value, human) => {
|
|
29
|
+
observation?.result(value);
|
|
29
30
|
writeResult(program, value, human);
|
|
30
31
|
};
|
|
31
32
|
installationOptions(program.command("configure"))
|
|
@@ -43,9 +44,11 @@ export function installationCommand() {
|
|
|
43
44
|
const result = await runConfiguration({
|
|
44
45
|
...options,
|
|
45
46
|
...program.opts(),
|
|
46
|
-
});
|
|
47
|
+
}, (mode) => observation?.configurationMode(mode));
|
|
47
48
|
if (options.nonInteractive)
|
|
48
49
|
output(result);
|
|
50
|
+
else
|
|
51
|
+
observation?.result(result);
|
|
49
52
|
});
|
|
50
53
|
program
|
|
51
54
|
.command("recipes")
|
|
@@ -161,7 +161,7 @@ function quote(value) {
|
|
|
161
161
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
162
162
|
}
|
|
163
163
|
/** The command selects the workflow; users never manipulate preparation files. */
|
|
164
|
-
export async function runConfiguration(options) {
|
|
164
|
+
export async function runConfiguration(options, observeMode) {
|
|
165
165
|
options = {
|
|
166
166
|
...options,
|
|
167
167
|
directory: options.directory ?? defaultInstallationDirectory,
|
|
@@ -193,6 +193,7 @@ export async function runConfiguration(options) {
|
|
|
193
193
|
throw error;
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
|
+
observeMode?.(existing ? "edit" : "new");
|
|
196
197
|
let result = existing && state
|
|
197
198
|
? await editInstallationSettings(state, ui, options)
|
|
198
199
|
: await installFromAnswers(options, ui, operations, (message, work) => progress(message, work, options));
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import packageInfo from "../package.json" with { type: "json" };
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { mkdir, open, readFile } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { PostHog } from "posthog-node";
|
|
7
|
+
import { Agent, fetch } from "undici";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
const destinationSchema = z.strictObject({
|
|
10
|
+
host: z.url().refine((value) => {
|
|
11
|
+
const url = new URL(value);
|
|
12
|
+
return (!url.username &&
|
|
13
|
+
!url.password &&
|
|
14
|
+
!url.search &&
|
|
15
|
+
!url.hash &&
|
|
16
|
+
url.pathname === "/" &&
|
|
17
|
+
(url.protocol === "https:" ||
|
|
18
|
+
(url.protocol === "http:" && url.hostname === "127.0.0.1")));
|
|
19
|
+
}),
|
|
20
|
+
projectToken: z.string().regex(/^phc_[A-Za-z0-9_-]+$/),
|
|
21
|
+
});
|
|
22
|
+
// Deliberately finite: remote errors and OperatorError.code can contain arbitrary text.
|
|
23
|
+
const errorCodeSchema = z.enum([
|
|
24
|
+
"invalid_configuration",
|
|
25
|
+
"unsupported_platform",
|
|
26
|
+
"release_mismatch",
|
|
27
|
+
"stale_plan",
|
|
28
|
+
"change_unsupported",
|
|
29
|
+
"unavailable",
|
|
30
|
+
"operation_failed",
|
|
31
|
+
"invalid_arguments",
|
|
32
|
+
"operation_busy",
|
|
33
|
+
"invalid_team_configuration",
|
|
34
|
+
"upgrade_pending",
|
|
35
|
+
"upgrade_refused",
|
|
36
|
+
"upgrade_outcome_unknown",
|
|
37
|
+
"runtime_binding_changed",
|
|
38
|
+
"runtime_binding_unavailable",
|
|
39
|
+
"administrator_unverified",
|
|
40
|
+
"bootstrap_outcome_unknown",
|
|
41
|
+
"browser_unavailable",
|
|
42
|
+
"command_failed",
|
|
43
|
+
"configuration_changed",
|
|
44
|
+
"database_start_failed",
|
|
45
|
+
"executable_unavailable",
|
|
46
|
+
"incomplete_certificate",
|
|
47
|
+
"invalid_certificate",
|
|
48
|
+
"invalid_model_setup",
|
|
49
|
+
"model_credential_pending",
|
|
50
|
+
"invalid_connections_setup",
|
|
51
|
+
"connections_catalog_blocked",
|
|
52
|
+
"connections_configuration_pending",
|
|
53
|
+
"connections_configuration_refused",
|
|
54
|
+
"connections_configuration_unavailable",
|
|
55
|
+
"invalid_runtime_policy",
|
|
56
|
+
"native_unavailable",
|
|
57
|
+
"network_identity_changed",
|
|
58
|
+
"network_lookup_incomplete",
|
|
59
|
+
"network_outcome_unknown",
|
|
60
|
+
"network_unprepared",
|
|
61
|
+
"platform_unqualified",
|
|
62
|
+
"port_check_failed",
|
|
63
|
+
"port_in_use",
|
|
64
|
+
"private_directory_required",
|
|
65
|
+
"runtime_failed",
|
|
66
|
+
"runtime_identity_changed",
|
|
67
|
+
"runtime_lookup_incomplete",
|
|
68
|
+
"runtime_outcome_unknown",
|
|
69
|
+
"runtime_start_failed",
|
|
70
|
+
"runtime_stop_pending",
|
|
71
|
+
"startup_interrupted",
|
|
72
|
+
"startup_timed_out",
|
|
73
|
+
"unowned_directory",
|
|
74
|
+
"ownership_conflict",
|
|
75
|
+
"runtime_credentials_changed",
|
|
76
|
+
"database_setup_failed",
|
|
77
|
+
"outcome_unknown",
|
|
78
|
+
"unauthorized",
|
|
79
|
+
"forbidden",
|
|
80
|
+
"ENOENT",
|
|
81
|
+
"ENOTDIR",
|
|
82
|
+
"EACCES",
|
|
83
|
+
"EPERM",
|
|
84
|
+
"EEXIST",
|
|
85
|
+
"ELOOP",
|
|
86
|
+
"ENOSPC",
|
|
87
|
+
]);
|
|
88
|
+
/** One CLI invocation. No runtime, server, browser, or automatic exception instrumentation. */
|
|
89
|
+
export class CliTelemetry {
|
|
90
|
+
environment;
|
|
91
|
+
destinationFile;
|
|
92
|
+
client;
|
|
93
|
+
dispatcher;
|
|
94
|
+
started;
|
|
95
|
+
startTime = 0;
|
|
96
|
+
distinctId = "";
|
|
97
|
+
command = "";
|
|
98
|
+
action;
|
|
99
|
+
interactive = false;
|
|
100
|
+
version = "";
|
|
101
|
+
invocationId = randomUUID();
|
|
102
|
+
outcome = "success";
|
|
103
|
+
mode;
|
|
104
|
+
configuration;
|
|
105
|
+
constructor(environment = process.env, destinationFile = new URL("../release/telemetry.json", import.meta.url)) {
|
|
106
|
+
this.environment = environment;
|
|
107
|
+
this.destinationFile = destinationFile;
|
|
108
|
+
}
|
|
109
|
+
async start(command) {
|
|
110
|
+
if (this.environment.CLAWSCARF_TELEMETRY_DISABLED === "1")
|
|
111
|
+
return;
|
|
112
|
+
try {
|
|
113
|
+
const destination = destinationSchema
|
|
114
|
+
.nullable()
|
|
115
|
+
.parse(JSON.parse(await readFile(this.destinationFile, "utf8")));
|
|
116
|
+
if (!destination)
|
|
117
|
+
return;
|
|
118
|
+
const manifest = z
|
|
119
|
+
.object({
|
|
120
|
+
version: z.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
|
|
121
|
+
})
|
|
122
|
+
.parse(packageInfo);
|
|
123
|
+
this.version = manifest.version;
|
|
124
|
+
const directory = join(this.environment.XDG_CONFIG_HOME || homedir(), this.environment.XDG_CONFIG_HOME ? "clawscarf" : ".config/clawscarf");
|
|
125
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
126
|
+
const file = join(directory, "telemetry-id");
|
|
127
|
+
try {
|
|
128
|
+
const handle = await open(file, "wx", 0o600);
|
|
129
|
+
try {
|
|
130
|
+
await handle.writeFile(randomUUID() + "\n");
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
await handle.close();
|
|
134
|
+
}
|
|
135
|
+
process.stderr.write("ClawScarf reports CLI usage and failure codes to PostHog. Disable with CLAWSCARF_TELEMETRY_DISABLED=1. Details: https://github.com/clawscarf/clawscarf/blob/main/deploy/deployment/installation.md#telemetry\n");
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (!(error instanceof Error &&
|
|
139
|
+
"code" in error &&
|
|
140
|
+
error.code === "EEXIST"))
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
// A concurrent creator may still be writing. Skip this invocation if incomplete.
|
|
144
|
+
this.distinctId = z.uuid().parse((await readFile(file, "utf8")).trim());
|
|
145
|
+
const names = [];
|
|
146
|
+
for (let current = command; current.parent; current = current.parent)
|
|
147
|
+
names.unshift(current.name());
|
|
148
|
+
this.command = names.join(" ");
|
|
149
|
+
const options = command.optsWithGlobals();
|
|
150
|
+
this.interactive =
|
|
151
|
+
[process.stdin.isTTY, process.stdout.isTTY].every((isTTY) => isTTY) &&
|
|
152
|
+
!options.nonInteractive;
|
|
153
|
+
if (this.command === "stop")
|
|
154
|
+
this.action = options.delete ? "delete" : "stop";
|
|
155
|
+
const dispatcher = new Agent({ connect: { timeout: 500 } });
|
|
156
|
+
this.dispatcher = dispatcher;
|
|
157
|
+
this.client = new PostHog(destination.projectToken, {
|
|
158
|
+
host: destination.host,
|
|
159
|
+
isServer: false,
|
|
160
|
+
disableGeoip: true,
|
|
161
|
+
enableExceptionAutocapture: false,
|
|
162
|
+
enableLocalEvaluation: false,
|
|
163
|
+
flushInterval: 0,
|
|
164
|
+
fetchRetryCount: 0,
|
|
165
|
+
requestTimeout: 500,
|
|
166
|
+
disableCompression: true,
|
|
167
|
+
fetch: async (url, options) => {
|
|
168
|
+
// Only uncompressed event JSON is supported; never follow ingestion redirects.
|
|
169
|
+
if (typeof options.body !== "string")
|
|
170
|
+
throw Error("Expected event JSON");
|
|
171
|
+
const response = await fetch(url, {
|
|
172
|
+
method: options.method,
|
|
173
|
+
headers: options.headers,
|
|
174
|
+
body: options.body,
|
|
175
|
+
dispatcher,
|
|
176
|
+
redirect: "error",
|
|
177
|
+
signal: AbortSignal.timeout(500),
|
|
178
|
+
});
|
|
179
|
+
const body = await response.text();
|
|
180
|
+
return {
|
|
181
|
+
status: response.status,
|
|
182
|
+
headers: response.headers,
|
|
183
|
+
text: () => Promise.resolve(body),
|
|
184
|
+
json: () => Promise.resolve().then(() => JSON.parse(body)),
|
|
185
|
+
};
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
this.startTime = performance.now();
|
|
189
|
+
this.started = this.capture("cli_command_started");
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// Telemetry configuration/storage must never prevent a command from running.
|
|
193
|
+
await this.dispatcher?.destroy().catch(() => { });
|
|
194
|
+
this.client = undefined;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
configurationMode(mode) {
|
|
198
|
+
this.mode = mode;
|
|
199
|
+
}
|
|
200
|
+
result(value) {
|
|
201
|
+
if (!value || typeof value !== "object")
|
|
202
|
+
return;
|
|
203
|
+
if ("state" in value && value.state === "cancelled")
|
|
204
|
+
this.outcome = "cancelled";
|
|
205
|
+
else if ("state" in value && value.state === "action_required")
|
|
206
|
+
this.outcome = "action_required";
|
|
207
|
+
else if (this.command === "configure" || this.command === "start") {
|
|
208
|
+
if ("ready" in value && value.ready === false)
|
|
209
|
+
this.outcome = "action_required";
|
|
210
|
+
}
|
|
211
|
+
if (this.command !== "configure")
|
|
212
|
+
return;
|
|
213
|
+
if ("ready" in value && value.ready === true)
|
|
214
|
+
this.configuration = "ready";
|
|
215
|
+
else if ("state" in value && value.state === "unchanged")
|
|
216
|
+
this.configuration = "unchanged";
|
|
217
|
+
else if ("state" in value && value.state === "prepared")
|
|
218
|
+
this.configuration = "saved";
|
|
219
|
+
}
|
|
220
|
+
async finish(exitCode, failureCode) {
|
|
221
|
+
if (!this.client)
|
|
222
|
+
return;
|
|
223
|
+
const outcome = exitCode === 130 || failureCode === "cancelled"
|
|
224
|
+
? "cancelled"
|
|
225
|
+
: failureCode !== undefined ||
|
|
226
|
+
(exitCode !== undefined && Number(exitCode) !== 0)
|
|
227
|
+
? "failure"
|
|
228
|
+
: this.outcome;
|
|
229
|
+
const code = errorCodeSchema.safeParse(failureCode);
|
|
230
|
+
try {
|
|
231
|
+
await Promise.all([
|
|
232
|
+
this.started,
|
|
233
|
+
this.capture("cli_command_finished", {
|
|
234
|
+
outcome,
|
|
235
|
+
duration_ms: Math.round(performance.now() - this.startTime),
|
|
236
|
+
...(outcome === "failure"
|
|
237
|
+
? {
|
|
238
|
+
error_code: code.success ? code.data : "operation_failed",
|
|
239
|
+
operation: this.command,
|
|
240
|
+
}
|
|
241
|
+
: {}),
|
|
242
|
+
...(this.mode ? { configuration_mode: this.mode } : {}),
|
|
243
|
+
...(this.configuration
|
|
244
|
+
? { configuration_outcome: this.configuration }
|
|
245
|
+
: {}),
|
|
246
|
+
}),
|
|
247
|
+
]);
|
|
248
|
+
await this.client.shutdown(1000);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// Best effort only: no retries, offline queue, output, or changed exit status.
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
await this.dispatcher?.destroy().catch(() => { });
|
|
255
|
+
this.client = undefined;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async capture(event, result = {}) {
|
|
259
|
+
try {
|
|
260
|
+
await this.client?.captureImmediate({
|
|
261
|
+
distinctId: this.distinctId,
|
|
262
|
+
event,
|
|
263
|
+
timestamp: new Date(),
|
|
264
|
+
properties: {
|
|
265
|
+
command: this.command,
|
|
266
|
+
invocation_id: this.invocationId,
|
|
267
|
+
cli_version: this.version,
|
|
268
|
+
os: process.platform,
|
|
269
|
+
architecture: process.arch,
|
|
270
|
+
interactive: this.interactive,
|
|
271
|
+
...(this.action ? { action: this.action } : {}),
|
|
272
|
+
...result,
|
|
273
|
+
$process_person_profile: false,
|
|
274
|
+
$ip: null,
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// Never forward telemetry errors (including remote response bodies) to output.
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|