create-egregore 0.19.32 → 0.20.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.
Files changed (173) hide show
  1. package/assets/egregore-launcher.js +118 -13
  2. package/bin/cli.js +48 -3
  3. package/lib/connect-runtime.js +4 -2
  4. package/lib/local.js +15 -1
  5. package/lib/prime-runtime.js +208 -0
  6. package/lib/setup.js +45 -3
  7. package/package.json +5 -4
  8. package/runtime/codex/.codex/skills/ingest-notion/SKILL.md +29 -0
  9. package/runtime/codex/.codex/skills/notion-connect/SKILL.md +29 -0
  10. package/runtime/codex/.codex/skills/teams-connect/SKILL.md +29 -0
  11. package/runtime/codex/.codex/spec-manifest.json +3 -3
  12. package/runtime/codex/AGENTS.md +11 -4
  13. package/runtime/codex/bin/activity-data.sh +4 -8
  14. package/runtime/codex/bin/attendant.sh +13 -0
  15. package/runtime/codex/bin/autosave.sh +27 -1
  16. package/runtime/codex/bin/codex-render-spec.mjs +6 -0
  17. package/runtime/codex/bin/codex-session-start.sh +8 -1
  18. package/runtime/codex/bin/connect-refresh.sh +33 -0
  19. package/runtime/codex/bin/graph-batch.sh +25 -6
  20. package/runtime/codex/bin/graph-op.sh +5 -4
  21. package/runtime/codex/bin/graph.sh +31 -5
  22. package/runtime/codex/bin/ingest.py +81 -5
  23. package/runtime/codex/bin/lib/context.sh +26 -1
  24. package/runtime/codex/bin/lib/greeting.sh +4 -1
  25. package/runtime/codex/bin/lib/worktree-links.sh +25 -0
  26. package/runtime/codex/manifest.json +8 -1
  27. package/runtime/pi/.codex/skills/ingest-notion/SKILL.md +29 -0
  28. package/runtime/pi/.codex/skills/notion-connect/SKILL.md +29 -0
  29. package/runtime/pi/.codex/skills/teams-connect/SKILL.md +29 -0
  30. package/runtime/pi/.codex/spec-manifest.json +3 -3
  31. package/runtime/pi/.pi/APPEND_SYSTEM.md +3 -2
  32. package/runtime/pi/.pi/extensions/egregore.ts +17 -6
  33. package/runtime/pi/.pi/spec-manifest.json +2 -2
  34. package/runtime/pi/AGENTS.md +11 -4
  35. package/runtime/pi/bin/activity-data.sh +4 -8
  36. package/runtime/pi/bin/attendant.sh +13 -0
  37. package/runtime/pi/bin/autosave.sh +27 -1
  38. package/runtime/pi/bin/codex-render-spec.mjs +6 -0
  39. package/runtime/pi/bin/codex-session-start.sh +8 -1
  40. package/runtime/pi/bin/connect-refresh.sh +33 -0
  41. package/runtime/pi/bin/graph-batch.sh +25 -6
  42. package/runtime/pi/bin/graph-op.sh +5 -4
  43. package/runtime/pi/bin/graph.sh +31 -5
  44. package/runtime/pi/bin/ingest.py +81 -5
  45. package/runtime/pi/bin/lib/context.sh +26 -1
  46. package/runtime/pi/bin/lib/greeting.sh +4 -1
  47. package/runtime/pi/bin/lib/worktree-links.sh +25 -0
  48. package/runtime/pi/bin/pi-branch-policy.mjs +138 -1
  49. package/runtime/pi/bin/pi-product-workflows.mjs +35 -7
  50. package/runtime/pi/manifest.json +8 -1
  51. package/runtime/prime/.claude/context/notification-consent.md +66 -0
  52. package/runtime/prime/.codex/config.toml +17 -0
  53. package/runtime/prime/.codex/hooks/branch-guard.js +461 -0
  54. package/runtime/prime/.codex/hooks/search-hint.js +67 -0
  55. package/runtime/prime/.codex/hooks.json +30 -0
  56. package/runtime/prime/.codex/skills/activity/SKILL.md +82 -0
  57. package/runtime/prime/.codex/skills/announce/SKILL.md +67 -0
  58. package/runtime/prime/.codex/skills/archive/SKILL.md +45 -0
  59. package/runtime/prime/.codex/skills/branch/SKILL.md +29 -0
  60. package/runtime/prime/.codex/skills/checkup/SKILL.md +45 -0
  61. package/runtime/prime/.codex/skills/commit/SKILL.md +29 -0
  62. package/runtime/prime/.codex/skills/contribute/SKILL.md +29 -0
  63. package/runtime/prime/.codex/skills/create-skill/SKILL.md +29 -0
  64. package/runtime/prime/.codex/skills/dark-mode/SKILL.md +29 -0
  65. package/runtime/prime/.codex/skills/dashboard/SKILL.md +59 -0
  66. package/runtime/prime/.codex/skills/deep-reflect/SKILL.md +109 -0
  67. package/runtime/prime/.codex/skills/delete-user/SKILL.md +29 -0
  68. package/runtime/prime/.codex/skills/emissary/SKILL.md +45 -0
  69. package/runtime/prime/.codex/skills/handoff/SKILL.md +166 -0
  70. package/runtime/prime/.codex/skills/harvest/QUESTION_PALETTE.md +76 -0
  71. package/runtime/prime/.codex/skills/harvest/SKILL.md +131 -0
  72. package/runtime/prime/.codex/skills/ingest/SKILL.md +29 -0
  73. package/runtime/prime/.codex/skills/ingest-notion/SKILL.md +29 -0
  74. package/runtime/prime/.codex/skills/ingest-user-interview/SKILL.md +45 -0
  75. package/runtime/prime/.codex/skills/invite/SKILL.md +44 -0
  76. package/runtime/prime/.codex/skills/issue/SKILL.md +45 -0
  77. package/runtime/prime/.codex/skills/me/SKILL.md +29 -0
  78. package/runtime/prime/.codex/skills/meeting/SKILL.md +45 -0
  79. package/runtime/prime/.codex/skills/note/SKILL.md +29 -0
  80. package/runtime/prime/.codex/skills/notion-connect/SKILL.md +29 -0
  81. package/runtime/prime/.codex/skills/onboarding/SKILL.md +45 -0
  82. package/runtime/prime/.codex/skills/pr/SKILL.md +29 -0
  83. package/runtime/prime/.codex/skills/pull/SKILL.md +29 -0
  84. package/runtime/prime/.codex/skills/push/SKILL.md +29 -0
  85. package/runtime/prime/.codex/skills/quest/SKILL.md +80 -0
  86. package/runtime/prime/.codex/skills/reflect/SKILL.md +45 -0
  87. package/runtime/prime/.codex/skills/review-pr/SKILL.md +45 -0
  88. package/runtime/prime/.codex/skills/save/SKILL.md +125 -0
  89. package/runtime/prime/.codex/skills/slack-connect/SKILL.md +29 -0
  90. package/runtime/prime/.codex/skills/summon/SKILL.md +45 -0
  91. package/runtime/prime/.codex/skills/sync-repos/SKILL.md +29 -0
  92. package/runtime/prime/.codex/skills/teams-connect/SKILL.md +29 -0
  93. package/runtime/prime/.codex/skills/telegram-connect/SKILL.md +29 -0
  94. package/runtime/prime/.codex/skills/telemetry/SKILL.md +29 -0
  95. package/runtime/prime/.codex/skills/test/SKILL.md +45 -0
  96. package/runtime/prime/.codex/skills/the-spiral/SKILL.md +84 -0
  97. package/runtime/prime/.codex/skills/tui-design/SKILL.md +45 -0
  98. package/runtime/prime/.codex/skills/tutorial/SKILL.md +45 -0
  99. package/runtime/prime/.codex/skills/update/SKILL.md +29 -0
  100. package/runtime/prime/.codex/skills/view/SKILL.md +156 -0
  101. package/runtime/prime/.codex/skills/view/agents/openai.yaml +4 -0
  102. package/runtime/prime/.codex/skills/view/assets/compose-scaffold.html +221 -0
  103. package/runtime/prime/.codex/skills/visual-explain/SKILL.md +45 -0
  104. package/runtime/prime/.codex/skills/wrap/SKILL.md +58 -0
  105. package/runtime/prime/.codex/spec-manifest.json +132 -0
  106. package/runtime/prime/.prime/agent/APPEND_SYSTEM.md +393 -0
  107. package/runtime/prime/.prime/agent/extensions/egregore.ts +460 -0
  108. package/runtime/prime/.prime/agent/settings.json +4 -0
  109. package/runtime/prime/.prime/agent/skills/egregore/SKILL.md +34 -0
  110. package/runtime/prime/.prime/agent/skills/egregore/pyproject.toml +13 -0
  111. package/runtime/prime/.prime/agent/skills/egregore/src/egregore/__init__.py +156 -0
  112. package/runtime/prime/.prime/agent/spec-manifest.json +12 -0
  113. package/runtime/prime/AGENTS.md +489 -0
  114. package/runtime/prime/bin/activity-data.sh +238 -0
  115. package/runtime/prime/bin/agent.sh +775 -0
  116. package/runtime/prime/bin/attendant.sh +707 -0
  117. package/runtime/prime/bin/autosave.sh +112 -0
  118. package/runtime/prime/bin/capture-reconcile.sh +69 -0
  119. package/runtime/prime/bin/capture-run.sh +341 -0
  120. package/runtime/prime/bin/codex-install-skills.sh +78 -0
  121. package/runtime/prime/bin/codex-render-spec.mjs +440 -0
  122. package/runtime/prime/bin/codex-session-start.sh +265 -0
  123. package/runtime/prime/bin/codex-skill-render.mjs +347 -0
  124. package/runtime/prime/bin/codex-sync-skills.sh +232 -0
  125. package/runtime/prime/bin/connect-refresh.sh +33 -0
  126. package/runtime/prime/bin/dashboard-data.sh +178 -0
  127. package/runtime/prime/bin/graph-batch.sh +79 -0
  128. package/runtime/prime/bin/graph-op.sh +1102 -0
  129. package/runtime/prime/bin/graph-wal.sh +229 -0
  130. package/runtime/prime/bin/graph.sh +223 -0
  131. package/runtime/prime/bin/handoff-pr-backfill.sh +86 -0
  132. package/runtime/prime/bin/handoff-run.sh +671 -0
  133. package/runtime/prime/bin/handoff-save-egregore.sh +212 -0
  134. package/runtime/prime/bin/index-handoff.sh +322 -0
  135. package/runtime/prime/bin/ingest-graph.sh +80 -0
  136. package/runtime/prime/bin/ingest-surface.html +495 -0
  137. package/runtime/prime/bin/ingest.py +756 -0
  138. package/runtime/prime/bin/ingest.sh +279 -0
  139. package/runtime/prime/bin/ingest_graph.py +856 -0
  140. package/runtime/prime/bin/ingest_surface.py +544 -0
  141. package/runtime/prime/bin/lib/artifact-id.sh +46 -0
  142. package/runtime/prime/bin/lib/config.sh +116 -0
  143. package/runtime/prime/bin/lib/context.sh +621 -0
  144. package/runtime/prime/bin/lib/dashboard-artifact.sh +242 -0
  145. package/runtime/prime/bin/lib/ensure-develop.sh +183 -0
  146. package/runtime/prime/bin/lib/git-sync.sh +406 -0
  147. package/runtime/prime/bin/lib/greeting.sh +519 -0
  148. package/runtime/prime/bin/lib/hash.sh +13 -0
  149. package/runtime/prime/bin/lib/identity.sh +213 -0
  150. package/runtime/prime/bin/lib/metrics.sh +374 -0
  151. package/runtime/prime/bin/lib/noncode.sh +31 -0
  152. package/runtime/prime/bin/lib/permissions.sh +106 -0
  153. package/runtime/prime/bin/lib/time.sh +56 -0
  154. package/runtime/prime/bin/lib/worktree-links.sh +79 -0
  155. package/runtime/prime/bin/notify.sh +636 -0
  156. package/runtime/prime/bin/person.py +630 -0
  157. package/runtime/prime/bin/person.sh +448 -0
  158. package/runtime/prime/bin/pi-branch-policy.mjs +312 -0
  159. package/runtime/prime/bin/pi-product-workflows.mjs +394 -0
  160. package/runtime/prime/bin/prime-render-spec.mjs +123 -0
  161. package/runtime/prime/bin/prime-session-start.sh +24 -0
  162. package/runtime/prime/bin/publish-artifact.sh +271 -0
  163. package/runtime/prime/bin/publish-references.sh +144 -0
  164. package/runtime/prime/bin/repo-state.sh +96 -0
  165. package/runtime/prime/bin/search.sh +240 -0
  166. package/runtime/prime/bin/session-autosave.sh +209 -0
  167. package/runtime/prime/bin/session-log.sh +199 -0
  168. package/runtime/prime/bin/settings.sh +304 -0
  169. package/runtime/prime/bin/telemetry.sh +391 -0
  170. package/runtime/prime/bin/worktree-create.sh +128 -0
  171. package/runtime/prime/bin/worktree-remove.sh +50 -0
  172. package/runtime/prime/bin/worktree.sh +198 -0
  173. package/runtime/prime/manifest.json +178 -0
