squads-cli 0.2.0 → 0.4.0

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.
@@ -0,0 +1,10 @@
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+ COPY requirements.txt .
5
+ RUN pip install --no-cache-dir -r requirements.txt
6
+
7
+ COPY main.py .
8
+
9
+ ENV PORT=8080
10
+ CMD exec gunicorn --bind :$PORT --workers 1 --threads 2 --timeout 30 main:app
@@ -0,0 +1,69 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ PROJECT_ID="inspired-answer-481202-f6"
5
+ REGION="us-central1"
6
+ SERVICE_NAME="squads-telemetry"
7
+ TOPIC_ID="squads-cli-telemetry"
8
+ DATASET_ID="telemetry"
9
+ TABLE_ID="cli_events"
10
+
11
+ echo "=== Squads Telemetry Deployment ==="
12
+
13
+ # 1. Enable APIs
14
+ echo "[1/6] Enabling APIs..."
15
+ gcloud services enable \
16
+ pubsub.googleapis.com \
17
+ bigquery.googleapis.com \
18
+ run.googleapis.com \
19
+ cloudbuild.googleapis.com \
20
+ --project=$PROJECT_ID
21
+
22
+ # 2. Create Pub/Sub topic
23
+ echo "[2/6] Creating Pub/Sub topic..."
24
+ gcloud pubsub topics create $TOPIC_ID --project=$PROJECT_ID 2>/dev/null || echo "Topic exists"
25
+
26
+ # 3. Create BigQuery dataset
27
+ echo "[3/6] Creating BigQuery dataset..."
28
+ bq --project_id=$PROJECT_ID mk --dataset $DATASET_ID 2>/dev/null || echo "Dataset exists"
29
+
30
+ # 4. Create BigQuery table
31
+ echo "[4/6] Creating BigQuery table..."
32
+ bq --project_id=$PROJECT_ID mk --table $DATASET_ID.$TABLE_ID \
33
+ event:STRING,timestamp:TIMESTAMP,anonymous_id:STRING,cli_version:STRING,properties:STRING,server_ts:TIMESTAMP,source:STRING \
34
+ 2>/dev/null || echo "Table exists"
35
+
36
+ # 5. Create Pub/Sub → BigQuery subscription
37
+ echo "[5/6] Creating BigQuery subscription..."
38
+ gcloud pubsub subscriptions create ${TOPIC_ID}-bq \
39
+ --topic=$TOPIC_ID \
40
+ --bigquery-table=$PROJECT_ID:$DATASET_ID.$TABLE_ID \
41
+ --use-topic-schema=false \
42
+ --write-metadata \
43
+ --project=$PROJECT_ID 2>/dev/null || echo "Subscription exists"
44
+
45
+ # 6. Deploy Cloud Run
46
+ echo "[6/6] Deploying to Cloud Run..."
47
+ gcloud run deploy $SERVICE_NAME \
48
+ --source=. \
49
+ --region=$REGION \
50
+ --platform=managed \
51
+ --allow-unauthenticated \
52
+ --set-env-vars="GCP_PROJECT=$PROJECT_ID,PUBSUB_TOPIC=$TOPIC_ID" \
53
+ --memory=256Mi \
54
+ --min-instances=0 \
55
+ --max-instances=5 \
56
+ --project=$PROJECT_ID
57
+
58
+ # Get URL
59
+ URL=$(gcloud run services describe $SERVICE_NAME --region=$REGION --project=$PROJECT_ID --format='value(status.url)')
60
+
61
+ echo ""
62
+ echo "=== Deployment Complete ==="
63
+ echo "Endpoint: $URL/ping"
64
+ echo ""
65
+ echo "Test with:"
66
+ echo " curl -X POST $URL/ping -H 'Content-Type: application/json' -d '{\"event\":\"test\"}'"
67
+ echo ""
68
+ echo "Update squads-cli telemetry URL:"
69
+ echo " SQUADS_TELEMETRY_URL=$URL/ping"
@@ -0,0 +1,136 @@
1
+ """
2
+ Squads CLI Telemetry Ping - Public endpoint for anonymous CLI telemetry.
3
+ Receives events and publishes to Pub/Sub for BigQuery streaming.
4
+ """
5
+ import os
6
+ import json
7
+ import hashlib
8
+ import hmac
9
+ from datetime import datetime
10
+ from flask import Flask, request, jsonify
11
+ from google.cloud import pubsub_v1
12
+
13
+ app = Flask(__name__)
14
+
15
+ # Config
16
+ PROJECT_ID = os.environ.get("GCP_PROJECT", "inspired-answer-481202-f6")
17
+ TOPIC_ID = os.environ.get("PUBSUB_TOPIC", "squads-cli-telemetry")
18
+ DEBUG = os.environ.get("DEBUG", "0") == "1"
19
+
20
+ # API Key for validation (prevents spam/contamination)
21
+ # CLI embeds this key (obfuscated) - not true security, but adds friction
22
+ API_KEY = os.environ.get("TELEMETRY_API_KEY", "sq_tel_v1_7f8a9b2c3d4e5f6a")
23
+
24
+ # Valid event prefixes (reject unknown events)
25
+ VALID_EVENT_PREFIXES = ("cli.", "agent.", "squad.", "error.")
26
+
27
+ # Pub/Sub publisher (lazy init)
28
+ publisher = None
29
+
30
+ def get_publisher():
31
+ global publisher
32
+ if publisher is None:
33
+ publisher = pubsub_v1.PublisherClient()
34
+ return publisher
35
+
36
+ def validate_request() -> tuple[bool, str]:
37
+ """Validate API key and basic structure."""
38
+ # Check API key header
39
+ api_key = request.headers.get("X-Squads-Key") or request.headers.get("Authorization", "").replace("Bearer ", "")
40
+ if not api_key or not hmac.compare_digest(api_key, API_KEY):
41
+ return False, "invalid_key"
42
+ return True, ""
43
+
44
+ def validate_event(event: dict) -> bool:
45
+ """Validate event structure."""
46
+ if not isinstance(event, dict):
47
+ return False
48
+
49
+ event_name = event.get("event", "")
50
+ if not event_name or not any(event_name.startswith(p) for p in VALID_EVENT_PREFIXES):
51
+ return False
52
+
53
+ # Must have properties dict (can be empty)
54
+ if "properties" in event and not isinstance(event.get("properties"), dict):
55
+ return False
56
+
57
+ return True
58
+
59
+ def publish_event(event: dict) -> bool:
60
+ """Publish event to Pub/Sub."""
61
+ try:
62
+ pub = get_publisher()
63
+ topic_path = pub.topic_path(PROJECT_ID, TOPIC_ID)
64
+
65
+ # Add server metadata
66
+ event["server_ts"] = datetime.utcnow().isoformat() + "Z"
67
+ event["source"] = "squads-cli"
68
+
69
+ data = json.dumps(event).encode("utf-8")
70
+ future = pub.publish(topic_path, data)
71
+ future.result(timeout=5) # Wait for ack
72
+ return True
73
+ except Exception as e:
74
+ if DEBUG:
75
+ print(f"Pub/Sub error: {e}")
76
+ return False
77
+
78
+ @app.route("/", methods=["GET"])
79
+ def health():
80
+ """Health check."""
81
+ return jsonify({"status": "ok", "service": "squads-telemetry"}), 200
82
+
83
+ @app.route("/ping", methods=["POST"])
84
+ def ping():
85
+ """
86
+ Receive telemetry ping from CLI.
87
+
88
+ Headers:
89
+ X-Squads-Key: <api_key>
90
+
91
+ POST /ping
92
+ Content-Type: application/json
93
+
94
+ {"event": "cli.run", "properties": {"squad": "marketing", "durationMs": 1234}}
95
+ """
96
+ # Validate API key
97
+ valid, error = validate_request()
98
+ if not valid:
99
+ return jsonify({"error": error}), 401
100
+
101
+ try:
102
+ data = request.get_json(silent=True)
103
+ if not data:
104
+ return jsonify({"error": "invalid json"}), 400
105
+
106
+ # Support single event or batch
107
+ events = data.get("events", [data])
108
+
109
+ # Validate and publish
110
+ published = 0
111
+ rejected = 0
112
+ for event in events:
113
+ if validate_event(event):
114
+ if publish_event(event):
115
+ published += 1
116
+ else:
117
+ rejected += 1
118
+
119
+ return jsonify({
120
+ "status": "ok",
121
+ "received": len(events),
122
+ "published": published,
123
+ "rejected": rejected,
124
+ }), 200
125
+
126
+ except Exception as e:
127
+ return jsonify({"error": str(e)}), 500
128
+
129
+ @app.route("/ping", methods=["GET"])
130
+ def ping_get():
131
+ """Simple GET ping for uptime checks."""
132
+ return jsonify({"pong": True, "ts": datetime.utcnow().isoformat()}), 200
133
+
134
+ if __name__ == "__main__":
135
+ port = int(os.environ.get("PORT", 8080))
136
+ app.run(host="0.0.0.0", port=port, debug=DEBUG)
@@ -0,0 +1,3 @@
1
+ flask==3.0.0
2
+ gunicorn==21.2.0
3
+ google-cloud-pubsub==2.18.4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squads-cli",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "A CLI for humans and agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,7 +15,17 @@
15
15
  }
16
16
  },
17
17
  "files": [
18
- "dist"
18
+ "dist",
19
+ "docker/docker-compose.yml",
20
+ "docker/docker-compose.engram.yml",
21
+ "docker/.env.example",
22
+ "docker/init-db.sql",
23
+ "docker/init-engram-db.sql",
24
+ "docker/init-langfuse-db.sh",
25
+ "docker/otel-collector.yaml",
26
+ "docker/README.md",
27
+ "docker/squads-bridge/",
28
+ "docker/telemetry-ping/"
19
29
  ],
20
30
  "scripts": {
21
31
  "build": "tsup",
@@ -56,6 +66,9 @@
56
66
  },
57
67
  "dependencies": {
58
68
  "@supabase/supabase-js": "^2.89.0",
69
+ "@types/blessed": "^0.1.27",
70
+ "blessed": "^0.1.81",
71
+ "blessed-contrib": "^4.11.0",
59
72
  "chalk": "^5.3.0",
60
73
  "commander": "^12.1.0",
61
74
  "dotenv": "^17.2.3",