@clix-so/clix-agent-skills 0.1.3 → 0.1.4

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,34 @@
1
+ # Personalization + Audience Mapping
2
+
3
+ ## Contents
4
+
5
+ - How user properties are used
6
+ - Personalization (`user.*`)
7
+ - Audience filters
8
+ - Common mistakes
9
+
10
+ ## How user properties are used
11
+
12
+ User properties power:
13
+
14
+ - **Message personalization** via `user.*`
15
+ - **Audience targeting** (filters on user/custom attributes)
16
+
17
+ ## Personalization (`user.*`)
18
+
19
+ Use `user.*` variables in templates to personalize message content and links.
20
+ Missing values render as empty string.
21
+
22
+ ## Audience filters
23
+
24
+ Audience builder can filter on:
25
+
26
+ - user id
27
+ - built-in attributes (like last session)
28
+ - custom attributes (your user properties)
29
+
30
+ ## Common mistakes
31
+
32
+ - Storing PII (email/phone/name) as user properties by default.
33
+ - Sending inconsistent types for the same property (e.g., `"25"` vs `25`).
34
+ - Renaming keys without migrating campaign filters.
@@ -0,0 +1,65 @@
1
+ # User Property Schema + PII Guardrails
2
+
3
+ ## Contents
4
+
5
+ - Naming rules
6
+ - Allowed types
7
+ - PII guardrails
8
+ - User Plan template
9
+
10
+ ## Naming rules
11
+
12
+ - **Property keys**: `snake_case` recommended.
13
+ - Prefer stable keys that won’t be renamed every sprint.
14
+
15
+ ## Allowed types
16
+
17
+ - `string`
18
+ - `number`
19
+ - `boolean`
20
+
21
+ If you must send complex objects: stringify intentionally and document it.
22
+
23
+ ## PII guardrails
24
+
25
+ Default stance: **do not store PII in user properties** unless explicitly
26
+ approved.
27
+
28
+ Common PII/sensitive values to avoid:
29
+
30
+ - email, phone number, full name
31
+ - free-text fields (notes, messages)
32
+ - government IDs, payment card details
33
+
34
+ Prefer stable internal IDs:
35
+
36
+ - `user_id` (your system’s id) instead of email/phone
37
+
38
+ ## User Plan template
39
+
40
+ Create `.clix/user-plan.json` (recommended) or `user-plan.json` in project root:
41
+
42
+ ```json
43
+ {
44
+ "conventions": {
45
+ "property_key_case": "snake_case"
46
+ },
47
+ "pii": {
48
+ "allowed": [],
49
+ "blocked": ["email", "phone", "name", "free_text"]
50
+ },
51
+ "user_id": {
52
+ "source": "auth response",
53
+ "example": "user_12345"
54
+ },
55
+ "logout_policy": "do_not_set_user_id_null",
56
+ "properties": {
57
+ "subscription_tier": { "type": "string", "required": false },
58
+ "premium": { "type": "boolean", "required": false },
59
+ "age": { "type": "number", "required": false }
60
+ }
61
+ }
62
+ ```
63
+
64
+ **Note**: The validation script validates `logout_policy`, `user_id`, and
65
+ `properties`. The `conventions` and `pii` fields are optional metadata.
@@ -0,0 +1,56 @@
1
+ # Clix User Management Contract
2
+
3
+ ## Contents
4
+
5
+ - Core concepts
6
+ - SDK surface (conceptual)
7
+ - User properties types
8
+ - Platform notes
9
+ - Pitfalls
10
+
11
+ ## Core concepts
12
+
13
+ - **Anonymous user**: default when no user ID is set.
14
+ - **Identified user**: after setting user ID; historical activity is linked.
15
+ - **Multi-device**: user identity can be consistent across devices when using
16
+ the same user ID.
17
+
18
+ ## SDK surface (conceptual)
19
+
20
+ - `setUserId(userId)`
21
+ - `removeUserId()`
22
+ - `setUserProperty(key, value)`
23
+ - `setUserProperties(map)`
24
+ - `removeUserProperty(key)`
25
+ - `removeUserProperties(keys)`
26
+
27
+ ## User properties types
28
+
29
+ Prefer JSON primitives:
30
+
31
+ - **string**
32
+ - **number**
33
+ - **boolean**
34
+
35
+ Avoid:
36
+
37
+ - `null` (inconsistent behavior)
38
+ - nested objects/arrays unless you intentionally stringify them
39
+ - PII unless explicitly approved
40
+
41
+ ## Platform notes
42
+
43
+ - **iOS**: async + sync APIs exist; async is recommended.
44
+ - **Android**: suspend functions; internal device service maps user ID to a user
45
+ property on the device backend.
46
+ - **React Native**: Promise-based APIs.
47
+ - **Flutter**: async APIs; user property model infers type and stringifies
48
+ unsupported values.
49
+
50
+ ## Pitfalls
51
+
52
+ - Calling `setUserId(null)` on logout (don’t).
53
+ - Setting user ID before login is confirmed (causes wrong attribution).
54
+ - Sending PII as user properties by default.
55
+ - Using unstable identifiers (email/phone) as user ID when an internal ID
56
+ exists.
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Validate a Clix user management plan (user-plan.json).
4
+ #
5
+ # Usage:
6
+ # bash skills/user-management/scripts/validate-user-plan.sh path/to/user-plan.json
7
+ #
8
+ set -euo pipefail
9
+
10
+ plan_path="${1:-}"
11
+ if [[ -z "$plan_path" ]]; then
12
+ echo "Usage: bash skills/user-management/scripts/validate-user-plan.sh path/to/user-plan.json" >&2
13
+ exit 2
14
+ fi
15
+
16
+ if [[ ! -f "$plan_path" ]]; then
17
+ echo "Error: file not found: $plan_path" >&2
18
+ exit 2
19
+ fi
20
+
21
+ validate_with_python() {
22
+ python3 - "$plan_path" <<'PY'
23
+ import json
24
+ import re
25
+ import sys
26
+
27
+ path = sys.argv[1]
28
+ snake = re.compile(r"^[a-z][a-z0-9_]*$")
29
+
30
+ with open(path, "r", encoding="utf-8") as f:
31
+ data = json.load(f)
32
+
33
+ errors = []
34
+
35
+ logout_policy = data.get("logout_policy")
36
+ if logout_policy not in (None, "do_not_set_user_id_null"):
37
+ errors.append("logout_policy must be 'do_not_set_user_id_null' if present")
38
+
39
+ user_id = data.get("user_id")
40
+ if not isinstance(user_id, dict):
41
+ errors.append("user_id must be an object")
42
+ else:
43
+ src = user_id.get("source")
44
+ ex = user_id.get("example")
45
+ if not isinstance(src, str) or not src.strip():
46
+ errors.append("user_id.source must be a non-empty string")
47
+ if ex is not None and (not isinstance(ex, str) or not ex.strip()):
48
+ errors.append("user_id.example must be a non-empty string if present")
49
+
50
+ props = data.get("properties")
51
+ if not isinstance(props, dict) or not props:
52
+ errors.append("properties must be a non-empty object")
53
+ else:
54
+ for key, spec in props.items():
55
+ if not isinstance(key, str) or not snake.match(key):
56
+ errors.append(f"properties key '{key}' must be snake_case")
57
+ continue
58
+ if not isinstance(spec, dict):
59
+ errors.append(f"properties['{key}'] must be an object")
60
+ continue
61
+ t = spec.get("type")
62
+ if t is None:
63
+ errors.append(f"properties['{key}'].type is required")
64
+ elif t not in ("string", "number", "boolean"):
65
+ errors.append(f"properties['{key}'].type must be one of: string, number, boolean")
66
+ req = spec.get("required")
67
+ if req is not None and not isinstance(req, bool):
68
+ errors.append(f"properties['{key}'].required must be boolean if present")
69
+
70
+ if errors:
71
+ print("❌ user-plan validation failed:")
72
+ for e in errors:
73
+ print(f"- {e}")
74
+ sys.exit(1)
75
+
76
+ print("✅ user-plan validation passed")
77
+ PY
78
+ }
79
+
80
+ if command -v python3 >/dev/null 2>&1; then
81
+ validate_with_python
82
+ exit 0
83
+ fi
84
+
85
+ echo "Warning: python3 not found; only checking JSON validity with node if available." >&2
86
+ if command -v node >/dev/null 2>&1; then
87
+ node -e "JSON.parse(require('fs').readFileSync(process.argv[1], 'utf8')); console.log('✅ JSON is valid');" "$plan_path"
88
+ exit 0
89
+ fi
90
+
91
+ echo "Error: neither python3 nor node found; cannot validate." >&2
92
+ exit 2