create-egregore 0.17.3 → 0.18.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 (110) hide show
  1. package/assets/egregore-launcher.js +1965 -0
  2. package/assets/egregore-launcher.sh +385 -0
  3. package/bin/cli.js +472 -81
  4. package/lib/api.js +3 -3
  5. package/lib/auth.js +255 -20
  6. package/lib/codex-runtime.js +268 -0
  7. package/lib/commands.js +31 -0
  8. package/lib/context.js +1 -1
  9. package/lib/local.js +618 -193
  10. package/lib/setup.js +837 -85
  11. package/lib/ui.js +780 -84
  12. package/package.json +17 -4
  13. package/runtime/codex/.codex/config.toml +15 -0
  14. package/runtime/codex/.codex/hooks/branch-guard.js +234 -0
  15. package/runtime/codex/.codex/hooks.json +17 -0
  16. package/runtime/codex/.codex/skills/activity/SKILL.md +70 -0
  17. package/runtime/codex/.codex/skills/add/SKILL.md +25 -0
  18. package/runtime/codex/.codex/skills/announce/SKILL.md +54 -0
  19. package/runtime/codex/.codex/skills/answer/SKILL.md +25 -0
  20. package/runtime/codex/.codex/skills/archive/SKILL.md +41 -0
  21. package/runtime/codex/.codex/skills/ask/SKILL.md +59 -0
  22. package/runtime/codex/.codex/skills/branch/SKILL.md +25 -0
  23. package/runtime/codex/.codex/skills/checkup/SKILL.md +41 -0
  24. package/runtime/codex/.codex/skills/comment/SKILL.md +25 -0
  25. package/runtime/codex/.codex/skills/commit/SKILL.md +25 -0
  26. package/runtime/codex/.codex/skills/contribute/SKILL.md +25 -0
  27. package/runtime/codex/.codex/skills/dark-mode/SKILL.md +25 -0
  28. package/runtime/codex/.codex/skills/dashboard/SKILL.md +59 -0
  29. package/runtime/codex/.codex/skills/deep-reflect/SKILL.md +70 -0
  30. package/runtime/codex/.codex/skills/delete-user/SKILL.md +25 -0
  31. package/runtime/codex/.codex/skills/edit/SKILL.md +25 -0
  32. package/runtime/codex/.codex/skills/edit-process/SKILL.md +25 -0
  33. package/runtime/codex/.codex/skills/egregore-voice/SKILL.md +25 -0
  34. package/runtime/codex/.codex/skills/emissary/SKILL.md +41 -0
  35. package/runtime/codex/.codex/skills/env/SKILL.md +25 -0
  36. package/runtime/codex/.codex/skills/handoff/SKILL.md +67 -0
  37. package/runtime/codex/.codex/skills/harvest/SKILL.md +107 -0
  38. package/runtime/codex/.codex/skills/infra/SKILL.md +41 -0
  39. package/runtime/codex/.codex/skills/ingest/SKILL.md +25 -0
  40. package/runtime/codex/.codex/skills/ingest-google/SKILL.md +25 -0
  41. package/runtime/codex/.codex/skills/ingest-user-interview/SKILL.md +41 -0
  42. package/runtime/codex/.codex/skills/invite/SKILL.md +40 -0
  43. package/runtime/codex/.codex/skills/issue/SKILL.md +41 -0
  44. package/runtime/codex/.codex/skills/me/SKILL.md +25 -0
  45. package/runtime/codex/.codex/skills/meeting/SKILL.md +41 -0
  46. package/runtime/codex/.codex/skills/note/SKILL.md +25 -0
  47. package/runtime/codex/.codex/skills/onboarding/SKILL.md +41 -0
  48. package/runtime/codex/.codex/skills/pr/SKILL.md +25 -0
  49. package/runtime/codex/.codex/skills/project/SKILL.md +41 -0
  50. package/runtime/codex/.codex/skills/pull/SKILL.md +25 -0
  51. package/runtime/codex/.codex/skills/push/SKILL.md +25 -0
  52. package/runtime/codex/.codex/skills/quest/SKILL.md +77 -0
  53. package/runtime/codex/.codex/skills/quest-suggest/SKILL.md +25 -0
  54. package/runtime/codex/.codex/skills/reflect/SKILL.md +41 -0
  55. package/runtime/codex/.codex/skills/review-pr/SKILL.md +41 -0
  56. package/runtime/codex/.codex/skills/save/SKILL.md +62 -0
  57. package/runtime/codex/.codex/skills/setup/SKILL.md +25 -0
  58. package/runtime/codex/.codex/skills/summon/SKILL.md +41 -0
  59. package/runtime/codex/.codex/skills/sync-repos/SKILL.md +25 -0
  60. package/runtime/codex/.codex/skills/telegram-connect/SKILL.md +25 -0
  61. package/runtime/codex/.codex/skills/telemetry/SKILL.md +25 -0
  62. package/runtime/codex/.codex/skills/test/SKILL.md +41 -0
  63. package/runtime/codex/.codex/skills/the-spiral/SKILL.md +84 -0
  64. package/runtime/codex/.codex/skills/todo/SKILL.md +41 -0
  65. package/runtime/codex/.codex/skills/tui-design/SKILL.md +41 -0
  66. package/runtime/codex/.codex/skills/tutorial/SKILL.md +41 -0
  67. package/runtime/codex/.codex/skills/update/SKILL.md +25 -0
  68. package/runtime/codex/.codex/skills/view/SKILL.md +41 -0
  69. package/runtime/codex/.codex/skills/visual-explain/SKILL.md +41 -0
  70. package/runtime/codex/.codex/skills/wrap/SKILL.md +55 -0
  71. package/runtime/codex/.codex/spec-manifest.json +108 -0
  72. package/runtime/codex/AGENTS.md +328 -0
  73. package/runtime/codex/bin/activity-data.sh +180 -0
  74. package/runtime/codex/bin/agent.sh +669 -0
  75. package/runtime/codex/bin/codex-install-skills.sh +78 -0
  76. package/runtime/codex/bin/codex-render-spec.mjs +308 -0
  77. package/runtime/codex/bin/codex-session-start.sh +238 -0
  78. package/runtime/codex/bin/codex-skill-render.mjs +315 -0
  79. package/runtime/codex/bin/codex-sync-skills.sh +223 -0
  80. package/runtime/codex/bin/dashboard-data.sh +178 -0
  81. package/runtime/codex/bin/graph-batch.sh +60 -0
  82. package/runtime/codex/bin/graph-op.sh +397 -0
  83. package/runtime/codex/bin/graph-wal.sh +228 -0
  84. package/runtime/codex/bin/graph.sh +141 -0
  85. package/runtime/codex/bin/handoff-pr-backfill.sh +86 -0
  86. package/runtime/codex/bin/handoff-run.sh +569 -0
  87. package/runtime/codex/bin/handoff-save-egregore.sh +131 -0
  88. package/runtime/codex/bin/index-handoff.sh +306 -0
  89. package/runtime/codex/bin/lib/artifact-id.sh +46 -0
  90. package/runtime/codex/bin/lib/config.sh +71 -0
  91. package/runtime/codex/bin/lib/context.sh +511 -0
  92. package/runtime/codex/bin/lib/dashboard-artifact.sh +142 -0
  93. package/runtime/codex/bin/lib/edit-loopback.mjs +652 -0
  94. package/runtime/codex/bin/lib/ensure-develop.sh +183 -0
  95. package/runtime/codex/bin/lib/git-sync.sh +262 -0
  96. package/runtime/codex/bin/lib/greeting.sh +432 -0
  97. package/runtime/codex/bin/lib/hash.sh +13 -0
  98. package/runtime/codex/bin/lib/identity.sh +164 -0
  99. package/runtime/codex/bin/lib/permissions.sh +106 -0
  100. package/runtime/codex/bin/lib/time.sh +56 -0
  101. package/runtime/codex/bin/lib/worktree-links.sh +54 -0
  102. package/runtime/codex/bin/notify.sh +207 -0
  103. package/runtime/codex/bin/publish-artifact.sh +187 -0
  104. package/runtime/codex/bin/publish-references.sh +144 -0
  105. package/runtime/codex/bin/repo-state.sh +99 -0
  106. package/runtime/codex/bin/telemetry.sh +350 -0
  107. package/runtime/codex/bin/worktree-create.sh +115 -0
  108. package/runtime/codex/bin/worktree-remove.sh +50 -0
  109. package/runtime/codex/bin/worktree.sh +199 -0
  110. package/runtime/codex/manifest.json +162 -0
