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.
- package/assets/egregore-launcher.js +1965 -0
- package/assets/egregore-launcher.sh +385 -0
- package/bin/cli.js +472 -81
- package/lib/api.js +3 -3
- package/lib/auth.js +255 -20
- package/lib/codex-runtime.js +268 -0
- package/lib/commands.js +31 -0
- package/lib/context.js +1 -1
- package/lib/local.js +618 -193
- package/lib/setup.js +837 -85
- package/lib/ui.js +780 -84
- package/package.json +17 -4
- package/runtime/codex/.codex/config.toml +15 -0
- package/runtime/codex/.codex/hooks/branch-guard.js +234 -0
- package/runtime/codex/.codex/hooks.json +17 -0
- package/runtime/codex/.codex/skills/activity/SKILL.md +70 -0
- package/runtime/codex/.codex/skills/add/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/announce/SKILL.md +54 -0
- package/runtime/codex/.codex/skills/answer/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/archive/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/ask/SKILL.md +59 -0
- package/runtime/codex/.codex/skills/branch/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/checkup/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/comment/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/commit/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/contribute/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/dark-mode/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/dashboard/SKILL.md +59 -0
- package/runtime/codex/.codex/skills/deep-reflect/SKILL.md +70 -0
- package/runtime/codex/.codex/skills/delete-user/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/edit/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/edit-process/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/egregore-voice/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/emissary/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/env/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/handoff/SKILL.md +67 -0
- package/runtime/codex/.codex/skills/harvest/SKILL.md +107 -0
- package/runtime/codex/.codex/skills/infra/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/ingest/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/ingest-google/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/ingest-user-interview/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/invite/SKILL.md +40 -0
- package/runtime/codex/.codex/skills/issue/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/me/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/meeting/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/note/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/onboarding/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/pr/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/project/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/pull/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/push/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/quest/SKILL.md +77 -0
- package/runtime/codex/.codex/skills/quest-suggest/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/reflect/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/review-pr/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/save/SKILL.md +62 -0
- package/runtime/codex/.codex/skills/setup/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/summon/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/sync-repos/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/telegram-connect/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/telemetry/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/test/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/the-spiral/SKILL.md +84 -0
- package/runtime/codex/.codex/skills/todo/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/tui-design/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/tutorial/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/update/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/view/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/visual-explain/SKILL.md +41 -0
- package/runtime/codex/.codex/skills/wrap/SKILL.md +55 -0
- package/runtime/codex/.codex/spec-manifest.json +108 -0
- package/runtime/codex/AGENTS.md +328 -0
- package/runtime/codex/bin/activity-data.sh +180 -0
- package/runtime/codex/bin/agent.sh +669 -0
- package/runtime/codex/bin/codex-install-skills.sh +78 -0
- package/runtime/codex/bin/codex-render-spec.mjs +308 -0
- package/runtime/codex/bin/codex-session-start.sh +238 -0
- package/runtime/codex/bin/codex-skill-render.mjs +315 -0
- package/runtime/codex/bin/codex-sync-skills.sh +223 -0
- package/runtime/codex/bin/dashboard-data.sh +178 -0
- package/runtime/codex/bin/graph-batch.sh +60 -0
- package/runtime/codex/bin/graph-op.sh +397 -0
- package/runtime/codex/bin/graph-wal.sh +228 -0
- package/runtime/codex/bin/graph.sh +141 -0
- package/runtime/codex/bin/handoff-pr-backfill.sh +86 -0
- package/runtime/codex/bin/handoff-run.sh +569 -0
- package/runtime/codex/bin/handoff-save-egregore.sh +131 -0
- package/runtime/codex/bin/index-handoff.sh +306 -0
- package/runtime/codex/bin/lib/artifact-id.sh +46 -0
- package/runtime/codex/bin/lib/config.sh +71 -0
- package/runtime/codex/bin/lib/context.sh +511 -0
- package/runtime/codex/bin/lib/dashboard-artifact.sh +142 -0
- package/runtime/codex/bin/lib/edit-loopback.mjs +652 -0
- package/runtime/codex/bin/lib/ensure-develop.sh +183 -0
- package/runtime/codex/bin/lib/git-sync.sh +262 -0
- package/runtime/codex/bin/lib/greeting.sh +432 -0
- package/runtime/codex/bin/lib/hash.sh +13 -0
- package/runtime/codex/bin/lib/identity.sh +164 -0
- package/runtime/codex/bin/lib/permissions.sh +106 -0
- package/runtime/codex/bin/lib/time.sh +56 -0
- package/runtime/codex/bin/lib/worktree-links.sh +54 -0
- package/runtime/codex/bin/notify.sh +207 -0
- package/runtime/codex/bin/publish-artifact.sh +187 -0
- package/runtime/codex/bin/publish-references.sh +144 -0
- package/runtime/codex/bin/repo-state.sh +99 -0
- package/runtime/codex/bin/telemetry.sh +350 -0
- package/runtime/codex/bin/worktree-create.sh +115 -0
- package/runtime/codex/bin/worktree-remove.sh +50 -0
- package/runtime/codex/bin/worktree.sh +199 -0
- 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
|