@@ -0,0 +1,304 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # settings.sh — deterministic config verbs for an Egregore instance.
5
+ #
6
+ # Edits egregore.json atomically. No agent, no network, no session. Idempotent:
7
+ # re-adding an existing item is a no-op success; removing a missing one is too.
8
+ # This is the deterministic core the launcher settings screen (and slash
9
+ # commands) call — the reliability lives here, the surfaces are thin callers.
10
+ #
11
+ # Usage:
12
+ # settings.sh hosting status|on|off
13
+ # settings.sh relay status|on|off
14
+ # settings.sh repo list | add <name> [description] | remove <name>
15
+ # settings.sh admin list | add <github-handle> | remove <github-handle>
16
+ # settings.sh dump # full settings snapshot as JSON (for the launcher)
17
+ #
18
+ # Add --json to a read verb (status/list) for machine-readable output.
19
+ #
20
+ # Exit codes: 0 ok · 1 usage/validation error · 2 config not found.
21
+
22
+ SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
23
+ CONFIG="$SCRIPT_DIR/egregore.json"
24
+
25
+ # ── helpers ──────────────────────────────────────────────────────────────
26
+
27
+ _die() { echo "$1" >&2; exit "${2:-1}"; }
28
+
29
+ _valid_token() {
30
+ # non-empty, GitHub-handle / repo-name safe charset
31
+ [ -n "${1:-}" ] && printf '%s' "$1" | grep -qE '^[A-Za-z0-9._-]+$'
32
+ }
33
+
34
+ _save() {
35
+ # _save '<jq-filter>' [jq-args...] — apply filter to CONFIG, write atomically.
36
+ local filter="$1"; shift
37
+ local tmp
38
+ tmp="$(mktemp "$CONFIG.XXXXXX")" || _die "cannot create temp file" 1
39
+ if jq "$@" "$filter" "$CONFIG" > "$tmp" 2>/dev/null && [ -s "$tmp" ]; then
40
+ mv "$tmp" "$CONFIG"
41
+ else
42
+ rm -f "$tmp"
43
+ _die "failed to update $CONFIG (invalid jq filter or unwritable file)" 1
44
+ fi
45
+ }
46
+
47
+ _has_repo() { jq -e --arg n "$1" 'any(.repos[]?; (if type=="object" then .name else . end) == $n)' "$CONFIG" >/dev/null; }
48
+ _has_admin() { jq -e --arg h "$1" 'any(.admins[]?; . == $h)' "$CONFIG" >/dev/null; }
49
+
50
+ usage() {
51
+ cat >&2 <<'EOF'
52
+ settings.sh — Egregore instance settings
53
+
54
+ settings.sh hosting status|on|off
55
+ settings.sh relay status|on|off
56
+ settings.sh repo list | add <name> [description] | remove <name>
57
+ settings.sh admin list | add <github-handle> | remove <github-handle>
58
+ settings.sh people list | add <github-handle> | remove <github-handle>
59
+ settings.sh dump full snapshot (JSON)
60
+
61
+ --json machine-readable output for status/list
62
+ EOF
63
+ exit "${1:-1}"
64
+ }
65
+
66
+ # ── verbs ────────────────────────────────────────────────────────────────
67
+
68
+ cmd_hosting() {
69
+ local action="${1:-status}"
70
+ # NEVER use jq's `// "true"` here: `//` treats an explicit `false` as empty and
71
+ # would return the default, silently defeating the off-switch. Test `!= false`
72
+ # so absent/true => enabled, explicit false => disabled. `enabled` is the
73
+ # resolved boolean string ("true"/"false").
74
+ local enabled; enabled="$(jq -r '.features.publishing != false' "$CONFIG" 2>/dev/null || echo "true")"
75
+ case "$action" in
76
+ status)
77
+ if [ "$JSON" = 1 ]; then
78
+ jq -n --argjson on "$enabled" '{domain:"hosting", publishing:$on}'
79
+ elif [ "$enabled" = "false" ]; then echo "Hosting (egregore.xyz): OFF"
80
+ else echo "Hosting (egregore.xyz): ON"; fi ;;
81
+ on)
82
+ _save '.features.publishing = true'
83
+ echo "Hosting (egregore.xyz): ON" ;;
84
+ off)
85
+ _save '.features.publishing = false'
86
+ echo "Hosting (egregore.xyz): OFF — artifacts will no longer upload to egregore.xyz" ;;
87
+ *) _die "usage: settings.sh hosting status|on|off" 1 ;;
88
+ esac
89
+ }
90
+
91
+ # The public share relay is the only upload route when there is no org API
92
+ # key. It is unauthenticated, readable by anyone with the URL, and expires
93
+ # after 7 days, so it is OFF unless explicitly enabled.
94
+ cmd_relay() {
95
+ local action="${1:-status}"
96
+ local enabled; enabled="$(jq -r 'if .features.public_relay == true then "true" else "false" end' "$CONFIG" 2>/dev/null || echo "false")"
97
+ case "$action" in
98
+ status)
99
+ if [ "$JSON" = 1 ]; then
100
+ jq -n --argjson on "$enabled" '{domain:"relay", public_relay:$on}'
101
+ elif [ "$enabled" = "true" ]; then
102
+ echo "Public share relay: ON — artifacts published without an org API key upload to a public, unauthenticated URL (expires after 7 days)"
103
+ else
104
+ echo "Public share relay: OFF — without an org API key, nothing uploads anywhere"
105
+ fi ;;
106
+ on)
107
+ _save '.features.public_relay = true'
108
+ echo "Public share relay: ON"
109
+ echo " Artifacts published without an org API key now upload to a public, unauthenticated URL that anyone with the link can read for 7 days." ;;
110
+ off)
111
+ _save '.features.public_relay = false'
112
+ echo "Public share relay: OFF — without an org API key, nothing uploads anywhere" ;;
113
+ *) _die "usage: settings.sh relay status|on|off" 1 ;;
114
+ esac
115
+ }
116
+
117
+ cmd_repo() {
118
+ local action="${1:-list}"; shift || true
119
+ case "$action" in
120
+ list)
121
+ if [ "$JSON" = 1 ]; then
122
+ jq '[.repos[]? | if type=="object" then . else {name:.} end]' "$CONFIG"
123
+ else
124
+ jq -r '.repos[]? | if type=="object" then " " + .name + (if .description then " — " + .description else "" end) else " " + . end' "$CONFIG"
125
+ fi ;;
126
+ add)
127
+ local name="${1:-}"; shift || true
128
+ local desc="${*:-}"
129
+ _valid_token "$name" || _die "usage: settings.sh repo add <name> [description]" 1
130
+ if _has_repo "$name"; then echo "repo '$name' already managed"; return 0; fi
131
+ if [ -n "$desc" ]; then
132
+ _save '.repos = ((.repos // []) + [{name:$n, description:$d}])' --arg n "$name" --arg d "$desc"
133
+ else
134
+ _save '.repos = ((.repos // []) + [{name:$n}])' --arg n "$name"
135
+ fi
136
+ echo "added repo '$name'"
137
+ echo " clone it with: /sync-repos (or reopen the egregore)" ;;
138
+ remove)
139
+ local name="${1:-}"
140
+ _valid_token "$name" || _die "usage: settings.sh repo remove <name>" 1
141
+ if ! _has_repo "$name"; then echo "repo '$name' is not managed"; return 0; fi
142
+ _save '.repos = [.repos[]? | select((if type=="object" then .name else . end) != $n)]' --arg n "$name"
143
+ echo "removed repo '$name' from config"
144
+ echo " note: the local ../$name checkout is left untouched" ;;
145
+ *) _die "usage: settings.sh repo list|add|remove" 1 ;;
146
+ esac
147
+ }
148
+
149
+ cmd_admin() {
150
+ local action="${1:-list}"; shift || true
151
+ case "$action" in
152
+ list)
153
+ if [ "$JSON" = 1 ]; then jq '{admins:(.admins // [])}' "$CONFIG"
154
+ else jq -r '.admins[]? | " " + .' "$CONFIG"; fi ;;
155
+ add)
156
+ local h="${1:-}"
157
+ _valid_token "$h" || _die "usage: settings.sh admin add <github-handle>" 1
158
+ if _has_admin "$h"; then echo "'$h' is already an admin"; return 0; fi
159
+ _save '.admins = ((.admins // []) + [$h])' --arg h "$h"
160
+ echo "added admin '$h'" ;;
161
+ remove)
162
+ local h="${1:-}"
163
+ _valid_token "$h" || _die "usage: settings.sh admin remove <github-handle>" 1
164
+ if ! _has_admin "$h"; then echo "'$h' is not an admin"; return 0; fi
165
+ local count; count="$(jq '(.admins // []) | length' "$CONFIG")"
166
+ [ "$count" -le 1 ] && _die "refusing to remove the last admin ('$h') — add another admin first" 1
167
+ _save '.admins = [.admins[]? | select(. != $h)]' --arg h "$h"
168
+ echo "removed admin '$h'" ;;
169
+ *) _die "usage: settings.sh admin list|add|remove" 1 ;;
170
+ esac
171
+ }
172
+
173
+ # ── people ───────────────────────────────────────────────────────────────
174
+ # People live as files in memory/people/. add/remove manage that directory AND
175
+ # GitHub repo access (the meaningful local grant). Connected-mode extras — the
176
+ # invite link + Telegram (/invite) and Supabase/Neo4j teardown (/delete-user) —
177
+ # are pointed to, not duplicated here.
178
+
179
+ _people_dir() { echo "$SCRIPT_DIR/memory/people"; }
180
+
181
+ _people_names() {
182
+ local pdir; pdir="$(_people_dir)"
183
+ [ -d "$pdir" ] || return 0
184
+ ls "$pdir"/*.md 2>/dev/null | while IFS= read -r f; do basename "$f" .md; done \
185
+ | grep -viE '^(index|readme)$' || true
186
+ }
187
+
188
+ _people_json() {
189
+ local names; names="$(_people_names)"
190
+ if [ -z "$names" ]; then echo '[]'; return; fi
191
+ printf '%s\n' "$names" | jq -R . | jq -sc 'map(select(length>0))'
192
+ }
193
+
194
+ _has_person() { [ -f "$(_people_dir)/$1.md" ]; }
195
+
196
+ # add|remove a GitHub push-collaborator across the instance's repos. Best-effort:
197
+ # no token/org => skip with a note; per-repo failures never abort.
198
+ _people_github() {
199
+ local user="$1" op="$2" token org repos r
200
+ token="$(grep '^GITHUB_TOKEN=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d= -f2- || true)"
201
+ org="$(jq -r '.github_org // empty' "$CONFIG" 2>/dev/null)"
202
+ if [ -z "$token" ] || [ -z "$org" ]; then
203
+ echo " (no GitHub token/org — skipped repo access; run bin/github-auth.sh)" >&2
204
+ return 0
205
+ fi
206
+ repos="$(jq -r '[.repo_name, (.memory_repo // "" | split("/") | last | sub("\\.git$";"")), (.repos[]? | if type=="object" then .name else . end)] | map(select(. != null and . != "")) | unique | .[]' "$CONFIG" 2>/dev/null)"
207
+ for r in $repos; do
208
+ if [ "$op" = "add" ]; then
209
+ curl -s -o /dev/null -X PUT -H "Authorization: Bearer $token" -H "Accept: application/vnd.github+json" \
210
+ "https://api.github.com/repos/$org/$r/collaborators/$user" -d '{"permission":"push"}' 2>/dev/null || true
211
+ else
212
+ curl -s -o /dev/null -X DELETE -H "Authorization: Bearer $token" -H "Accept: application/vnd.github+json" \
213
+ "https://api.github.com/repos/$org/$r/collaborators/$user" 2>/dev/null || true
214
+ fi
215
+ done
216
+ }
217
+
218
+ # create|remove memory/people/<user>.md, committing ONLY the people dir.
219
+ _people_file() {
220
+ local user="$1" op="$2" pdir f inviter today
221
+ pdir="$(_people_dir)"
222
+ [ -d "$SCRIPT_DIR/memory" ] || { echo " (no memory dir — skipped person file)" >&2; return 0; }
223
+ mkdir -p "$pdir"
224
+ f="$pdir/$user.md"
225
+ if [ "$op" = "add" ]; then
226
+ if [ ! -f "$f" ]; then
227
+ inviter="$(jq -r '.display_name // .github_username // "admin"' "$SCRIPT_DIR/.egregore-state.json" 2>/dev/null || echo admin)"
228
+ today="$(date -u +%Y-%m-%d)"
229
+ printf -- '---\nname: %s\nperson_id: github-login:%s\ngithub: %s\ninvited_by: %s\njoined: %s\n---\n' "$user" "$(printf '%s' "$user" | tr '[:upper:]' '[:lower:]')" "$user" "$inviter" "$today" > "$f"
230
+ fi
231
+ else
232
+ rm -f "$f"
233
+ fi
234
+ ( cd "$SCRIPT_DIR/memory" 2>/dev/null && git add -A people/ 2>/dev/null \
235
+ && git commit -q -m "settings: people $op $user" 2>/dev/null \
236
+ && git push -q 2>/dev/null ) || true
237
+ }
238
+
239
+ cmd_people() {
240
+ local action="${1:-list}"; shift || true
241
+ case "$action" in
242
+ list)
243
+ if [ "$JSON" = 1 ]; then _people_json
244
+ else
245
+ local names; names="$(_people_names)"
246
+ if [ -n "$names" ]; then printf ' %s\n' $names; else echo " (none)"; fi
247
+ fi ;;
248
+ add)
249
+ local h="${1:-}"
250
+ _valid_token "$h" || _die "usage: settings.sh people add <github-handle>" 1
251
+ if _has_person "$h"; then echo "'$h' is already in people"; return 0; fi
252
+ _people_github "$h" "add"
253
+ _people_file "$h" "add"
254
+ echo "added '$h' (repo access + person file)"
255
+ [ -n "$(jq -r '.api_url // empty' "$CONFIG" 2>/dev/null)" ] && echo " connected org: for the invite link + Telegram, run /invite $h" ;;
256
+ remove)
257
+ local h="${1:-}"
258
+ _valid_token "$h" || _die "usage: settings.sh people remove <github-handle>" 1
259
+ if ! _has_person "$h"; then echo "'$h' is not in people"; return 0; fi
260
+ _people_github "$h" "remove"
261
+ _people_file "$h" "remove"
262
+ echo "removed '$h' (repo access + person file)"
263
+ [ -n "$(jq -r '.api_url // empty' "$CONFIG" 2>/dev/null)" ] && echo " connected org: for full deprovision (Supabase/Neo4j), run /delete-user $h" ;;
264
+ *) _die "usage: settings.sh people list|add|remove" 1 ;;
265
+ esac
266
+ }
267
+
268
+ cmd_dump() {
269
+ # Always JSON — the launcher calls this once to render the whole settings screen.
270
+ local people; people="$(_people_json)"
271
+ jq --argjson people "$people" '{
272
+ org: .org_name,
273
+ slug: .slug,
274
+ hosting: (.features.publishing != false),
275
+ public_relay: (.features.public_relay == true),
276
+ repos: [.repos[]? | if type=="object" then . else {name:.} end],
277
+ admins: (.admins // []),
278
+ people: $people
279
+ }' "$CONFIG"
280
+ }
281
+
282
+ # ── dispatch ─────────────────────────────────────────────────────────────
283
+
284
+ [ -f "$CONFIG" ] || _die "egregore.json not found at $CONFIG" 2
285
+
286
+ # Pull out --json (bash 3.2-safe array handling), leave the rest as positionals.
287
+ JSON=0
288
+ _args=()
289
+ for _a in "$@"; do
290
+ if [ "$_a" = "--json" ]; then JSON=1; else _args+=("$_a"); fi
291
+ done
292
+ set -- "${_args[@]+"${_args[@]}"}"
293
+
294
+ DOMAIN="${1:-}"; shift || true
295
+ case "$DOMAIN" in
296
+ hosting) cmd_hosting "$@" ;;
297
+ relay) cmd_relay "$@" ;;
298
+ repo) cmd_repo "$@" ;;
299
+ admin) cmd_admin "$@" ;;
300
+ people) cmd_people "$@" ;;
301
+ dump) cmd_dump ;;
302
+ ""|-h|--help|help) usage 0 ;;
303
+ *) _die "unknown settings domain: '$DOMAIN' (try: hosting, relay, repo, admin, people, dump)" 1 ;;
304
+ esac
@@ -0,0 +1,391 @@
1
+ #!/usr/bin/env bash
2
+ # Egregore telemetry — privacy-respecting product analytics.
3
+ # Mirrors bin/graph.sh and bin/notify.sh patterns.
4
+ #
5
+ # Subcommands:
6
+ # emit <type> <json> Append event to local JSONL buffer (no network)
7
+ # flush Send buffered events to API and clear buffer
8
+ # status Show telemetry status (enabled/disabled, buffer size)
9
+ # enable Enable telemetry
10
+ # disable Disable telemetry
11
+ # clear Clear local buffer
12
+ # show [n] Show last N events from buffer (default 10)
13
+ #
14
+ # Consent: opt-out via EGREGORE_NO_TELEMETRY=1, DO_NOT_TRACK=1,
15
+ # or .egregore-state.json → "telemetry": false
16
+ #
17
+ # Never collected: file paths, file contents, code, env var values,
18
+ # conversation content, command arguments with user content.
19
+ # Only collected: command names, timestamps, durations, error codes, branch names.
20
+ set -euo pipefail
21
+
22
+ SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
23
+ BUFFER_DIR="$HOME/.egregore"
24
+ BUFFER_FILE="$BUFFER_DIR/telemetry.jsonl"
25
+ STATE_FILE="$SCRIPT_DIR/.egregore-state.json"
26
+ CONFIG="$SCRIPT_DIR/egregore.json"
27
+ ENV_FILE="$SCRIPT_DIR/.env"
28
+ MAX_BUFFER_BYTES=1048576 # 1MB
29
+ MAX_EVENTS_AFTER_TRUNCATE=500
30
+ LOCK_DIR="$BUFFER_DIR/telemetry.lock"
31
+ # Public relay for local/OSS egregores without API keys
32
+ TELEMETRY_RELAY_URL="https://egregore-production-55f2.up.railway.app"
33
+
34
+ # --- Cross-platform file locking via mkdir (atomic on all POSIX systems) ---
35
+ _lock() {
36
+ local attempts=0
37
+ while ! mkdir "$LOCK_DIR" 2>/dev/null; do
38
+ attempts=$((attempts + 1))
39
+ if [ "$attempts" -gt 50 ]; then
40
+ local holder_pid
41
+ holder_pid=$(cat "$LOCK_DIR/pid" 2>/dev/null || echo "")
42
+ if [ -n "$holder_pid" ] && ! kill -0 "$holder_pid" 2>/dev/null; then
43
+ rm -rf "$LOCK_DIR" 2>/dev/null
44
+ continue
45
+ fi
46
+ return 1
47
+ fi
48
+ sleep 0.1
49
+ done
50
+ echo $$ > "$LOCK_DIR/pid" 2>/dev/null
51
+ }
52
+
53
+ _unlock() {
54
+ rm -rf "$LOCK_DIR" 2>/dev/null
55
+ }
56
+
57
+ trap '_unlock' EXIT
58
+
59
+ # --- Consent check (fast path) ---
60
+ _check_consent() {
61
+ # Environment variable opt-outs (fastest check)
62
+ [ "${EGREGORE_NO_TELEMETRY:-}" = "1" ] && return 1
63
+ [ "${DO_NOT_TRACK:-}" = "1" ] && return 1
64
+
65
+ # State file opt-out
66
+ if [ -f "$STATE_FILE" ]; then
67
+ local val
68
+ val=$(jq -r '.telemetry // "true"' "$STATE_FILE" 2>/dev/null || echo "true")
69
+ [ "$val" = "false" ] && return 1
70
+ fi
71
+
72
+ return 0
73
+ }
74
+
75
+ # --- Debug mode ---
76
+ _is_debug() {
77
+ [ "${EGREGORE_TELEMETRY_DEBUG:-}" = "1" ]
78
+ }
79
+
80
+ # --- Resolve identity (from env vars first, fallback to config files) ---
81
+ _resolve_user() {
82
+ if [ -n "${EGREGORE_USER:-}" ]; then
83
+ echo "$EGREGORE_USER"
84
+ elif [ -f "$STATE_FILE" ]; then
85
+ jq -r '.github_username // .name // "unknown"' "$STATE_FILE" 2>/dev/null || echo "unknown"
86
+ else
87
+ echo "unknown"
88
+ fi
89
+ }
90
+
91
+ _resolve_org() {
92
+ if [ -n "${EGREGORE_ORG:-}" ]; then
93
+ echo "$EGREGORE_ORG"
94
+ elif [ -f "$CONFIG" ]; then
95
+ jq -r '.slug // .github_org // "unknown"' "$CONFIG" 2>/dev/null || echo "unknown"
96
+ else
97
+ echo "unknown"
98
+ fi
99
+ }
100
+
101
+ _resolve_proj_hash() {
102
+ # Use git common dir so worktrees hash to the same path as the main checkout
103
+ local repo_root
104
+ repo_root=$(git -C "$SCRIPT_DIR" rev-parse --path-format=absolute --git-common-dir 2>/dev/null | sed 's|/\.git$||') || repo_root="$SCRIPT_DIR"
105
+ echo -n "$repo_root" | md5 2>/dev/null || echo -n "$repo_root" | md5sum 2>/dev/null | cut -d' ' -f1
106
+ }
107
+
108
+ _resolve_session_id() {
109
+ if [ -n "${EGREGORE_SESSION_ID:-}" ]; then
110
+ echo "$EGREGORE_SESSION_ID"
111
+ return
112
+ fi
113
+
114
+ # Fallback: read from file written by session-start.sh
115
+ # (env vars from hooks don't propagate into the Claude Code agent)
116
+ local proj_hash
117
+ proj_hash=$(_resolve_proj_hash)
118
+ local sid_file="$HOME/.egregore/session-${proj_hash}.id"
119
+ if [ -f "$sid_file" ]; then
120
+ cat "$sid_file" 2>/dev/null || echo "unknown"
121
+ else
122
+ echo "unknown"
123
+ fi
124
+ }
125
+
126
+ _resolve_model() {
127
+ local proj_hash
128
+ proj_hash=$(_resolve_proj_hash)
129
+ local model_file="$HOME/.egregore/session-model-${proj_hash}"
130
+ if [ ! -s "$model_file" ]; then
131
+ return 0
132
+ fi
133
+
134
+ local model=""
135
+ IFS= read -r model < "$model_file" || true
136
+ if [ -n "$model" ]; then
137
+ printf '%s\n' "$model"
138
+ fi
139
+ }
140
+
141
+ _stamp_model_if_absent() {
142
+ local compact_data="$1"
143
+ local has_model=""
144
+ has_model=$(printf '%s' "$compact_data" | jq -e 'has("model")' 2>/dev/null || true)
145
+ if [ "$has_model" != "false" ]; then
146
+ printf '%s' "$compact_data"
147
+ return 0
148
+ fi
149
+
150
+ local model=""
151
+ model=$(_resolve_model 2>/dev/null || true)
152
+ if [ -z "$model" ]; then
153
+ printf '%s' "$compact_data"
154
+ return 0
155
+ fi
156
+
157
+ local stamped_data=""
158
+ if stamped_data=$(printf '%s' "$compact_data" | jq -c --arg m "$model" 'if has("model") then . else . + {model:$m} end' 2>/dev/null); then
159
+ printf '%s' "$stamped_data"
160
+ else
161
+ printf '%s' "$compact_data"
162
+ fi
163
+ }
164
+
165
+ # --- emit: append event to local JSONL buffer ---
166
+ cmd_emit() {
167
+ local event_type="${1:?Usage: telemetry.sh emit <type> <json>}"
168
+ local data="${2:-"{}"}"
169
+
170
+ # Consent gate
171
+ if ! _check_consent; then
172
+ return 0
173
+ fi
174
+
175
+ local ts
176
+ ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
177
+ # Compact data to single line — callers may pass pretty-printed JSON
178
+ local compact_data
179
+ compact_data=$(printf '%s' "$data" | tr -d '\n' | tr -s ' ')
180
+ compact_data=$(_stamp_model_if_absent "$compact_data")
181
+
182
+ # Debug mode: print to stderr instead of writing
183
+ if _is_debug; then
184
+ printf '{"ts":"%s","type":"%s","sid":"%s","org":"%s","user":"%s","data":%s}\n' \
185
+ "$ts" "$event_type" "$(_resolve_session_id)" "$(_resolve_org)" "$(_resolve_user)" "$compact_data" >&2
186
+ return 0
187
+ fi
188
+
189
+ # Ensure buffer directory exists
190
+ mkdir -p "$BUFFER_DIR"
191
+
192
+ # Build event line (outside lock for minimal hold time)
193
+ local line
194
+ line=$(printf '{"ts":"%s","type":"%s","sid":"%s","org":"%s","user":"%s","data":%s}' \
195
+ "$ts" "$event_type" "$(_resolve_session_id)" "$(_resolve_org)" "$(_resolve_user)" "$compact_data")
196
+
197
+ # Append under lock
198
+ _lock || { echo "$line" >> "$BUFFER_FILE" 2>/dev/null; return 0; }
199
+ echo "$line" >> "$BUFFER_FILE"
200
+
201
+ # Buffer guard: if file exceeds max size, truncate to last N events
202
+ if [ -f "$BUFFER_FILE" ]; then
203
+ local size
204
+ size=$(wc -c < "$BUFFER_FILE" 2>/dev/null | tr -d ' ')
205
+ if [ "$size" -gt "$MAX_BUFFER_BYTES" ] 2>/dev/null; then
206
+ local tmp="$BUFFER_FILE.truncate.$$"
207
+ tail -n "$MAX_EVENTS_AFTER_TRUNCATE" "$BUFFER_FILE" > "$tmp" 2>/dev/null \
208
+ && mv "$tmp" "$BUFFER_FILE" \
209
+ || rm -f "$tmp"
210
+ fi
211
+ fi
212
+ _unlock
213
+ }
214
+
215
+ # --- flush: send buffered events to API ---
216
+ cmd_flush() {
217
+ if [ ! -f "$BUFFER_FILE" ] || [ ! -s "$BUFFER_FILE" ]; then
218
+ return 0
219
+ fi
220
+
221
+ # Read API config
222
+ local api_url=""
223
+ local api_key=""
224
+ local endpoint=""
225
+ local auth_header=""
226
+ if [ -f "$CONFIG" ]; then
227
+ api_url=$(jq -r '.api_url // empty' "$CONFIG" 2>/dev/null || true)
228
+ fi
229
+ if [ -f "$ENV_FILE" ]; then
230
+ api_key=$(grep '^EGREGORE_API_KEY=' "$ENV_FILE" 2>/dev/null | cut -d'=' -f2- || true)
231
+ fi
232
+
233
+ if [ -n "$api_url" ] && [ -n "$api_key" ]; then
234
+ # Connected mode: authenticated ingest
235
+ endpoint="${api_url}/api/telemetry/ingest"
236
+ auth_header="Authorization: Bearer $api_key"
237
+ else
238
+ # Local/OSS mode: public relay (no auth needed)
239
+ endpoint="${TELEMETRY_RELAY_URL}/api/telemetry/relay"
240
+ auth_header=""
241
+ fi
242
+
243
+ # Atomic rotate under lock: move buffer so new events can keep writing
244
+ _lock || return 0
245
+ local flush_file="$BUFFER_FILE.flush.$$"
246
+ mv "$BUFFER_FILE" "$flush_file" 2>/dev/null || { _unlock; return 0; }
247
+ _unlock
248
+
249
+ # POST to API
250
+ local response
251
+ local http_code
252
+ local curl_args=(-s -o /dev/null -w '%{http_code}' -X POST "$endpoint"
253
+ -H "Content-Type: application/x-ndjson"
254
+ --data-binary "@$flush_file"
255
+ --max-time 15)
256
+ if [ -n "$auth_header" ]; then
257
+ curl_args+=(-H "$auth_header")
258
+ fi
259
+ http_code=$(curl "${curl_args[@]}" 2>/dev/null || echo "000")
260
+
261
+ if [ "$http_code" -ge 200 ] 2>/dev/null && [ "$http_code" -lt 300 ] 2>/dev/null; then
262
+ # Success — remove flush file
263
+ rm -f "$flush_file"
264
+ else
265
+ # Failure — move events back so they aren't lost (under lock to prevent races)
266
+ _lock || { mv "$flush_file" "$BUFFER_FILE" 2>/dev/null || true; return 1; }
267
+ if [ -f "$BUFFER_FILE" ]; then
268
+ # New events were written while we were flushing; prepend old events
269
+ cat "$flush_file" "$BUFFER_FILE" > "$BUFFER_FILE.merge.$$" 2>/dev/null \
270
+ && mv "$BUFFER_FILE.merge.$$" "$BUFFER_FILE" \
271
+ && rm -f "$flush_file" \
272
+ || rm -f "$BUFFER_FILE.merge.$$"
273
+ else
274
+ mv "$flush_file" "$BUFFER_FILE" 2>/dev/null || true
275
+ fi
276
+ _unlock
277
+ fi
278
+ }
279
+
280
+ # --- status: show telemetry status ---
281
+ cmd_status() {
282
+ echo "Telemetry status:"
283
+ echo ""
284
+
285
+ # Consent
286
+ if _check_consent; then
287
+ echo " Enabled: yes"
288
+ else
289
+ echo " Enabled: no"
290
+ if [ "${EGREGORE_NO_TELEMETRY:-}" = "1" ]; then
291
+ echo " Reason: EGREGORE_NO_TELEMETRY=1"
292
+ elif [ "${DO_NOT_TRACK:-}" = "1" ]; then
293
+ echo " Reason: DO_NOT_TRACK=1"
294
+ else
295
+ echo " Reason: telemetry=false in .egregore-state.json"
296
+ fi
297
+ fi
298
+
299
+ # Buffer
300
+ if [ -f "$BUFFER_FILE" ]; then
301
+ local count size
302
+ count=$(grep -c '^{' "$BUFFER_FILE" 2>/dev/null || echo 0)
303
+ size=$(wc -c < "$BUFFER_FILE" 2>/dev/null | tr -d ' ')
304
+ echo " Buffer: $count events ($size bytes)"
305
+ echo " Path: $BUFFER_FILE"
306
+ else
307
+ echo " Buffer: empty"
308
+ fi
309
+
310
+ echo ""
311
+ echo " Opt-out: set EGREGORE_NO_TELEMETRY=1 in .env, or run /telemetry off"
312
+ }
313
+
314
+ # --- enable: turn telemetry on ---
315
+ cmd_enable() {
316
+ if [ -f "$STATE_FILE" ]; then
317
+ jq '.telemetry = true' "$STATE_FILE" > "$STATE_FILE.tmp" \
318
+ && mv "$STATE_FILE.tmp" "$STATE_FILE"
319
+ fi
320
+ echo "Telemetry enabled."
321
+ }
322
+
323
+ # --- disable: turn telemetry off ---
324
+ cmd_disable() {
325
+ if [ -f "$STATE_FILE" ]; then
326
+ jq '.telemetry = false' "$STATE_FILE" > "$STATE_FILE.tmp" \
327
+ && mv "$STATE_FILE.tmp" "$STATE_FILE"
328
+ else
329
+ echo '{"telemetry": false}' > "$STATE_FILE"
330
+ fi
331
+ echo "Telemetry disabled."
332
+ }
333
+
334
+ # --- clear: delete local buffer ---
335
+ cmd_clear() {
336
+ rm -f "$BUFFER_FILE"
337
+ echo "Telemetry buffer cleared."
338
+ }
339
+
340
+ # --- show: display last N events ---
341
+ cmd_show() {
342
+ local n="${1:-10}"
343
+ if [ ! -f "$BUFFER_FILE" ] || [ ! -s "$BUFFER_FILE" ]; then
344
+ echo "No buffered events."
345
+ return 0
346
+ fi
347
+ echo "Last $n events:"
348
+ echo ""
349
+ # Extract JSON objects (handles both single-line and legacy multi-line format)
350
+ grep '^{' "$BUFFER_FILE" | tail -n "$n" | jq -c . 2>/dev/null \
351
+ || grep '^{' "$BUFFER_FILE" | tail -n "$n"
352
+ }
353
+
354
+ # --- Main dispatch ---
355
+ case "${1:-status}" in
356
+ emit)
357
+ shift
358
+ cmd_emit "$@"
359
+ ;;
360
+ flush)
361
+ cmd_flush
362
+ ;;
363
+ status)
364
+ cmd_status
365
+ ;;
366
+ enable)
367
+ cmd_enable
368
+ ;;
369
+ disable)
370
+ cmd_disable
371
+ ;;
372
+ clear)
373
+ cmd_clear
374
+ ;;
375
+ show)
376
+ shift
377
+ cmd_show "${1:-10}"
378
+ ;;
379
+ help|*)
380
+ echo "Usage: telemetry.sh <command>"
381
+ echo ""
382
+ echo "Commands:"
383
+ echo " emit <type> <json> Append event to local buffer (no network)"
384
+ echo " flush Send buffered events to API"
385
+ echo " status Show telemetry status"
386
+ echo " enable Enable telemetry"
387
+ echo " disable Disable telemetry"
388
+ echo " clear Clear local buffer"
389
+ echo " show [n] Show last N events (default 10)"
390
+ ;;
391
+ esac