@@ -0,0 +1,106 @@
1
+ # shellcheck shell=bash
2
+ # permissions.sh — Soft-enforced admin-only access for Egregore content.
3
+ #
4
+ # Content can be marked admin-only via frontmatter:
5
+ #
6
+ # ---
7
+ # admin: true
8
+ # ---
9
+ #
10
+ # Admins are listed in egregore.json under "admins": ["github1", "github2", ...].
11
+ #
12
+ # Two enforcement layers:
13
+ #
14
+ # - Read-side (CLI listings, /view rendering) — soft, tag-not-hide:
15
+ # admin entries appear in /activity and /dashboard listings tagged so admins
16
+ # can identify them at a glance. For non-admins the title/topic/branch are
17
+ # replaced with an opaque "[admin]" placeholder — they see admin work
18
+ # exists but the topic doesn't leak. Anyone with git access still bypasses
19
+ # by `cat`-ing the file.
20
+ #
21
+ # - Publish-side (egregore.xyz upload) — strict: publish-artifact.sh refuses
22
+ # to push admin-only files because egregore.xyz URLs are not auth-gated.
23
+ # Override with --allow-admin (set by /view after explicit user
24
+ # confirmation via AskUserQuestion).
25
+
26
+ # Get list of admin GitHub usernames (newline-separated, lowercase)
27
+ _get_admins() {
28
+ local config="${CONFIG:-${SCRIPT_DIR:-$(pwd)}/egregore.json}"
29
+ if [ -f "$config" ]; then
30
+ jq -r '.admins[]? // empty' "$config" 2>/dev/null | tr '[:upper:]' '[:lower:]'
31
+ fi
32
+ }
33
+
34
+ # Returns 0 if username is an admin, 1 otherwise. Empty username = not admin.
35
+ _is_admin() {
36
+ local username="${1:-}"
37
+ [ -z "$username" ] && return 1
38
+ local lc
39
+ lc=$(printf '%s' "$username" | tr '[:upper:]' '[:lower:]')
40
+ _get_admins | grep -qx "$lc"
41
+ }
42
+
43
+ # Returns 0 if the file's frontmatter contains `admin: true`, 1 otherwise.
44
+ # Tolerates whitespace and case in the value. Looks at the first ~60 lines
45
+ # to keep the scan bounded.
46
+ _is_admin_only_file() {
47
+ local path="$1"
48
+ [ -f "$path" ] || return 1
49
+ awk '
50
+ BEGIN { in_fm = 0; found = 0 }
51
+ NR == 1 && /^---[[:space:]]*$/ { in_fm = 1; next }
52
+ in_fm && /^---[[:space:]]*$/ { exit (found ? 0 : 1) }
53
+ in_fm && tolower($0) ~ /^admin:[[:space:]]*true[[:space:]]*$/ { found = 1 }
54
+ NR > 60 { exit (found ? 0 : 1) }
55
+ END { exit (found ? 0 : 1) }
56
+ ' "$path" 2>/dev/null
57
+ }
58
+
59
+ # Returns 0 if the file's frontmatter has the `admin:` key set at all (true or
60
+ # false), 1 otherwise. Used by the admin-content-detector hook to skip files
61
+ # whose admin status was already decided — including an explicit `admin: false`
62
+ # meaning "reviewed, public on purpose". Without this, the hook would re-prompt
63
+ # on every edit of a sensitive-keyword file the user has already cleared.
64
+ _has_admin_decision() {
65
+ local path="$1"
66
+ [ -f "$path" ] || return 1
67
+ awk '
68
+ BEGIN { in_fm = 0; found = 0 }
69
+ NR == 1 && /^---[[:space:]]*$/ { in_fm = 1; next }
70
+ in_fm && /^---[[:space:]]*$/ { exit (found ? 0 : 1) }
71
+ in_fm && tolower($0) ~ /^admin:[[:space:]]*(true|false)[[:space:]]*$/ { found = 1 }
72
+ NR > 60 { exit (found ? 0 : 1) }
73
+ END { exit (found ? 0 : 1) }
74
+ ' "$path" 2>/dev/null
75
+ }
76
+
77
+ # Resolve current user's GitHub username (lowercase). Falls back through:
78
+ # $1 → .egregore-state.json → git user.name
79
+ _current_user() {
80
+ local explicit="${1:-}"
81
+ if [ -n "$explicit" ]; then
82
+ printf '%s' "$explicit" | tr '[:upper:]' '[:lower:]'
83
+ return 0
84
+ fi
85
+ local state="${SCRIPT_DIR:-$(pwd)}/.egregore-state.json"
86
+ if [ -f "$state" ]; then
87
+ local u
88
+ u=$(jq -r '.github_username // empty' "$state" 2>/dev/null)
89
+ if [ -n "$u" ]; then
90
+ printf '%s' "$u" | tr '[:upper:]' '[:lower:]'
91
+ return 0
92
+ fi
93
+ fi
94
+ git config user.name 2>/dev/null | tr '[:upper:]' '[:lower:]'
95
+ }
96
+
97
+ # Returns 0 if the file's body contains sensitive keywords (fundraising, legal,
98
+ # personnel, strategic). Used by the admin-content-detector hook to flag files
99
+ # that probably should be admin-only but aren't yet. Whole-word match on a
100
+ # short curated list — easy to extend without retraining intuition.
101
+ _looks_sensitive() {
102
+ local path="$1"
103
+ [ -f "$path" ] || return 1
104
+ # Word-boundary regex (case-insensitive). Keep this list curated, not exhaustive.
105
+ grep -qiE '\b(fundrais(e|ing)|term[ -]?sheet|cap[ -]?table|valuation|runway|investor|VC[s]?|raise|seed[ -]?round|series[ -][a-d]|board[ -]prep|equity[ -]grant|stock[ -]option|RSU|salary|compensation|payroll|severance|PIP|NDA|attorney|legal[ -]hold|lawsuit|acquisition|merger|due[ -]diligence)\b' "$path"
106
+ }
@@ -0,0 +1,56 @@
1
+ #!/bin/bash
2
+ # Cross-platform time helpers — sourced by scripts that need time formatting.
3
+
4
+ # Convert ISO timestamp to epoch seconds (macOS + Linux compatible)
5
+ # Assumes input is UTC (Z or +00:00 suffix, or bare — treated as UTC)
6
+ _iso_to_epoch() {
7
+ local iso="$1"
8
+ local clean
9
+ clean=$(echo "$iso" | sed 's/Z$//; s/+00:00$//; s/\.[0-9]*//')
10
+ # If date-only (no T), append midnight to avoid macOS date -j filling current time
11
+ [[ "$clean" != *T* ]] && clean="${clean}T00:00:00"
12
+ # macOS → date -j, Linux → date -d
13
+ TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%S" "$clean" "+%s" 2>/dev/null || \
14
+ TZ=UTC date -d "$clean" +%s 2>/dev/null || echo "0"
15
+ }
16
+
17
+ # Convert epoch to relative time string (e.g. "3h ago", "yesterday")
18
+ # Uses calendar-day comparison for multi-day ranges (not raw delta)
19
+ _epoch_to_relative() {
20
+ local epoch="$1"
21
+ local now="${2:-$(date +%s)}"
22
+ [ "$epoch" = "0" ] && echo "--" && return
23
+ local delta=$(( now - epoch ))
24
+ if [ "$delta" -lt 60 ]; then echo "just now"
25
+ elif [ "$delta" -lt 3600 ]; then echo "$(( delta / 60 ))m ago"
26
+ elif [ "$delta" -lt 86400 ]; then echo "$(( delta / 3600 ))h ago"
27
+ else
28
+ # Calendar-day based: compare local dates for accurate "yesterday" / "Xd ago"
29
+ local event_date today_date
30
+ event_date=$(date -r "$epoch" +%Y-%m-%d 2>/dev/null || \
31
+ date -d "@$epoch" +%Y-%m-%d 2>/dev/null || echo "")
32
+ today_date=$(date +%Y-%m-%d)
33
+ if [ -z "$event_date" ] || [ "$event_date" = "$today_date" ]; then echo "today"
34
+ else
35
+ local today_e event_e
36
+ today_e=$(date -j -f "%Y-%m-%d" "$today_date" +%s 2>/dev/null || \
37
+ date -d "$today_date" +%s 2>/dev/null || echo "$now")
38
+ event_e=$(date -j -f "%Y-%m-%d" "$event_date" +%s 2>/dev/null || \
39
+ date -d "$event_date" +%s 2>/dev/null || echo "$epoch")
40
+ local days=$(( (today_e - event_e) / 86400 ))
41
+ [ "$days" -lt 1 ] && days=1
42
+ if [ "$days" -eq 1 ]; then echo "yesterday"
43
+ else echo "${days}d ago"
44
+ fi
45
+ fi
46
+ fi
47
+ }
48
+
49
+ # Millisecond timer (for latency measurement)
50
+ _millis() {
51
+ if command -v gdate &>/dev/null; then
52
+ gdate +%s%3N
53
+ else
54
+ python3 -c "import time; print(int(time.time() * 1000))" 2>/dev/null || echo "0"
55
+ fi
56
+ }
@@ -0,0 +1,54 @@
1
+ # shellcheck shell=bash
2
+ # worktree-links.sh - shared-state bootstrap for git worktrees.
3
+ #
4
+ # Claude Code worktrees are normally created through bin/worktree-create.sh,
5
+ # which links memory/, .env, and local state from the main checkout. Portable
6
+ # runtimes such as Codex can land in a plain git worktree, so startup and
7
+ # agent.sh use this helper to self-heal those links before reading context.
8
+
9
+ egregore_main_project_dir() {
10
+ local project_dir="${1:?project dir required}"
11
+ if [ -f "$project_dir/.git" ]; then
12
+ local wt_gitdir
13
+ wt_gitdir=$(sed 's/^gitdir: //' "$project_dir/.git" 2>/dev/null || true)
14
+ if [ -n "$wt_gitdir" ]; then
15
+ cd "$wt_gitdir/../../.." 2>/dev/null && pwd
16
+ return
17
+ fi
18
+ fi
19
+ cd "$project_dir" 2>/dev/null && pwd
20
+ }
21
+
22
+ egregore_link_shared_state() {
23
+ local worktree_dir="${1:?worktree dir required}"
24
+ local main_dir="${2:-}"
25
+ local file src dst resolved
26
+
27
+ [ -f "$worktree_dir/.git" ] || return 0
28
+ [ -n "$main_dir" ] || main_dir="$(egregore_main_project_dir "$worktree_dir")"
29
+ [ -n "$main_dir" ] || return 0
30
+ [ "$main_dir" != "$worktree_dir" ] || return 0
31
+
32
+ for file in memory .env .egregore-state.json .egregore-session-id; do
33
+ src="$main_dir/$file"
34
+ dst="$worktree_dir/$file"
35
+
36
+ if [ "$file" = "memory" ]; then
37
+ if [ -L "$src" ] || [ -d "$src" ]; then
38
+ resolved="$(realpath "$src" 2>/dev/null || true)"
39
+ [ -n "$resolved" ] && [ -d "$resolved" ] || continue
40
+ src="$resolved"
41
+ else
42
+ continue
43
+ fi
44
+ else
45
+ [ -f "$src" ] || continue
46
+ fi
47
+
48
+ if [ -L "$dst" ]; then
49
+ ln -sfn "$src" "$dst" 2>/dev/null || true
50
+ elif [ ! -e "$dst" ]; then
51
+ ln -s "$src" "$dst" 2>/dev/null || true
52
+ fi
53
+ done
54
+ }
@@ -0,0 +1,207 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
5
+ CONFIG="$SCRIPT_DIR/egregore.json"
6
+
7
+ if [ ! -f "$CONFIG" ]; then
8
+ echo "Error: egregore.json not found." >&2
9
+ exit 1
10
+ fi
11
+
12
+ # --- Mode detection ---
13
+ _MODE=$(jq -r '.mode // "connected"' "$CONFIG" 2>/dev/null)
14
+
15
+ # Public relay for local mode group messages.
16
+ # No API key needed — rate-limited, group messages only.
17
+ RELAY_URL="https://egregore-production-55f2.up.railway.app"
18
+
19
+ # Load specific variables from .env if it exists (safe extraction, no arbitrary code execution)
20
+ if [ -f "$SCRIPT_DIR/.env" ]; then
21
+ EGREGORE_API_URL="${EGREGORE_API_URL:-$(grep '^EGREGORE_API_URL=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2- || true)}"
22
+ EGREGORE_API_KEY="${EGREGORE_API_KEY:-$(grep '^EGREGORE_API_KEY=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2- || true)}"
23
+ fi
24
+
25
+ # Check if API mode or direct mode
26
+ # api_url comes from egregore.json (committed, non-secret)
27
+ # api_key comes from .env only (EGREGORE_API_KEY) — never from egregore.json
28
+ API_URL="${EGREGORE_API_URL:-$(jq -r '.api_url // empty' "$CONFIG")}"
29
+ API_KEY="${EGREGORE_API_KEY:-}"
30
+
31
+ if [ -n "$API_URL" ] && [ -n "$API_KEY" ]; then
32
+ # === API MODE: Call Egregore API gateway ===
33
+
34
+ send_to_person() {
35
+ local name="$1"
36
+ local message="$2"
37
+
38
+ local response
39
+ response=$(curl -s -X POST "${API_URL}/api/notify/send" \
40
+ -H "Authorization: Bearer $API_KEY" \
41
+ -H "Content-Type: application/json" \
42
+ -d "$(jq -n --arg to "$name" --arg message "$message" \
43
+ '{to: $to, message: $message}')" \
44
+ --max-time 10)
45
+
46
+ if echo "$response" | jq -e '.status == "sent"' >/dev/null 2>&1; then
47
+ echo "Sent"
48
+ bash "$SCRIPT_DIR/bin/telemetry.sh" emit "notification" \
49
+ '{"type":"send","status":"sent"}' 2>/dev/null &
50
+ else
51
+ local detail
52
+ detail=$(echo "$response" | jq -r '.detail // .status // "unknown error"')
53
+ if echo "$detail" | grep -qi "forbidden\|can't initiate\|bot was blocked"; then
54
+ echo "Failed: DM not available. Tell $name to message the Egregore bot first (/start)."
55
+ else
56
+ echo "Failed: $detail"
57
+ fi
58
+ bash "$SCRIPT_DIR/bin/telemetry.sh" emit "notification" \
59
+ '{"type":"send","status":"failed"}' 2>/dev/null &
60
+ fi
61
+ }
62
+
63
+ send_to_group() {
64
+ local message="$1"
65
+
66
+ local response
67
+ response=$(curl -s -X POST "${API_URL}/api/notify/group" \
68
+ -H "Authorization: Bearer $API_KEY" \
69
+ -H "Content-Type: application/json" \
70
+ -d "$(jq -n --arg message "$message" '{message: $message}')" \
71
+ --max-time 10)
72
+
73
+ if echo "$response" | jq -e '.status == "sent"' >/dev/null 2>&1; then
74
+ echo "Sent"
75
+ bash "$SCRIPT_DIR/bin/telemetry.sh" emit "notification" \
76
+ '{"type":"group","status":"sent"}' 2>/dev/null &
77
+ else
78
+ local detail
79
+ detail=$(echo "$response" | jq -r '.detail // .status // "unknown error"')
80
+ echo "Failed: $detail"
81
+ bash "$SCRIPT_DIR/bin/telemetry.sh" emit "notification" \
82
+ '{"type":"group","status":"failed"}' 2>/dev/null &
83
+ fi
84
+ }
85
+
86
+ test_connection() {
87
+ local response
88
+ response=$(curl -s -X GET "${API_URL}/api/notify/test" \
89
+ -H "Authorization: Bearer $API_KEY" \
90
+ --max-time 10)
91
+
92
+ if echo "$response" | jq -e '.status == "ok"' >/dev/null 2>&1; then
93
+ echo "Telegram connected via Egregore API"
94
+ else
95
+ echo "Failed to connect" >&2
96
+ echo "$response" >&2
97
+ exit 1
98
+ fi
99
+ }
100
+
101
+ elif [ "$_MODE" = "local" ]; then
102
+ # === LOCAL MODE: Group messages via public relay, DMs not available ===
103
+ # Telegram config: prefer .egregore-state.json (symlinked across worktrees),
104
+ # fall back to egregore.json for backward compatibility.
105
+ STATE_FILE="$SCRIPT_DIR/.egregore-state.json"
106
+
107
+ _read_telegram_config() {
108
+ local field="$1"
109
+ local val
110
+ val=$(jq -r ".$field // empty" "$STATE_FILE" 2>/dev/null)
111
+ [ -z "$val" ] && val=$(jq -r ".$field // empty" "$CONFIG" 2>/dev/null)
112
+ echo "$val"
113
+ }
114
+
115
+ send_to_person() {
116
+ echo "DMs require managed hosting. Group message sent instead."
117
+ send_to_group "@${1}: ${2}"
118
+ }
119
+
120
+ send_to_group() {
121
+ local message="$1"
122
+ local chat_id
123
+ chat_id=$(_read_telegram_config telegram_chat_id)
124
+ local group_link
125
+ group_link=$(_read_telegram_config telegram_group_link)
126
+
127
+ if [ -z "$chat_id" ] && [ -z "$group_link" ]; then
128
+ echo '{"status":"offline","reason":"no_telegram_group"}'
129
+ return
130
+ fi
131
+
132
+ local slug
133
+ slug=$(jq -r '.slug // empty' "$CONFIG" 2>/dev/null)
134
+ local org_name
135
+ org_name=$(jq -r '.org_name // empty' "$CONFIG" 2>/dev/null)
136
+
137
+ local response
138
+ response=$(curl -s -X POST "${RELAY_URL}/api/notify/relay" \
139
+ -H "Content-Type: application/json" \
140
+ -d "$(jq -n \
141
+ --arg chat_id "$chat_id" \
142
+ --arg group_link "$group_link" \
143
+ --arg message "$message" \
144
+ --arg slug "$slug" \
145
+ --arg org_name "$org_name" \
146
+ '{chat_id: $chat_id, group_link: $group_link, message: $message, slug: $slug, org_name: $org_name}')" \
147
+ --max-time 10 2>/dev/null)
148
+
149
+ if echo "$response" | jq -e '.status == "sent"' >/dev/null 2>&1; then
150
+ echo "Sent"
151
+ else
152
+ local detail
153
+ detail=$(echo "$response" | jq -r '.detail // .status // "relay unavailable"' 2>/dev/null)
154
+ echo "Failed: $detail"
155
+ fi
156
+ }
157
+
158
+ test_connection() {
159
+ local group_link
160
+ group_link=$(_read_telegram_config telegram_group_link)
161
+ if [ -n "$group_link" ]; then
162
+ echo "Telegram group configured (local mode — group messages via relay)"
163
+ else
164
+ echo "No Telegram group configured. Run /telegram-connect."
165
+ fi
166
+ }
167
+
168
+ else
169
+ # === OFFLINE MODE: No API key, not local — skip notifications ===
170
+ send_to_person() { echo '{"status":"offline","reason":"no_api_key"}'; }
171
+ send_to_group() { echo '{"status":"offline","reason":"no_api_key"}'; }
172
+ test_connection() { echo '{"status":"offline","reason":"no_api_key"}'; }
173
+ fi
174
+
175
+ case "${1:-help}" in
176
+ send)
177
+ recipient="${2:?Usage: notify.sh send <name> <message>}"
178
+ message="${3:?Usage: notify.sh send <name> <message>}"
179
+ result=$(send_to_person "$recipient" "$message")
180
+ if [[ "$result" == Failed* ]]; then
181
+ send_to_group "@${recipient}: ${message}"
182
+ echo "DM failed, sent to group instead. ${result}"
183
+ else
184
+ echo "$result"
185
+ fi
186
+ ;;
187
+ group)
188
+ message="${2:?Usage: notify.sh group <message>}"
189
+ send_to_group "$message"
190
+ ;;
191
+ file)
192
+ echo "File upload not yet supported via API. Use direct mode." >&2
193
+ exit 1
194
+ ;;
195
+ test)
196
+ test_connection
197
+ ;;
198
+ help|*)
199
+ echo "Usage: notify.sh <command>"
200
+ echo ""
201
+ echo "Commands:"
202
+ echo " send <name> <message> Send DM to a person (auto-fallback to group on failure)"
203
+ echo " group <message> Send to the group chat"
204
+ echo " file <path> [caption] Send a file to the group chat"
205
+ echo " test Test connection"
206
+ ;;
207
+ esac
@@ -0,0 +1,187 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # publish-artifact.sh — Generate HTML artifact + upload to API
5
+ # Usage: publish-artifact.sh <type> <file> [--title "..."] [--author "..."] [--description "..."] [--id "..."] [--raw-html] [--no-references]
6
+ #
7
+ # Connected mode (API key present): publishes to /api/artifacts/publish (permanent, org-scoped)
8
+ # OSS mode (no API key): publishes to /api/artifacts/share (ephemeral, 7-day TTL)
9
+ #
10
+ # --id <slug>: stable artifact ID — re-publishing with the same ID upserts the content
11
+ # at the same URL (e.g. egregore.xyz/view/{org}/board). Connected mode only;
12
+ # ignored in OSS mode. Must be 1-50 chars, alphanumeric/hyphen/underscore.
13
+ #
14
+ # --raw-html: Skip the egregore-artifacts render step and upload <file> directly.
15
+ # Use for already-rendered HTML attachments referenced from a handoff.
16
+ #
17
+ # --no-references: Skip the depth-1 auto-publish of backtick-referenced memory files.
18
+ # Set automatically when publish-references.sh invokes us for a child,
19
+ # preventing recursion.
20
+ #
21
+ # Outputs artifact URL on success, exits silently on failure.
22
+ # Designed for fire-and-forget use: `bash bin/publish-artifact.sh handoff file.md &`
23
+
24
+ SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
25
+ CONFIG="$SCRIPT_DIR/egregore.json"
26
+
27
+ # --- Parse arguments ---
28
+ TYPE=""
29
+ FILE=""
30
+ TITLE=""
31
+ AUTHOR=""
32
+ DESCRIPTION=""
33
+ ARTIFACT_ID=""
34
+ RAW_HTML=0
35
+ SKIP_REFERENCES=0
36
+
37
+ while [[ $# -gt 0 ]]; do
38
+ case "$1" in
39
+ --title) TITLE="$2"; shift 2 ;;
40
+ --author) AUTHOR="$2"; shift 2 ;;
41
+ --description) DESCRIPTION="$2"; shift 2 ;;
42
+ --id) ARTIFACT_ID="$2"; shift 2 ;;
43
+ --raw-html) RAW_HTML=1; shift ;;
44
+ --no-references) SKIP_REFERENCES=1; shift ;;
45
+ *)
46
+ if [ -z "$TYPE" ]; then
47
+ TYPE="$1"
48
+ elif [ -z "$FILE" ]; then
49
+ FILE="$1"
50
+ fi
51
+ shift ;;
52
+ esac
53
+ done
54
+
55
+ if [ -z "$TYPE" ] || [ -z "$FILE" ]; then
56
+ echo "Usage: publish-artifact.sh <type> <file> [--title ...] [--author ...] [--description ...] [--id ...]" >&2
57
+ exit 1
58
+ fi
59
+
60
+ # Validate --id format if provided (matches API regex ^[a-zA-Z0-9_-]{1,50}$)
61
+ if [ -n "$ARTIFACT_ID" ] && ! [[ "$ARTIFACT_ID" =~ ^[a-zA-Z0-9_-]{1,50}$ ]]; then
62
+ echo "--id must be 1-50 chars, alphanumeric/hyphen/underscore only" >&2
63
+ exit 1
64
+ fi
65
+
66
+ if [ ! -f "$FILE" ]; then
67
+ echo "File not found: $FILE" >&2
68
+ exit 1
69
+ fi
70
+
71
+ # --- Load config ---
72
+ if [ -f "$SCRIPT_DIR/.env" ]; then
73
+ EGREGORE_API_URL="${EGREGORE_API_URL:-$(grep '^EGREGORE_API_URL=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2- || true)}"
74
+ EGREGORE_API_KEY="${EGREGORE_API_KEY:-$(grep '^EGREGORE_API_KEY=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2- || true)}"
75
+ EGREGORE_USE_PUBLISHED="${EGREGORE_USE_PUBLISHED:-$(grep '^EGREGORE_USE_PUBLISHED=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2- || true)}"
76
+ fi
77
+
78
+ API_URL="${EGREGORE_API_URL:-$(jq -r '.api_url // empty' "$CONFIG" 2>/dev/null)}"
79
+ API_KEY="${EGREGORE_API_KEY:-}"
80
+ ORG_SLUG="$(jq -r '.slug // empty' "$CONFIG" 2>/dev/null)"
81
+ VIEW_BASE="https://egregore.xyz/view"
82
+
83
+ # Resolve "use published renderer" from env first, then egregore.json. The
84
+ # env var lets individuals override per-shell; egregore.json lets an
85
+ # instance (e.g. the upstream dev repo) opt in for every teammate.
86
+ if [ -z "${EGREGORE_USE_PUBLISHED:-}" ]; then
87
+ CONFIG_USE_PUBLISHED="$(jq -r '.prefer_published_artifacts // empty' "$CONFIG" 2>/dev/null)"
88
+ if [ "$CONFIG_USE_PUBLISHED" = "true" ]; then
89
+ EGREGORE_USE_PUBLISHED=1
90
+ fi
91
+ fi
92
+
93
+ # Public relay for OSS share endpoint
94
+ RELAY_URL="https://egregore-production-55f2.up.railway.app"
95
+
96
+ # --- Prepare HTML ---
97
+ TMP_HTML="/tmp/egregore-artifacts/publish-$$.html"
98
+ mkdir -p /tmp/egregore-artifacts
99
+
100
+ if [ "$RAW_HTML" -eq 1 ]; then
101
+ # Upload the file as-is (used for pre-rendered HTML attachments).
102
+ cp "$FILE" "$TMP_HTML" 2>/dev/null || exit 0
103
+ else
104
+ # Pass --org-slug + --view-base so the renderer can emit clickable links
105
+ # for backtick `memory/*.{md,html}` references. Only when connected AND the
106
+ # slug is known — in OSS mode we leave them empty so rendering falls back
107
+ # to plain <code> and we don't produce dead links.
108
+ RENDER_ARGS=("$TYPE" "$FILE" --output "$TMP_HTML")
109
+ if [ -n "$API_KEY" ] && [ -n "$ORG_SLUG" ]; then
110
+ RENDER_ARGS+=(--org-slug "$ORG_SLUG" --view-base "$VIEW_BASE")
111
+ fi
112
+
113
+ # Prefer the in-repo package copy when it's present AND its dependencies
114
+ # have been installed — that way local edits to packages/egregore-artifacts
115
+ # are exercised without waiting for an npm release. Fall back to the
116
+ # published `npx egregore-artifacts` otherwise.
117
+ # Set EGREGORE_USE_PUBLISHED=1 to force the npm-published renderer even
118
+ # when a local install is present — useful in the upstream dev repo to
119
+ # dogfood what teammates and OSS users actually run.
120
+ LOCAL_CLI="$SCRIPT_DIR/packages/egregore-artifacts/bin/cli.js"
121
+ if [ "${EGREGORE_USE_PUBLISHED:-0}" != "1" ] && [ -f "$LOCAL_CLI" ] && [ -d "$SCRIPT_DIR/packages/egregore-artifacts/node_modules/react" ]; then
122
+ if ! node "$LOCAL_CLI" "${RENDER_ARGS[@]}" >/dev/null 2>&1; then
123
+ exit 0
124
+ fi
125
+ else
126
+ if ! npx egregore-artifacts "${RENDER_ARGS[@]}" >/dev/null 2>&1; then
127
+ exit 0
128
+ fi
129
+ fi
130
+ fi
131
+
132
+ if [ ! -f "$TMP_HTML" ]; then
133
+ exit 0
134
+ fi
135
+
136
+ # Default title from filename if not provided
137
+ if [ -z "$TITLE" ]; then
138
+ TITLE="$(basename "$FILE" | sed 's/\.[^.]*$//' | sed 's/[-_]/ /g')"
139
+ fi
140
+
141
+ # --- Upload ---
142
+ RESPONSE=""
143
+
144
+ if [ -n "$API_URL" ] && [ -n "$API_KEY" ]; then
145
+ # Connected mode: permanent, org-scoped
146
+ RESPONSE=$(curl -s -X POST "${API_URL}/api/artifacts/publish" \
147
+ -H "Authorization: Bearer $API_KEY" \
148
+ -F "file=@${TMP_HTML}" \
149
+ -F "artifact_type=${TYPE}" \
150
+ -F "title=${TITLE}" \
151
+ -F "author=${AUTHOR}" \
152
+ -F "description=${DESCRIPTION}" \
153
+ -F "artifact_id=${ARTIFACT_ID}" \
154
+ --max-time 15 2>/dev/null) || true
155
+ else
156
+ # OSS mode: ephemeral, 7-day TTL
157
+ RESPONSE=$(curl -s -X POST "${RELAY_URL}/api/artifacts/share" \
158
+ -F "file=@${TMP_HTML}" \
159
+ -F "artifact_type=${TYPE}" \
160
+ -F "title=${TITLE}" \
161
+ -F "author=${AUTHOR}" \
162
+ -F "description=${DESCRIPTION}" \
163
+ --max-time 15 2>/dev/null) || true
164
+ fi
165
+
166
+ # Clean up
167
+ rm -f "$TMP_HTML"
168
+
169
+ # Output URL if successful
170
+ URL=""
171
+ if [ -n "$RESPONSE" ]; then
172
+ URL=$(echo "$RESPONSE" | jq -r '.url // empty' 2>/dev/null)
173
+ if [ -n "$URL" ]; then
174
+ echo "$URL"
175
+ fi
176
+ fi
177
+
178
+ # Depth-1 auto-publish of backtick-referenced memory files so the clickable
179
+ # links in the rendered parent resolve. Connected mode only; the helper exits
180
+ # silently in OSS mode. Skipped when:
181
+ # - parent publish failed (no URL),
182
+ # - --raw-html (HTML attachments don't embed markdown refs we'd parse),
183
+ # - --no-references (caller explicitly opts out / recursion guard).
184
+ if [ -n "$URL" ] && [ "$RAW_HTML" -eq 0 ] && [ "$SKIP_REFERENCES" -eq 0 ]; then
185
+ bash "$SCRIPT_DIR/bin/publish-references.sh" "$FILE" >/dev/null 2>&1 &
186
+ disown 2>/dev/null || true
187
+ fi