create-egregore 0.19.8 → 0.19.11
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/lib/local.js +11 -1
- package/lib/setup.js +33 -0
- package/package.json +1 -1
- package/runtime/codex/.codex/hooks/branch-guard.js +242 -34
- package/runtime/codex/.codex/skills/activity/SKILL.md +18 -6
- package/runtime/codex/.codex/skills/deep-reflect/SKILL.md +90 -55
- package/runtime/codex/.codex/skills/deploy-bridge/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/handoff/SKILL.md +6 -2
- package/runtime/codex/.codex/skills/save/SKILL.md +21 -3
- package/runtime/codex/.codex/skills/scroll/SKILL.md +54 -3
- package/runtime/codex/.codex/skills/task/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/tui-design/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/wrap/SKILL.md +4 -1
- package/runtime/codex/.codex/spec-manifest.json +15 -3
- package/runtime/codex/AGENTS.md +45 -9
- package/runtime/codex/bin/activity-data.sh +1 -1
- package/runtime/codex/bin/agent.sh +66 -23
- package/runtime/codex/bin/attendant.sh +525 -0
- package/runtime/codex/bin/capture-reconcile.sh +67 -0
- package/runtime/codex/bin/capture-run.sh +341 -0
- package/runtime/codex/bin/codex-render-spec.mjs +15 -4
- package/runtime/codex/bin/codex-session-start.sh +45 -3
- package/runtime/codex/bin/codex-skill-render.mjs +23 -4
- package/runtime/codex/bin/graph-op.sh +719 -49
- package/runtime/codex/bin/graph-wal.sh +3 -2
- package/runtime/codex/bin/handoff-run.sh +72 -0
- package/runtime/codex/bin/handoff-save-egregore.sh +51 -11
- package/runtime/codex/bin/index-handoff.sh +26 -17
- package/runtime/codex/bin/ingest-graph.sh +80 -0
- package/runtime/codex/bin/ingest-surface.html +429 -0
- package/runtime/codex/bin/ingest.py +614 -0
- package/runtime/codex/bin/ingest.sh +231 -0
- package/runtime/codex/bin/ingest_graph.py +776 -0
- package/runtime/codex/bin/ingest_surface.py +464 -0
- package/runtime/codex/bin/lib/git-sync.sh +2 -6
- package/runtime/codex/bin/lib/greeting.sh +157 -22
- package/runtime/codex/bin/lib/identity.sh +50 -29
- package/runtime/codex/bin/lib/noncode.sh +31 -0
- package/runtime/codex/bin/person.py +610 -0
- package/runtime/codex/bin/person.sh +448 -0
- package/runtime/codex/bin/session-autosave.sh +184 -0
- package/runtime/codex/bin/session-log.sh +199 -0
- package/runtime/codex/bin/settings.sh +1 -1
- package/runtime/codex/manifest.json +19 -1
- package/runtime/pi/.codex/hooks/branch-guard.js +242 -34
- package/runtime/pi/.codex/skills/activity/SKILL.md +18 -6
- package/runtime/pi/.codex/skills/deep-reflect/SKILL.md +90 -55
- package/runtime/pi/.codex/skills/deploy-bridge/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/handoff/SKILL.md +6 -2
- package/runtime/pi/.codex/skills/save/SKILL.md +21 -3
- package/runtime/pi/.codex/skills/scroll/SKILL.md +54 -3
- package/runtime/pi/.codex/skills/task/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/tui-design/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/wrap/SKILL.md +4 -1
- package/runtime/pi/.codex/spec-manifest.json +15 -3
- package/runtime/pi/.pi/APPEND_SYSTEM.md +41 -5
- package/runtime/pi/.pi/extensions/egregore.ts +98 -8
- package/runtime/pi/.pi/spec-manifest.json +2 -2
- package/runtime/pi/AGENTS.md +45 -9
- package/runtime/pi/bin/activity-data.sh +1 -1
- package/runtime/pi/bin/agent.sh +66 -23
- package/runtime/pi/bin/attendant.sh +525 -0
- package/runtime/pi/bin/capture-reconcile.sh +67 -0
- package/runtime/pi/bin/capture-run.sh +341 -0
- package/runtime/pi/bin/codex-render-spec.mjs +15 -4
- package/runtime/pi/bin/codex-session-start.sh +45 -3
- package/runtime/pi/bin/codex-skill-render.mjs +23 -4
- package/runtime/pi/bin/graph-op.sh +719 -49
- package/runtime/pi/bin/graph-wal.sh +3 -2
- package/runtime/pi/bin/handoff-run.sh +72 -0
- package/runtime/pi/bin/handoff-save-egregore.sh +51 -11
- package/runtime/pi/bin/index-handoff.sh +26 -17
- package/runtime/pi/bin/ingest-graph.sh +80 -0
- package/runtime/pi/bin/ingest-surface.html +429 -0
- package/runtime/pi/bin/ingest.py +614 -0
- package/runtime/pi/bin/ingest.sh +231 -0
- package/runtime/pi/bin/ingest_graph.py +776 -0
- package/runtime/pi/bin/ingest_surface.py +464 -0
- package/runtime/pi/bin/lib/git-sync.sh +2 -6
- package/runtime/pi/bin/lib/greeting.sh +157 -22
- package/runtime/pi/bin/lib/identity.sh +50 -29
- package/runtime/pi/bin/lib/noncode.sh +31 -0
- package/runtime/pi/bin/person.py +610 -0
- package/runtime/pi/bin/person.sh +448 -0
- package/runtime/pi/bin/pi-branch-policy.mjs +164 -4
- package/runtime/pi/bin/pi-product-workflows.mjs +31 -2
- package/runtime/pi/bin/publish-artifact.sh +14 -0
- package/runtime/pi/bin/session-autosave.sh +184 -0
- package/runtime/pi/bin/session-log.sh +199 -0
- package/runtime/pi/bin/settings.sh +274 -0
- package/runtime/pi/manifest.json +20 -1
|
@@ -95,6 +95,20 @@ fi
|
|
|
95
95
|
# Public relay for OSS share endpoint
|
|
96
96
|
RELAY_URL="https://egregore-production-55f2.up.railway.app"
|
|
97
97
|
|
|
98
|
+
# --- Artifact hosting off-switch (compliance / privacy) ---
|
|
99
|
+
# An instance can disable ALL egregore.xyz publishing via `bin/settings.sh hosting off`
|
|
100
|
+
# (sets features.publishing=false). Absent/true means ON, so existing instances are
|
|
101
|
+
# unaffected. We bail BEFORE any render or upload so "off" truly does nothing and costs
|
|
102
|
+
# nothing. Exit 3 is distinct from usage errors; fire-and-forget callers ignore it.
|
|
103
|
+
# NOTE: test `!= false`, not jq's `// "true"` — `//` treats an explicit `false` as empty
|
|
104
|
+
# and would return the default, silently defeating the off-switch.
|
|
105
|
+
PUBLISHING_ENABLED="$(jq -r 'if .features.publishing == false then "false" else "true" end' "$CONFIG" 2>/dev/null || echo "true")"
|
|
106
|
+
if [ "$PUBLISHING_ENABLED" = "false" ]; then
|
|
107
|
+
echo "Artifact hosting is off for this egregore (features.publishing=false); nothing uploaded." >&2
|
|
108
|
+
echo "Turn it on with: bin/settings.sh hosting on (or: egregore settings → Hosting)." >&2
|
|
109
|
+
exit 3
|
|
110
|
+
fi
|
|
111
|
+
|
|
98
112
|
# --- Prepare HTML ---
|
|
99
113
|
TMP_HTML="/tmp/egregore-artifacts/publish-$$.html"
|
|
100
114
|
mkdir -p /tmp/egregore-artifacts
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# session-autosave.sh — save non-coding session work without an explicit /save.
|
|
3
|
+
#
|
|
4
|
+
# Terminal-close is the design constraint: no single "session end" event is
|
|
5
|
+
# reliable (people just close the window), so durability comes from three
|
|
6
|
+
# overlapping triggers:
|
|
7
|
+
# - Stop hook, debounced (Claude Code): saves DURING the session, after a
|
|
8
|
+
# turn completes — a killed terminal loses at most the debounce window.
|
|
9
|
+
# - Session-start sweep (--sweep, all runtimes): the next launch on this
|
|
10
|
+
# machine rescues leftovers in the main checkout and lingering worktrees.
|
|
11
|
+
# - SessionEnd hook / agent.sh wrap (Codex, Pi): immediate save on clean exit.
|
|
12
|
+
#
|
|
13
|
+
# Gate: acts ONLY when every pending change — uncommitted files AND commits
|
|
14
|
+
# ahead of origin/develop — is non-coding per bin/lib/noncode.sh. Any code in
|
|
15
|
+
# the pending set → exit silently; code always goes through explicit /save and
|
|
16
|
+
# review. Never auto-commits half-finished code.
|
|
17
|
+
#
|
|
18
|
+
# When the gate passes, delegates commit→push→PR→auto-merge to
|
|
19
|
+
# handoff-save-egregore.sh (--kind autosave), and pushes the memory repo.
|
|
20
|
+
#
|
|
21
|
+
# Usage:
|
|
22
|
+
# session-autosave.sh [--dir <repo-dir>] [--author <name>]
|
|
23
|
+
# [--debounce <sec>] [--min-idle <sec>] [--sweep]
|
|
24
|
+
# --debounce skip if this dir was autosaved less than <sec> ago (Stop hook)
|
|
25
|
+
# --min-idle skip if any dirty file was modified less than <sec> ago
|
|
26
|
+
# (don't commit prose someone is actively typing)
|
|
27
|
+
# --sweep iterate main checkout + .claude/worktrees/* (session start)
|
|
28
|
+
# (Hook invocations pass nothing; stdin JSON supplies cwd.)
|
|
29
|
+
|
|
30
|
+
set -uo pipefail
|
|
31
|
+
|
|
32
|
+
DIR=""
|
|
33
|
+
AUTHOR=""
|
|
34
|
+
DEBOUNCE=0
|
|
35
|
+
MIN_IDLE=0
|
|
36
|
+
SWEEP=0
|
|
37
|
+
while [ $# -gt 0 ]; do
|
|
38
|
+
case "$1" in
|
|
39
|
+
--dir) DIR="${2:-}"; shift 2 ;;
|
|
40
|
+
--author) AUTHOR="${2:-}"; shift 2 ;;
|
|
41
|
+
--debounce) DEBOUNCE="${2:-0}"; shift 2 ;;
|
|
42
|
+
--min-idle) MIN_IDLE="${2:-0}"; shift 2 ;;
|
|
43
|
+
--sweep) SWEEP=1; shift ;;
|
|
44
|
+
*) shift ;;
|
|
45
|
+
esac
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
SCRIPT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
49
|
+
|
|
50
|
+
# --- Sweep mode: rescue abandoned checkouts, then exit -------------------
|
|
51
|
+
if [ "$SWEEP" = "1" ]; then
|
|
52
|
+
# Let session-start's own git syncs settle before touching the checkout.
|
|
53
|
+
sleep 15
|
|
54
|
+
for d in "$SCRIPT_ROOT" "$SCRIPT_ROOT"/.claude/worktrees/*/; do
|
|
55
|
+
d="${d%/}"
|
|
56
|
+
[ -d "$d" ] || continue
|
|
57
|
+
# min-idle protects live sessions in other terminals: never commit files
|
|
58
|
+
# someone touched in the last 10 minutes. debounce dedupes concurrent
|
|
59
|
+
# session launches sweeping the same dirs.
|
|
60
|
+
bash "$0" --dir "$d" --min-idle 600 --debounce 120 >/dev/null 2>&1 || true
|
|
61
|
+
done
|
|
62
|
+
exit 0
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# Hook invocations deliver JSON on stdin (cwd = where the session worked).
|
|
66
|
+
if [ -z "$DIR" ] && [ ! -t 0 ]; then
|
|
67
|
+
HOOK_INPUT=$(cat 2>/dev/null || true)
|
|
68
|
+
DIR=$(echo "$HOOK_INPUT" | jq -r '.cwd // empty' 2>/dev/null || true)
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
[ -n "$DIR" ] && [ -d "$DIR" ] || DIR="$SCRIPT_ROOT"
|
|
72
|
+
cd "$DIR" 2>/dev/null || exit 0
|
|
73
|
+
git rev-parse --git-dir >/dev/null 2>&1 || exit 0
|
|
74
|
+
|
|
75
|
+
# --- Debounce (per-dir stamp) --------------------------------------------
|
|
76
|
+
# NB: `cmd | cut || fallback` doesn't work — cut succeeds on empty input, so
|
|
77
|
+
# the fallback never fires and every dir collapses to one stamp (macOS has
|
|
78
|
+
# md5, not md5sum). Group the hash commands, then cut.
|
|
79
|
+
DIR_HASH=$( { md5sum <<<"$DIR" 2>/dev/null || md5 -q -s "$DIR" 2>/dev/null || echo default; } | cut -c1-8 )
|
|
80
|
+
STAMP="/tmp/.egregore-autosave-${DIR_HASH}"
|
|
81
|
+
if [ "$DEBOUNCE" -gt 0 ] && [ -f "$STAMP" ]; then
|
|
82
|
+
LAST=$(stat -f %m "$STAMP" 2>/dev/null || stat -c %Y "$STAMP" 2>/dev/null || echo 0)
|
|
83
|
+
[ $(( $(date +%s) - LAST )) -lt "$DEBOUNCE" ] && exit 0
|
|
84
|
+
fi
|
|
85
|
+
touch "$STAMP" 2>/dev/null || true
|
|
86
|
+
|
|
87
|
+
# shellcheck source=bin/lib/noncode.sh
|
|
88
|
+
. "$SCRIPT_ROOT/bin/lib/noncode.sh" 2>/dev/null || exit 0
|
|
89
|
+
|
|
90
|
+
if [ -z "$AUTHOR" ]; then
|
|
91
|
+
AUTHOR=$(jq -r '.github_username // .name // empty' "$DIR/.egregore-state.json" 2>/dev/null || true)
|
|
92
|
+
[ -n "$AUTHOR" ] || AUTHOR=$(jq -r '.github_username // .name // empty' "$SCRIPT_ROOT/.egregore-state.json" 2>/dev/null || true)
|
|
93
|
+
[ -n "$AUTHOR" ] || AUTHOR=$(git config user.name 2>/dev/null | tr '[:upper:] ' '[:lower:]-' || true)
|
|
94
|
+
[ -n "$AUTHOR" ] || AUTHOR="egregore"
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# --- Memory repo: markdown by definition, push straight to main ----------
|
|
98
|
+
if git -C "$DIR/memory" rev-parse --git-dir >/dev/null 2>&1; then
|
|
99
|
+
if [ -n "$(git -C "$DIR/memory" status --porcelain 2>/dev/null)" ]; then
|
|
100
|
+
git -C "$DIR/memory" add -A >/dev/null 2>&1 || true
|
|
101
|
+
git -C "$DIR/memory" commit -m "Auto-save: session content" --quiet 2>/dev/null || true
|
|
102
|
+
fi
|
|
103
|
+
if [ -n "$(git -C "$DIR/memory" log origin/main..HEAD --oneline 2>/dev/null)" ]; then
|
|
104
|
+
for _try in 1 2 3; do
|
|
105
|
+
if git -C "$DIR/memory" pull --rebase origin main --quiet 2>/dev/null \
|
|
106
|
+
&& git -C "$DIR/memory" push origin main --quiet 2>/dev/null; then
|
|
107
|
+
break
|
|
108
|
+
fi
|
|
109
|
+
sleep 1
|
|
110
|
+
done
|
|
111
|
+
fi
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# --- Core repo gate: everything pending must be non-coding ---------------
|
|
115
|
+
# -uall expands untracked dirs to individual files: a collapsed "docs/" entry
|
|
116
|
+
# would defeat both the classifier (code nested under a content dir) and the
|
|
117
|
+
# idle guard (directory mtime, not file mtime).
|
|
118
|
+
DIRTY_PATHS=$(git status --porcelain -uall 2>/dev/null | cut -c4- | sed 's/.* -> //' | sed '/^$/d')
|
|
119
|
+
PENDING=$(
|
|
120
|
+
{
|
|
121
|
+
printf '%s\n' "$DIRTY_PATHS"
|
|
122
|
+
git diff origin/develop...HEAD --name-only 2>/dev/null
|
|
123
|
+
} | sed '/^$/d'
|
|
124
|
+
)
|
|
125
|
+
[ -n "$PENDING" ] || exit 0
|
|
126
|
+
|
|
127
|
+
BLOCKER=$(printf '%s\n' "$PENDING" | noncode_filter)
|
|
128
|
+
if [ -n "$BLOCKER" ]; then
|
|
129
|
+
# Code in the pending set — leave it for an explicit /save + review.
|
|
130
|
+
exit 0
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
# --- Idle guard: don't commit files someone is actively editing ----------
|
|
134
|
+
if [ "$MIN_IDLE" -gt 0 ] && [ -n "$DIRTY_PATHS" ]; then
|
|
135
|
+
NOW=$(date +%s)
|
|
136
|
+
while IFS= read -r f; do
|
|
137
|
+
[ -e "$f" ] || continue
|
|
138
|
+
M=$(stat -f %m "$f" 2>/dev/null || stat -c %Y "$f" 2>/dev/null || echo 0)
|
|
139
|
+
[ $(( NOW - M )) -lt "$MIN_IDLE" ] && exit 0
|
|
140
|
+
done <<< "$DIRTY_PATHS"
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
ORIG_BRANCH=$(git branch --show-current 2>/dev/null || echo "")
|
|
144
|
+
|
|
145
|
+
bash "$SCRIPT_ROOT/bin/handoff-save-egregore.sh" "$AUTHOR" \
|
|
146
|
+
"session content $(date +%Y-%m-%d)" --kind autosave --repo-dir "$DIR" \
|
|
147
|
+
--branch-suffix "$DIR_HASH" \
|
|
148
|
+
>/dev/null 2>&1 || true
|
|
149
|
+
|
|
150
|
+
# Tell the user what just happened. Under the Stop hook this line surfaces in
|
|
151
|
+
# the Claude Code UI; sweep/SessionEnd invocations discard stdout (their trail
|
|
152
|
+
# is the ledger, reported by the next greeting).
|
|
153
|
+
LEDGER="${EGREGORE_AUTOSAVE_LOG:-$HOME/.egregore/autosave.log}"
|
|
154
|
+
LAST=$(grep -F "|autosave|$DIR|" "$LEDGER" 2>/dev/null | tail -1 || true)
|
|
155
|
+
if [ -n "$LAST" ]; then
|
|
156
|
+
TS=$(echo "$LAST" | cut -d'|' -f1)
|
|
157
|
+
PR=$(echo "$LAST" | cut -d'|' -f5)
|
|
158
|
+
RESULT=$(echo "$LAST" | cut -d'|' -f6)
|
|
159
|
+
NF=$(echo "$LAST" | cut -d'|' -f7)
|
|
160
|
+
NOW_EPOCH=$(date +%s)
|
|
161
|
+
TS_EPOCH=$(date -u -j -f %Y-%m-%dT%H:%M:%SZ "$TS" +%s 2>/dev/null || date -u -d "$TS" +%s 2>/dev/null || echo 0)
|
|
162
|
+
if [ $(( NOW_EPOCH - TS_EPOCH )) -lt 120 ]; then
|
|
163
|
+
case "$RESULT" in
|
|
164
|
+
merged) echo "⟲ auto-saved: $NF non-coding file(s) → develop (PR #$PR merged)" ;;
|
|
165
|
+
open) echo "⟲ auto-saved: $NF non-coding file(s) → PR #$PR (merge pending)" ;;
|
|
166
|
+
*) echo "⟲ auto-saved: $NF non-coding file(s) → branch pushed (PR pending)" ;;
|
|
167
|
+
esac
|
|
168
|
+
fi
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
# The helper branches off protected branches before committing. If this
|
|
172
|
+
# checkout was on develop/main (e.g. the main checkout during a sweep),
|
|
173
|
+
# restore it so the next session doesn't land on a stray autosave branch.
|
|
174
|
+
case "$ORIG_BRANCH" in
|
|
175
|
+
develop|main|master)
|
|
176
|
+
CUR=$(git branch --show-current 2>/dev/null || echo "")
|
|
177
|
+
if [ "$CUR" != "$ORIG_BRANCH" ]; then
|
|
178
|
+
git checkout "$ORIG_BRANCH" --quiet 2>/dev/null || true
|
|
179
|
+
git pull --ff-only origin "$ORIG_BRANCH" --quiet 2>/dev/null || true
|
|
180
|
+
fi
|
|
181
|
+
;;
|
|
182
|
+
esac
|
|
183
|
+
|
|
184
|
+
exit 0
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Graceful session-end adapter: write a structured session log to memory/sessions/.
|
|
3
|
+
# Receives runtime-neutral JSON on stdin from Claude Code or Pi.
|
|
4
|
+
# Runs BEFORE transcript-archive.sh (needs obs buffer + worktree intact).
|
|
5
|
+
# Makes sessions visible in local-mode without requiring /handoff or /wrap.
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
# Suppress all output — hook scripts must not leak to terminal
|
|
9
|
+
exec >/dev/null 2>&1
|
|
10
|
+
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")/.." 2>/dev/null && pwd)" || SCRIPT_DIR=""
|
|
12
|
+
if [ -z "$SCRIPT_DIR" ] || [ ! -d "$SCRIPT_DIR" ]; then
|
|
13
|
+
SCRIPT_DIR="${CLAUDE_PROJECT_DIR:-}"
|
|
14
|
+
fi
|
|
15
|
+
if [ -z "$SCRIPT_DIR" ] || [ ! -d "$SCRIPT_DIR" ]; then
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# --- Read hook input from stdin ---
|
|
20
|
+
INPUT=$(cat /dev/stdin 2>/dev/null || echo "{}")
|
|
21
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null || true)
|
|
22
|
+
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null || true)
|
|
23
|
+
|
|
24
|
+
if [ -z "$SESSION_ID" ]; then
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# Sanitize SESSION_ID
|
|
29
|
+
SESSION_ID=$(echo "$SESSION_ID" | tr -cd 'a-zA-Z0-9_-')
|
|
30
|
+
if [ -z "$SESSION_ID" ] || [ ${#SESSION_ID} -gt 128 ]; then
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# --- Gate: memory must exist ---
|
|
35
|
+
if [ ! -L "$SCRIPT_DIR/memory" ] && [ ! -d "$SCRIPT_DIR/memory" ]; then
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# --- Read identity ---
|
|
40
|
+
STATE_FILE="$SCRIPT_DIR/.egregore-state.json"
|
|
41
|
+
AUTHOR=""
|
|
42
|
+
DISPLAY_NAME=""
|
|
43
|
+
if [ -f "$STATE_FILE" ]; then
|
|
44
|
+
AUTHOR=$(jq -r '.github_username // empty' "$STATE_FILE" 2>/dev/null || true)
|
|
45
|
+
DISPLAY_NAME=$(jq -r '.display_name // empty' "$STATE_FILE" 2>/dev/null || true)
|
|
46
|
+
fi
|
|
47
|
+
AUTHOR=$(echo "$AUTHOR" | tr -cd 'a-zA-Z0-9_-')
|
|
48
|
+
if [ -z "$AUTHOR" ]; then
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# --- Git metadata ---
|
|
53
|
+
BRANCH=$(git -C "$SCRIPT_DIR" branch --show-current 2>/dev/null || echo "")
|
|
54
|
+
|
|
55
|
+
# --- Derive topic from branch name ---
|
|
56
|
+
TOPIC=""
|
|
57
|
+
if [ -n "$BRANCH" ] && [ "$BRANCH" != "develop" ] && [ "$BRANCH" != "main" ]; then
|
|
58
|
+
TOPIC=$(echo "$BRANCH" | sed 's|^dev/[^/]*/||; s|^feature/||; s|^bugfix/||' | tr '-' ' ')
|
|
59
|
+
fi
|
|
60
|
+
[ -z "$TOPIC" ] && TOPIC="session"
|
|
61
|
+
|
|
62
|
+
# --- Read baseline (written by session-start.sh) ---
|
|
63
|
+
BASELINE_FILE="/tmp/egregore-baseline-${SESSION_ID}.json"
|
|
64
|
+
BASELINE_COMMIT=""
|
|
65
|
+
BASELINE_STARTED=""
|
|
66
|
+
if [ -f "$BASELINE_FILE" ]; then
|
|
67
|
+
BASELINE_COMMIT=$(jq -r '.commit // empty' "$BASELINE_FILE" 2>/dev/null || true)
|
|
68
|
+
BASELINE_STARTED=$(jq -r '.started_at // empty' "$BASELINE_FILE" 2>/dev/null || true)
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# --- Extract timestamps from transcript ---
|
|
72
|
+
STARTED_AT=""
|
|
73
|
+
ENDED_AT=""
|
|
74
|
+
if [ -n "$TRANSCRIPT_PATH" ]; then
|
|
75
|
+
TRANSCRIPT_PATH="${TRANSCRIPT_PATH/#\~/$HOME}"
|
|
76
|
+
TRANSCRIPT_PATH=$(realpath "$TRANSCRIPT_PATH" 2>/dev/null || echo "")
|
|
77
|
+
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
78
|
+
STARTED_AT=$(head -1 "$TRANSCRIPT_PATH" 2>/dev/null | jq -r '.timestamp // empty' 2>/dev/null || echo "")
|
|
79
|
+
ENDED_AT=$(tail -1 "$TRANSCRIPT_PATH" 2>/dev/null | jq -r '.timestamp // empty' 2>/dev/null || echo "")
|
|
80
|
+
fi
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
# Use baseline start time if transcript didn't provide one
|
|
84
|
+
[ -z "$STARTED_AT" ] && STARTED_AT="$BASELINE_STARTED"
|
|
85
|
+
# Fallback to now
|
|
86
|
+
[ -z "$ENDED_AT" ] && ENDED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
87
|
+
[ -z "$STARTED_AT" ] && STARTED_AT="$ENDED_AT"
|
|
88
|
+
|
|
89
|
+
# --- Compute duration ---
|
|
90
|
+
DURATION=""
|
|
91
|
+
DURATION_MIN=0
|
|
92
|
+
if [ -n "$STARTED_AT" ] && [ -n "$ENDED_AT" ]; then
|
|
93
|
+
S_EPOCH=$(python3 -c "from datetime import datetime; print(int(datetime.fromisoformat('$STARTED_AT'.replace('Z','+00:00')).timestamp()))" 2>/dev/null || echo "0")
|
|
94
|
+
E_EPOCH=$(python3 -c "from datetime import datetime; print(int(datetime.fromisoformat('$ENDED_AT'.replace('Z','+00:00')).timestamp()))" 2>/dev/null || echo "0")
|
|
95
|
+
if [ "$S_EPOCH" -gt 0 ] 2>/dev/null && [ "$E_EPOCH" -gt 0 ] 2>/dev/null; then
|
|
96
|
+
DURATION_MIN=$(( (E_EPOCH - S_EPOCH) / 60 ))
|
|
97
|
+
if [ "$DURATION_MIN" -lt 1 ] 2>/dev/null; then
|
|
98
|
+
DURATION="<1min"
|
|
99
|
+
elif [ "$DURATION_MIN" -lt 60 ] 2>/dev/null; then
|
|
100
|
+
DURATION="${DURATION_MIN}min"
|
|
101
|
+
else
|
|
102
|
+
HOURS=$(( DURATION_MIN / 60 ))
|
|
103
|
+
MINS=$(( DURATION_MIN % 60 ))
|
|
104
|
+
if [ "$MINS" -gt 0 ] 2>/dev/null; then
|
|
105
|
+
DURATION="${HOURS}h ${MINS}min"
|
|
106
|
+
else
|
|
107
|
+
DURATION="${HOURS}h"
|
|
108
|
+
fi
|
|
109
|
+
fi
|
|
110
|
+
fi
|
|
111
|
+
fi
|
|
112
|
+
[ -z "$DURATION" ] && DURATION="unknown"
|
|
113
|
+
|
|
114
|
+
# --- Read obs buffer for files touched ---
|
|
115
|
+
OBS_BUFFER="/tmp/egregore-obs-${SESSION_ID}.jsonl"
|
|
116
|
+
OBS_FILES=""
|
|
117
|
+
OBS_COUNT=0
|
|
118
|
+
if [ -f "$OBS_BUFFER" ] && [ -s "$OBS_BUFFER" ]; then
|
|
119
|
+
OBS_COUNT=$(wc -l < "$OBS_BUFFER" 2>/dev/null | tr -d ' ')
|
|
120
|
+
OBS_FILES=$(jq -r '.path // empty' "$OBS_BUFFER" 2>/dev/null | grep -v '^$' | grep -v '^unknown$' | sort -u | head -10 || true)
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# --- Count commits since baseline ---
|
|
124
|
+
COMMIT_COUNT=0
|
|
125
|
+
if [ -n "$BASELINE_COMMIT" ] && git -C "$SCRIPT_DIR" rev-parse "$BASELINE_COMMIT" >/dev/null 2>&1; then
|
|
126
|
+
COMMIT_COUNT=$(git -C "$SCRIPT_DIR" rev-list "${BASELINE_COMMIT}..HEAD" --count 2>/dev/null || echo "0")
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
# --- Empty session guard ---
|
|
130
|
+
# Skip if: duration < 2 min AND no commits AND no obs buffer activity
|
|
131
|
+
if [ "$DURATION_MIN" -lt 2 ] 2>/dev/null && [ "$COMMIT_COUNT" -eq 0 ] 2>/dev/null && [ "$OBS_COUNT" -eq 0 ] 2>/dev/null; then
|
|
132
|
+
rm -f "$BASELINE_FILE" 2>/dev/null
|
|
133
|
+
exit 0
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
# --- Deduplication: explicit /handoff or /wrap wins over baseline capture ---
|
|
137
|
+
TODAY=$(date +%d)
|
|
138
|
+
MONTH=$(date +%Y-%m)
|
|
139
|
+
if [ -n "$STARTED_AT" ]; then
|
|
140
|
+
MONTH=$(echo "$STARTED_AT" | grep -oE '^[0-9]{4}-[0-9]{2}' || date +%Y-%m)
|
|
141
|
+
TODAY=$(echo "$STARTED_AT" | grep -oE '^[0-9]{4}-[0-9]{2}-([0-9]{2})' | tail -c3 | sed 's/^-//' || date +%d)
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
AUTHOR_LC=$(echo "$AUTHOR" | tr '[:upper:]' '[:lower:]')
|
|
145
|
+
DISPLAY_LC=$(echo "$DISPLAY_NAME" | tr '[:upper:]' '[:lower:]')
|
|
146
|
+
AUTHOR_PATTERN="-${AUTHOR_LC}-"
|
|
147
|
+
[ -n "$DISPLAY_LC" ] && AUTHOR_PATTERN="${AUTHOR_PATTERN}|-${DISPLAY_LC}-"
|
|
148
|
+
for CAPTURE_DIR in \
|
|
149
|
+
"$SCRIPT_DIR/memory/handoffs/$MONTH" \
|
|
150
|
+
"$SCRIPT_DIR/memory/wraps/$MONTH"; do
|
|
151
|
+
[ -d "$CAPTURE_DIR" ] || continue
|
|
152
|
+
# Match the canonical handle or display name in today's filename.
|
|
153
|
+
EXISTING=""
|
|
154
|
+
for CANDIDATE in "$CAPTURE_DIR/${TODAY}-"*.md; do
|
|
155
|
+
[ -e "$CANDIDATE" ] || continue
|
|
156
|
+
CANDIDATE_NAME="$(basename "$CANDIDATE" | tr '[:upper:]' '[:lower:]')"
|
|
157
|
+
if printf '%s\n' "$CANDIDATE_NAME" | grep -Eq -- "$AUTHOR_PATTERN"; then
|
|
158
|
+
EXISTING="$CANDIDATE_NAME"
|
|
159
|
+
break
|
|
160
|
+
fi
|
|
161
|
+
done
|
|
162
|
+
if [ -n "$EXISTING" ]; then
|
|
163
|
+
rm -f "$BASELINE_FILE" 2>/dev/null
|
|
164
|
+
exit 0
|
|
165
|
+
fi
|
|
166
|
+
done
|
|
167
|
+
|
|
168
|
+
# --- Write through the shared capture engine -----------------------------
|
|
169
|
+
SUMMARY="${DURATION} session on ${TOPIC}; ${COMMIT_COUNT} commit(s), ${OBS_COUNT} observed action(s)."
|
|
170
|
+
{
|
|
171
|
+
if [ -n "$OBS_FILES" ]; then
|
|
172
|
+
echo "## Files"
|
|
173
|
+
echo "$OBS_FILES" | while read -r F; do
|
|
174
|
+
[ -n "$F" ] && echo "- $F"
|
|
175
|
+
done
|
|
176
|
+
echo ""
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
if [ "$COMMIT_COUNT" -gt 0 ] 2>/dev/null; then
|
|
180
|
+
echo "## Commits"
|
|
181
|
+
echo "$COMMIT_COUNT commits on ${BRANCH:-develop}"
|
|
182
|
+
echo ""
|
|
183
|
+
fi
|
|
184
|
+
} | bash "$SCRIPT_DIR/bin/capture-run.sh" \
|
|
185
|
+
--mode baseline \
|
|
186
|
+
--author "$AUTHOR" \
|
|
187
|
+
--display-name "$DISPLAY_NAME" \
|
|
188
|
+
--topic "$TOPIC" \
|
|
189
|
+
--summary "$SUMMARY" \
|
|
190
|
+
--session-id "$SESSION_ID" \
|
|
191
|
+
--branch "${BRANCH:-develop}" \
|
|
192
|
+
--date "$STARTED_AT" \
|
|
193
|
+
--duration "$DURATION" \
|
|
194
|
+
--async-push >/dev/null 2>&1 || true
|
|
195
|
+
|
|
196
|
+
# --- Clean up baseline ---
|
|
197
|
+
rm -f "$BASELINE_FILE" 2>/dev/null
|
|
198
|
+
|
|
199
|
+
exit 0
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/bin/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 repo list | add <name> [description] | remove <name>
|
|
14
|
+
# settings.sh admin list | add <github-handle> | remove <github-handle>
|
|
15
|
+
# settings.sh dump # full settings snapshot as JSON (for the launcher)
|
|
16
|
+
#
|
|
17
|
+
# Add --json to a read verb (status/list) for machine-readable output.
|
|
18
|
+
#
|
|
19
|
+
# Exit codes: 0 ok · 1 usage/validation error · 2 config not found.
|
|
20
|
+
|
|
21
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
22
|
+
CONFIG="$SCRIPT_DIR/egregore.json"
|
|
23
|
+
|
|
24
|
+
# ── helpers ──────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
_die() { echo "$1" >&2; exit "${2:-1}"; }
|
|
27
|
+
|
|
28
|
+
_valid_token() {
|
|
29
|
+
# non-empty, GitHub-handle / repo-name safe charset
|
|
30
|
+
[ -n "${1:-}" ] && printf '%s' "$1" | grep -qE '^[A-Za-z0-9._-]+$'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_save() {
|
|
34
|
+
# _save '<jq-filter>' [jq-args...] — apply filter to CONFIG, write atomically.
|
|
35
|
+
local filter="$1"; shift
|
|
36
|
+
local tmp
|
|
37
|
+
tmp="$(mktemp "$CONFIG.XXXXXX")" || _die "cannot create temp file" 1
|
|
38
|
+
if jq "$@" "$filter" "$CONFIG" > "$tmp" 2>/dev/null && [ -s "$tmp" ]; then
|
|
39
|
+
mv "$tmp" "$CONFIG"
|
|
40
|
+
else
|
|
41
|
+
rm -f "$tmp"
|
|
42
|
+
_die "failed to update $CONFIG (invalid jq filter or unwritable file)" 1
|
|
43
|
+
fi
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_has_repo() { jq -e --arg n "$1" 'any(.repos[]?; (if type=="object" then .name else . end) == $n)' "$CONFIG" >/dev/null; }
|
|
47
|
+
_has_admin() { jq -e --arg h "$1" 'any(.admins[]?; . == $h)' "$CONFIG" >/dev/null; }
|
|
48
|
+
|
|
49
|
+
usage() {
|
|
50
|
+
cat >&2 <<'EOF'
|
|
51
|
+
settings.sh — Egregore instance settings
|
|
52
|
+
|
|
53
|
+
settings.sh hosting status|on|off
|
|
54
|
+
settings.sh repo list | add <name> [description] | remove <name>
|
|
55
|
+
settings.sh admin list | add <github-handle> | remove <github-handle>
|
|
56
|
+
settings.sh people list | add <github-handle> | remove <github-handle>
|
|
57
|
+
settings.sh dump full snapshot (JSON)
|
|
58
|
+
|
|
59
|
+
--json machine-readable output for status/list
|
|
60
|
+
EOF
|
|
61
|
+
exit "${1:-1}"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# ── verbs ────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
cmd_hosting() {
|
|
67
|
+
local action="${1:-status}"
|
|
68
|
+
# NEVER use jq's `// "true"` here: `//` treats an explicit `false` as empty and
|
|
69
|
+
# would return the default, silently defeating the off-switch. Test `!= false`
|
|
70
|
+
# so absent/true => enabled, explicit false => disabled. `enabled` is the
|
|
71
|
+
# resolved boolean string ("true"/"false").
|
|
72
|
+
local enabled; enabled="$(jq -r '.features.publishing != false' "$CONFIG" 2>/dev/null || echo "true")"
|
|
73
|
+
case "$action" in
|
|
74
|
+
status)
|
|
75
|
+
if [ "$JSON" = 1 ]; then
|
|
76
|
+
jq -n --argjson on "$enabled" '{domain:"hosting", publishing:$on}'
|
|
77
|
+
elif [ "$enabled" = "false" ]; then echo "Hosting (egregore.xyz): OFF"
|
|
78
|
+
else echo "Hosting (egregore.xyz): ON"; fi ;;
|
|
79
|
+
on)
|
|
80
|
+
_save '.features.publishing = true'
|
|
81
|
+
echo "Hosting (egregore.xyz): ON" ;;
|
|
82
|
+
off)
|
|
83
|
+
_save '.features.publishing = false'
|
|
84
|
+
echo "Hosting (egregore.xyz): OFF — artifacts will no longer upload to egregore.xyz" ;;
|
|
85
|
+
*) _die "usage: settings.sh hosting status|on|off" 1 ;;
|
|
86
|
+
esac
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
cmd_repo() {
|
|
90
|
+
local action="${1:-list}"; shift || true
|
|
91
|
+
case "$action" in
|
|
92
|
+
list)
|
|
93
|
+
if [ "$JSON" = 1 ]; then
|
|
94
|
+
jq '[.repos[]? | if type=="object" then . else {name:.} end]' "$CONFIG"
|
|
95
|
+
else
|
|
96
|
+
jq -r '.repos[]? | if type=="object" then " " + .name + (if .description then " — " + .description else "" end) else " " + . end' "$CONFIG"
|
|
97
|
+
fi ;;
|
|
98
|
+
add)
|
|
99
|
+
local name="${1:-}"; shift || true
|
|
100
|
+
local desc="${*:-}"
|
|
101
|
+
_valid_token "$name" || _die "usage: settings.sh repo add <name> [description]" 1
|
|
102
|
+
if _has_repo "$name"; then echo "repo '$name' already managed"; return 0; fi
|
|
103
|
+
if [ -n "$desc" ]; then
|
|
104
|
+
_save '.repos = ((.repos // []) + [{name:$n, description:$d}])' --arg n "$name" --arg d "$desc"
|
|
105
|
+
else
|
|
106
|
+
_save '.repos = ((.repos // []) + [{name:$n}])' --arg n "$name"
|
|
107
|
+
fi
|
|
108
|
+
echo "added repo '$name'"
|
|
109
|
+
echo " clone it with: /sync-repos (or reopen the egregore)" ;;
|
|
110
|
+
remove)
|
|
111
|
+
local name="${1:-}"
|
|
112
|
+
_valid_token "$name" || _die "usage: settings.sh repo remove <name>" 1
|
|
113
|
+
if ! _has_repo "$name"; then echo "repo '$name' is not managed"; return 0; fi
|
|
114
|
+
_save '.repos = [.repos[]? | select((if type=="object" then .name else . end) != $n)]' --arg n "$name"
|
|
115
|
+
echo "removed repo '$name' from config"
|
|
116
|
+
echo " note: the local ../$name checkout is left untouched" ;;
|
|
117
|
+
*) _die "usage: settings.sh repo list|add|remove" 1 ;;
|
|
118
|
+
esac
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
cmd_admin() {
|
|
122
|
+
local action="${1:-list}"; shift || true
|
|
123
|
+
case "$action" in
|
|
124
|
+
list)
|
|
125
|
+
if [ "$JSON" = 1 ]; then jq '{admins:(.admins // [])}' "$CONFIG"
|
|
126
|
+
else jq -r '.admins[]? | " " + .' "$CONFIG"; fi ;;
|
|
127
|
+
add)
|
|
128
|
+
local h="${1:-}"
|
|
129
|
+
_valid_token "$h" || _die "usage: settings.sh admin add <github-handle>" 1
|
|
130
|
+
if _has_admin "$h"; then echo "'$h' is already an admin"; return 0; fi
|
|
131
|
+
_save '.admins = ((.admins // []) + [$h])' --arg h "$h"
|
|
132
|
+
echo "added admin '$h'" ;;
|
|
133
|
+
remove)
|
|
134
|
+
local h="${1:-}"
|
|
135
|
+
_valid_token "$h" || _die "usage: settings.sh admin remove <github-handle>" 1
|
|
136
|
+
if ! _has_admin "$h"; then echo "'$h' is not an admin"; return 0; fi
|
|
137
|
+
local count; count="$(jq '(.admins // []) | length' "$CONFIG")"
|
|
138
|
+
[ "$count" -le 1 ] && _die "refusing to remove the last admin ('$h') — add another admin first" 1
|
|
139
|
+
_save '.admins = [.admins[]? | select(. != $h)]' --arg h "$h"
|
|
140
|
+
echo "removed admin '$h'" ;;
|
|
141
|
+
*) _die "usage: settings.sh admin list|add|remove" 1 ;;
|
|
142
|
+
esac
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# ── people ───────────────────────────────────────────────────────────────
|
|
146
|
+
# People live as files in memory/people/. add/remove manage that directory AND
|
|
147
|
+
# GitHub repo access (the meaningful local grant). Connected-mode extras — the
|
|
148
|
+
# invite link + Telegram (/invite) and Supabase/Neo4j teardown (/delete-user) —
|
|
149
|
+
# are pointed to, not duplicated here.
|
|
150
|
+
|
|
151
|
+
_people_dir() { echo "$SCRIPT_DIR/memory/people"; }
|
|
152
|
+
|
|
153
|
+
_people_names() {
|
|
154
|
+
local pdir; pdir="$(_people_dir)"
|
|
155
|
+
[ -d "$pdir" ] || return 0
|
|
156
|
+
ls "$pdir"/*.md 2>/dev/null | while IFS= read -r f; do basename "$f" .md; done \
|
|
157
|
+
| grep -viE '^(index|readme)$' || true
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
_people_json() {
|
|
161
|
+
local names; names="$(_people_names)"
|
|
162
|
+
if [ -z "$names" ]; then echo '[]'; return; fi
|
|
163
|
+
printf '%s\n' "$names" | jq -R . | jq -sc 'map(select(length>0))'
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
_has_person() { [ -f "$(_people_dir)/$1.md" ]; }
|
|
167
|
+
|
|
168
|
+
# add|remove a GitHub push-collaborator across the instance's repos. Best-effort:
|
|
169
|
+
# no token/org => skip with a note; per-repo failures never abort.
|
|
170
|
+
_people_github() {
|
|
171
|
+
local user="$1" op="$2" token org repos r
|
|
172
|
+
token="$(grep '^GITHUB_TOKEN=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d= -f2- || true)"
|
|
173
|
+
org="$(jq -r '.github_org // empty' "$CONFIG" 2>/dev/null)"
|
|
174
|
+
if [ -z "$token" ] || [ -z "$org" ]; then
|
|
175
|
+
echo " (no GitHub token/org — skipped repo access; run bin/github-auth.sh)" >&2
|
|
176
|
+
return 0
|
|
177
|
+
fi
|
|
178
|
+
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)"
|
|
179
|
+
for r in $repos; do
|
|
180
|
+
if [ "$op" = "add" ]; then
|
|
181
|
+
curl -s -o /dev/null -X PUT -H "Authorization: Bearer $token" -H "Accept: application/vnd.github+json" \
|
|
182
|
+
"https://api.github.com/repos/$org/$r/collaborators/$user" -d '{"permission":"push"}' 2>/dev/null || true
|
|
183
|
+
else
|
|
184
|
+
curl -s -o /dev/null -X DELETE -H "Authorization: Bearer $token" -H "Accept: application/vnd.github+json" \
|
|
185
|
+
"https://api.github.com/repos/$org/$r/collaborators/$user" 2>/dev/null || true
|
|
186
|
+
fi
|
|
187
|
+
done
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# create|remove memory/people/<user>.md, committing ONLY the people dir.
|
|
191
|
+
_people_file() {
|
|
192
|
+
local user="$1" op="$2" pdir f inviter today
|
|
193
|
+
pdir="$(_people_dir)"
|
|
194
|
+
[ -d "$SCRIPT_DIR/memory" ] || { echo " (no memory dir — skipped person file)" >&2; return 0; }
|
|
195
|
+
mkdir -p "$pdir"
|
|
196
|
+
f="$pdir/$user.md"
|
|
197
|
+
if [ "$op" = "add" ]; then
|
|
198
|
+
if [ ! -f "$f" ]; then
|
|
199
|
+
inviter="$(jq -r '.display_name // .github_username // "admin"' "$SCRIPT_DIR/.egregore-state.json" 2>/dev/null || echo admin)"
|
|
200
|
+
today="$(date -u +%Y-%m-%d)"
|
|
201
|
+
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"
|
|
202
|
+
fi
|
|
203
|
+
else
|
|
204
|
+
rm -f "$f"
|
|
205
|
+
fi
|
|
206
|
+
( cd "$SCRIPT_DIR/memory" 2>/dev/null && git add -A people/ 2>/dev/null \
|
|
207
|
+
&& git commit -q -m "settings: people $op $user" 2>/dev/null \
|
|
208
|
+
&& git push -q 2>/dev/null ) || true
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
cmd_people() {
|
|
212
|
+
local action="${1:-list}"; shift || true
|
|
213
|
+
case "$action" in
|
|
214
|
+
list)
|
|
215
|
+
if [ "$JSON" = 1 ]; then _people_json
|
|
216
|
+
else
|
|
217
|
+
local names; names="$(_people_names)"
|
|
218
|
+
if [ -n "$names" ]; then printf ' %s\n' $names; else echo " (none)"; fi
|
|
219
|
+
fi ;;
|
|
220
|
+
add)
|
|
221
|
+
local h="${1:-}"
|
|
222
|
+
_valid_token "$h" || _die "usage: settings.sh people add <github-handle>" 1
|
|
223
|
+
if _has_person "$h"; then echo "'$h' is already in people"; return 0; fi
|
|
224
|
+
_people_github "$h" "add"
|
|
225
|
+
_people_file "$h" "add"
|
|
226
|
+
echo "added '$h' (repo access + person file)"
|
|
227
|
+
[ -n "$(jq -r '.api_url // empty' "$CONFIG" 2>/dev/null)" ] && echo " connected org: for the invite link + Telegram, run /invite $h" ;;
|
|
228
|
+
remove)
|
|
229
|
+
local h="${1:-}"
|
|
230
|
+
_valid_token "$h" || _die "usage: settings.sh people remove <github-handle>" 1
|
|
231
|
+
if ! _has_person "$h"; then echo "'$h' is not in people"; return 0; fi
|
|
232
|
+
_people_github "$h" "remove"
|
|
233
|
+
_people_file "$h" "remove"
|
|
234
|
+
echo "removed '$h' (repo access + person file)"
|
|
235
|
+
[ -n "$(jq -r '.api_url // empty' "$CONFIG" 2>/dev/null)" ] && echo " connected org: for full deprovision (Supabase/Neo4j), run /delete-user $h" ;;
|
|
236
|
+
*) _die "usage: settings.sh people list|add|remove" 1 ;;
|
|
237
|
+
esac
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
cmd_dump() {
|
|
241
|
+
# Always JSON — the launcher calls this once to render the whole settings screen.
|
|
242
|
+
local people; people="$(_people_json)"
|
|
243
|
+
jq --argjson people "$people" '{
|
|
244
|
+
org: .org_name,
|
|
245
|
+
slug: .slug,
|
|
246
|
+
hosting: (.features.publishing != false),
|
|
247
|
+
repos: [.repos[]? | if type=="object" then . else {name:.} end],
|
|
248
|
+
admins: (.admins // []),
|
|
249
|
+
people: $people
|
|
250
|
+
}' "$CONFIG"
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
# ── dispatch ─────────────────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
[ -f "$CONFIG" ] || _die "egregore.json not found at $CONFIG" 2
|
|
256
|
+
|
|
257
|
+
# Pull out --json (bash 3.2-safe array handling), leave the rest as positionals.
|
|
258
|
+
JSON=0
|
|
259
|
+
_args=()
|
|
260
|
+
for _a in "$@"; do
|
|
261
|
+
if [ "$_a" = "--json" ]; then JSON=1; else _args+=("$_a"); fi
|
|
262
|
+
done
|
|
263
|
+
set -- "${_args[@]+"${_args[@]}"}"
|
|
264
|
+
|
|
265
|
+
DOMAIN="${1:-}"; shift || true
|
|
266
|
+
case "$DOMAIN" in
|
|
267
|
+
hosting) cmd_hosting "$@" ;;
|
|
268
|
+
repo) cmd_repo "$@" ;;
|
|
269
|
+
admin) cmd_admin "$@" ;;
|
|
270
|
+
people) cmd_people "$@" ;;
|
|
271
|
+
dump) cmd_dump ;;
|
|
272
|
+
""|-h|--help|help) usage 0 ;;
|
|
273
|
+
*) _die "unknown settings domain: '$DOMAIN' (try: hosting, repo, admin, people, dump)" 1 ;;
|
|
274
|
+
esac
|