cohorte 1.2.1 → 1.2.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/CHANGELOG.md +5 -0
- package/README.md +1 -1
- package/package.json +1 -2
- package/profile/SCHEMA.md +2 -2
- package/telemetry/README.md +0 -49
- package/telemetry/collector.mjs +0 -79
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
Entries are shown by `/update-pipeline` ("What's new") after a core refresh. Keep them short,
|
|
4
4
|
user-facing, most recent first. One `## <version> — <YYYY-MM-DD>` section per release.
|
|
5
5
|
|
|
6
|
+
## 1.2.2 — 2026-07-29
|
|
7
|
+
|
|
8
|
+
- The reference collector moved to its own (private) deployment repo; the public repo keeps
|
|
9
|
+
the collector API contract in SCHEMA.md §Telemetry. No behavior change for users.
|
|
10
|
+
|
|
6
11
|
## 1.2.1 — 2026-07-29
|
|
7
12
|
|
|
8
13
|
- Telemetry collector URL shipped as the config-template default
|
package/README.md
CHANGED
|
@@ -277,7 +277,7 @@ result counts, and a *hash* of the feature id — never repo names, paths, code,
|
|
|
277
277
|
recorded so you're never re-asked. Withdraw anytime (`telemetry.enabled: false` in
|
|
278
278
|
`~/.claude/cohorte.config.yaml`); erase your history anytime (`/doctor` prints your `install_id`,
|
|
279
279
|
the collector's `DELETE /v1/install/<id>` drops it). Full spec + GDPR details:
|
|
280
|
-
`profile/SCHEMA.md` §Telemetry
|
|
280
|
+
`profile/SCHEMA.md` §Telemetry (including the collector API contract).
|
|
281
281
|
|
|
282
282
|
## License
|
|
283
283
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cohorte",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Portable, stack-agnostic multi-agent development pipeline for Claude Code — install the core, run /init-pipeline, and it adapts to your project's stack.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cohorte": "bin/cli.js"
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
"dashboard/server",
|
|
17
17
|
"dashboard/dist",
|
|
18
18
|
"dashboard/README.md",
|
|
19
|
-
"telemetry",
|
|
20
19
|
"install.sh",
|
|
21
20
|
"install.ps1",
|
|
22
21
|
"CHANGELOG.md"
|
package/profile/SCHEMA.md
CHANGED
|
@@ -348,9 +348,9 @@ work without revealing what is being built.
|
|
|
348
348
|
the next phase, no restart.
|
|
349
349
|
- **Erasure** — `/doctor` prints your `install_id`; send
|
|
350
350
|
`curl -X DELETE <endpoint-origin>/v1/install/<install_id>` and the collector drops every event
|
|
351
|
-
for that id (the
|
|
351
|
+
for that id (the deployed collector implements this and stores no IPs).
|
|
352
352
|
- **Access/portability** — events are keyed by your `install_id`; ask the operator for an export.
|
|
353
353
|
|
|
354
|
-
**Collector contract** (
|
|
354
|
+
**Collector contract** (any implementation must honor it):
|
|
355
355
|
`POST /v1/events` (one JSON event, allowlisted fields) · `DELETE /v1/install/<id>` (erasure) ·
|
|
356
356
|
`GET /healthz`. Operators must not retain IP-bearing access logs for the ingest vhost.
|
package/telemetry/README.md
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# cohorte telemetry collector (reference)
|
|
2
|
-
|
|
3
|
-
Zero-dependency ingest endpoint for cohorte's **opt-in** anonymous usage pings
|
|
4
|
-
(see the repo README §Privacy and `profile/SCHEMA.md` §Telemetry for what is —
|
|
5
|
-
and is not — collected).
|
|
6
|
-
|
|
7
|
-
## Deploy (any VPS, Node ≥ 18)
|
|
8
|
-
|
|
9
|
-
```sh
|
|
10
|
-
TELEMETRY_DATA=/var/lib/cohorte-telemetry/events.ndjson \
|
|
11
|
-
TELEMETRY_PORT=8787 node collector.mjs
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
Put it behind HTTPS (Caddy example):
|
|
15
|
-
|
|
16
|
-
```
|
|
17
|
-
telemetry.yourdomain.tld {
|
|
18
|
-
reverse_proxy 127.0.0.1:8787
|
|
19
|
-
log { output discard } # GDPR: do NOT keep access logs with IPs for this vhost
|
|
20
|
-
}
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Then set the public URL as the `endpoint:` default in
|
|
24
|
-
`profile/cohorte.config.template.yaml` (anchor `cfg:telemetry_endpoint`) and release —
|
|
25
|
-
consenting installs start sending to it.
|
|
26
|
-
|
|
27
|
-
## API
|
|
28
|
-
|
|
29
|
-
| Method | Path | Purpose |
|
|
30
|
-
|---|---|---|
|
|
31
|
-
| `POST` | `/v1/events` | Ingest one event (strict field allowlist, 4 KB max) |
|
|
32
|
-
| `DELETE` | `/v1/install/<install_id>` | **Right to erasure** — drops every event for that id |
|
|
33
|
-
| `GET` | `/healthz` | Liveness |
|
|
34
|
-
|
|
35
|
-
Storage is an append-only NDJSON file — point your dashboard (or a cron that loads
|
|
36
|
-
into SQLite/Postgres) at it. One JSON object per line:
|
|
37
|
-
|
|
38
|
-
```json
|
|
39
|
-
{"v":1,"install_id":"…","ts":"…","core_version":"1.2.0","os":"Darwin","event":"phase","phase":"build","feature_hash":"a1b2c3d4e5f6","seconds":412,"results":"ok,ok","received_at":"…"}
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
## GDPR checklist (operator side)
|
|
43
|
-
|
|
44
|
-
- [x] Opt-in only — the client never sends without recorded consent
|
|
45
|
-
- [x] No IPs stored (collector ignores them; disable proxy access logs)
|
|
46
|
-
- [x] Data minimization — hashed feature ids, no repo names/paths/code
|
|
47
|
-
- [x] Erasure — `DELETE /v1/install/<id>`; the user finds their id via `/doctor`
|
|
48
|
-
- [x] Withdrawal — `telemetry.enabled: false` in `~/.claude/cohorte.config.yaml`
|
|
49
|
-
- [ ] Your privacy notice — link the repo README §Privacy from wherever you present cohorte
|
package/telemetry/collector.mjs
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Reference telemetry collector for cohorte — zero-dependency, GDPR-first.
|
|
3
|
-
// Deploy on any VPS behind HTTPS (Caddy/nginx). Storage: NDJSON append-only file,
|
|
4
|
-
// one event per line — read it from your own dashboard however you like.
|
|
5
|
-
//
|
|
6
|
-
// TELEMETRY_DATA=/var/lib/cohorte-telemetry/events.ndjson \
|
|
7
|
-
// TELEMETRY_PORT=8787 node collector.mjs
|
|
8
|
-
//
|
|
9
|
-
// GDPR notes:
|
|
10
|
-
// - IPs are NEVER stored (this process doesn't read them; make sure your reverse
|
|
11
|
-
// proxy's access log is disabled or anonymized for this vhost).
|
|
12
|
-
// - DELETE /v1/install/<install_id> erases every event for that id (right to erasure —
|
|
13
|
-
// the install_id printed by /doctor is the user's erasure key).
|
|
14
|
-
// - Events are validated against a strict allowlist of fields; anything else is dropped.
|
|
15
|
-
import { createServer } from "node:http";
|
|
16
|
-
import { appendFileSync, existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
17
|
-
import { dirname } from "node:path";
|
|
18
|
-
|
|
19
|
-
const DATA = process.env.TELEMETRY_DATA || "./events.ndjson";
|
|
20
|
-
const PORT = Number(process.env.TELEMETRY_PORT || 8787);
|
|
21
|
-
const MAX_BODY = 4096;
|
|
22
|
-
|
|
23
|
-
mkdirSync(dirname(DATA), { recursive: true });
|
|
24
|
-
|
|
25
|
-
const FIELDS = ["v", "install_id", "ts", "core_version", "os", "event",
|
|
26
|
-
"phase", "feature_hash", "seconds", "results"];
|
|
27
|
-
const ID_RE = /^[A-Za-z0-9-]{8,64}$/;
|
|
28
|
-
|
|
29
|
-
function sanitize(raw) {
|
|
30
|
-
const e = {};
|
|
31
|
-
for (const k of FIELDS) if (k in raw) e[k] = raw[k];
|
|
32
|
-
if (e.v !== 1) return null;
|
|
33
|
-
if (typeof e.install_id !== "string" || !ID_RE.test(e.install_id)) return null;
|
|
34
|
-
if (typeof e.event !== "string" || e.event.length > 32) return null;
|
|
35
|
-
for (const k of ["ts", "core_version", "os", "phase", "feature_hash", "results"])
|
|
36
|
-
if (k in e && (typeof e[k] !== "string" || e[k].length > 64)) return null;
|
|
37
|
-
if ("seconds" in e && typeof e.seconds !== "number") return null;
|
|
38
|
-
e.received_at = new Date().toISOString();
|
|
39
|
-
return e;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const server = createServer((req, res) => {
|
|
43
|
-
const done = (code, body) => { res.writeHead(code, { "content-type": "application/json" }); res.end(JSON.stringify(body)); };
|
|
44
|
-
|
|
45
|
-
if (req.method === "GET" && req.url === "/healthz") return done(200, { ok: true });
|
|
46
|
-
|
|
47
|
-
if (req.method === "POST" && req.url === "/v1/events") {
|
|
48
|
-
let body = "";
|
|
49
|
-
req.on("data", (c) => { body += c; if (body.length > MAX_BODY) req.destroy(); });
|
|
50
|
-
req.on("end", () => {
|
|
51
|
-
try {
|
|
52
|
-
const e = sanitize(JSON.parse(body));
|
|
53
|
-
if (!e) return done(400, { error: "invalid event" });
|
|
54
|
-
appendFileSync(DATA, JSON.stringify(e) + "\n");
|
|
55
|
-
done(204, {});
|
|
56
|
-
} catch { done(400, { error: "bad json" }); }
|
|
57
|
-
});
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Right to erasure: drop every event carrying this install_id.
|
|
62
|
-
const del = req.url?.match(/^\/v1\/install\/([A-Za-z0-9-]{8,64})$/);
|
|
63
|
-
if (req.method === "DELETE" && del) {
|
|
64
|
-
const id = del[1];
|
|
65
|
-
let kept = [], dropped = 0;
|
|
66
|
-
if (existsSync(DATA))
|
|
67
|
-
for (const line of readFileSync(DATA, "utf8").split("\n")) {
|
|
68
|
-
if (!line.trim()) continue;
|
|
69
|
-
if (line.includes(`"install_id":"${id}"`)) { dropped++; continue; }
|
|
70
|
-
kept.push(line);
|
|
71
|
-
}
|
|
72
|
-
writeFileSync(DATA, kept.length ? kept.join("\n") + "\n" : "");
|
|
73
|
-
return done(200, { erased: dropped });
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
done(404, { error: "not found" });
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
server.listen(PORT, () => console.log(`cohorte telemetry collector on :${PORT} → ${DATA}`));
|