invite

SkillDev tools

Invite a GitHub user into this Egregore instance from Codex when the user invokes /invite or $invite, or asks to invite someone without exposing credentials.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the invite skill

What this skill tells your AI

The instructions your AI receives, as published by egregore-labs/egregore in .codex/skills/invite/SKILL.md and read by ahel’s review.

Invite someone to this Egregore. Handles GitHub org invitation + Egregore setup link.

When to invoke

User says: "/invite ", "invite ", "add to the org", or any request to invite a new person to this Egregore. Not this: removing a member → /delete-user.

Arguments: $ARGUMENTS (Required: GitHub username of the person to invite)

Execution rules

Neo4j-first (connected mode only). In connected mode, all queries via bash bin/graph.sh query "...". No MCP. No direct curl to Neo4j. In local mode, Step 1b routes to Step 2L (GitHub API only) — Steps 2–4b (graph + notify) are skipped entirely. Do NOT show any graph-related messaging in local mode (no "Person node created", no "recorded in graph", no Neo4j references). Notifications use the plan → approve → dispatch flow in .claude/context/notification-consent.md (connected mode only). No direct curl to Telegram. Inviting the person is not notification consent. CRITICAL: Suppress raw output. Never show raw JSON to the user. All bin/graph.sh and bin/notify.sh calls MUST capture output in a variable and only show formatted status lines.

CRITICAL: Never expose credentials in tool output.

  • Never read tokens in a separate bash call — always inline.
  • Never pass tokens as visible arguments — read from .env inside the script.
  • All credential handling happens inside a single bash call that only outputs the formatted result.

Step 1: Validate

If $ARGUMENTS is empty, show usage and stop:

Usage: /invite <github-username>

Example: /invite newuser

Step 1b: Local mode detection

Check api_url from egregore.json:

API_URL=$(jq -r '.api_url // empty' egregore.json 2>/dev/null)

If api_url is empty → use LOCAL INVITE FLOW (Step 2L below). Skip Steps 2-4b entirely. If api_url is set → use CONNECTED INVITE FLOW (Step 2 below).


Step 2L: Local mode invite (GitHub API only)

Run ONE bash call with description "Inviting {username} via GitHub":

bash -c '
USERNAME="$1"
GH_TOKEN=$(grep "^GITHUB_TOKEN=" .env | cut -d"=" -f2-)
GITHUB_ORG=$(jq -r ".github_org" egregore.json)
REPO_NAME=$(jq -r ".repo_name // empty" egregore.json)
MEMORY_REPO=$(jq -r ".memory_repo // empty" egregore.json)
MEMORY_NAME=$(basename "$MEMORY_REPO" .git)
REPOS=$(jq -r ".repos[]? // empty" egregore.json)

if [ -z "$GH_TOKEN" ]; then
  echo "ERROR: No GitHub token. Run: bash bin/github-auth.sh"
  exit 1
fi
if [ -z "$REPO_NAME" ]; then
  echo "ERROR: repo_name not set in egregore.json"
  exit 1
fi

# Add collaborator to core repo
CORE=$(curl -s -o /dev/null -w "%{http_code}" -X PUT \
  -H "Authorization: Bearer $GH_TOKEN" \
  -H "Accept: application/vnd.github+json" \
  "https://api.github.com/repos/$GITHUB_ORG/$REPO_NAME/collaborators/$USERNAME" \
  -d "{\"permission\":\"push\"}")

# Add collaborator to memory repo
MEM=$(curl -s -o /dev/null -w "%{http_code}" -X PUT \
  -H "Authorization: Bearer $GH_TOKEN" \
  -H "Accept: application/vnd.github+json" \
  "https://api.github.com/repos/$GITHUB_ORG/$MEMORY_NAME/collaborators/$USERNAME" \
  -d "{\"permission\":\"push\"}")

# Add to managed repos
for REPO in $REPOS; do
  curl -s -o /dev/null -X PUT \
    -H "Authorization: Bearer $GH_TOKEN" \
    -H "Accept: application/vnd.github+json" \
    "https://api.github.com/repos/$GITHUB_ORG/$REPO/collaborators/$USERNAME" \
    -d "{\"permission\":\"push\"}" 2>/dev/null || true
done

echo "{\"core\":\"$CORE\",\"memory\":\"$MEM\",\"username\":\"$USERNAME\",\"org\":\"$GITHUB_ORG\",\"repo\":\"$REPO_NAME\"}"
' -- "$ARGUMENTS"

Create person file in memory:

INVITE_USER="$ARGUMENTS"
INVITER=$(jq -r '.display_name // .github_username' .egregore-state.json 2>/dev/null)
TODAY=$(date -u +%Y-%m-%d)
cat > "memory/people/${INVITE_USER}.md" << EOF
---
name: ${INVITE_USER}
person_id: github-login:$(printf '%s' "$INVITE_USER" | tr '[:upper:]' '[:lower:]')
github: ${INVITE_USER}
invited_by: ${INVITER}
joined: ${TODAY}
---
EOF
cd memory && git add -A && git commit -m "Invite ${INVITE_USER}" && git push 2>/dev/null && cd -

Display result:

If core HTTP status is 201 or 204:

Invited {username} to {org_name}.

  Core repo access:    ✓ added
  Memory repo access:  ✓ added
  Person file:         ✓ created

Tell them to run:

  npx -y create-egregore@latest join {github_org}/{repo_name}

Telegram group link: After the join command, check egregore.json for telegram_group_link. If present, append:

Telegram group: {telegram_group_link}

If core HTTP status is 422: user was already a collaborator — show "already has access."

If core HTTP status is 403 or other error:

Could not add {username} as a collaborator.
You may need org admin permissions. Add them manually:
  https://github.com/{github_org}/{repo_name}/settings/access

After local invite, STOP. Do not proceed to Steps 3-5 (they require the API).


Step 2: Send invite (single call — credentials stay hidden)

Run ONE bash call with description "Sending invite to {username}":

bash -c '
USERNAME="$1"
GH_TOKEN=$(grep "^GITHUB_TOKEN=" .env | cut -d"=" -f2-)
API_KEY=$(grep "^EGREGORE_API_KEY=" .env | cut -d"=" -f2-)
GITHUB_ORG=$(jq -r ".github_org" egregore.json)
API_URL=$(jq -r ".api_url" egregore.json)
ORG_NAME=$(jq -r ".org_name" egregore.json)
REPO_NAME=$(jq -r ".repo_name // empty" egregore.json)
if [ -z "$REPO_NAME" ]; then echo "ERROR: repo_name not set in egregore.json"; exit 1; fi
SLUG=$(jq -r ".slug" egregore.json)

if [ -z "$GH_TOKEN" ]; then
  echo "ERROR: No GitHub token found. Run: bash bin/github-auth.sh"
  exit 1
fi

if [ -z "$API_KEY" ]; then
  echo "ERROR: No Egregore API key found. Check .env for EGREGORE_API_KEY"
  exit 1
fi

RESP=$(curl -s -X POST "$API_URL/api/org/invite" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $API_KEY" \
  -d "{\"github_org\": \"$GITHUB_ORG\", \"github_username\": \"$USERNAME\", \"repo_name\": \"$REPO_NAME\", \"slug\": \"$SLUG\", \"github_token\": \"$GH_TOKEN\"}")

# Output structured JSON for parsing
echo "$RESP" | jq -c "{
  ok: (if .invite_url then true else false end),
  invite_url: .invite_url,
  invite_token: .invite_token,
  github_invite: .github_invite,
  memory_access: .memory_access,
  org_name: \"$ORG_NAME\",
  github_org: \"$GITHUB_ORG\",
  username: \"$USERNAME\",
  error: .detail
}"
' -- "$ARGUMENTS"

Step 3: Display result

Parse the JSON output from Step 2. Never show raw JSON to the user.

Success (ok=true):

Inviting {username} to {org_name}...

  GitHub org invitation: sent
  Memory repo access:    added
  Invite link:           created

Share this link with {username}:

  {invite_url}

They'll authenticate with GitHub, accept the org invite,
and get a one-line install command.

Telegram group link: After the invite link block, check egregore.json for telegram_group_link. If present, append:

Telegram group: {telegram_group_link}

GitHub invite failed (github_invite.status == "collaborator_failed"):

Inviting {username} to {org_name}...

  GitHub org invitation: failed ({reason})
  Invite link:           created

You'll need to invite {username} to the GitHub org manually:
  https://github.com/orgs/{github_org}/people

Then share this link:

  {invite_url}

Already an org member (github_invite.status == "already_org_member"):

Inviting {username} to {org_name}...

  GitHub: {username} is already a member of {github_org} — access via org role
  Invite link:           created

Share this link with {username}:

  {invite_url}

Org invite pending (github_invite.status == "org_invite_pending"):

Inviting {username} to {org_name}...

  GitHub: org invitation already pending for {username} — they need to accept it
  Invite link:           created

Share this link with {username}:

  {invite_url}

API error (ok=false):

Failed to invite {username}: {error}

Step 4: Record in graph, then separately offer notification

Record the invite first. Do not combine this with notification approval.

Create Person node + sync to Supabase:

bash bin/graph.sh query "MERGE (p:Person {github: \$github}) ON CREATE SET p.personId = 'github-login:' + toLower(\$github), p.name = \$github, p.identityStatus = 'invited', p.invited = date(), p.invitedBy = \$inviter RETURN p.name" '{"github": "USERNAME", "inviter": "INVITER"}'

Then sync to Supabase (non-fatal):

API_URL=$(jq -r '.api_url // empty' egregore.json)
API_KEY=$(grep '^EGREGORE_API_KEY=' .env | cut -d'=' -f2-)
curl -sf "${API_URL}/api/user/ensure" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"github_username":"USERNAME"}' \
  --max-time 5 >/dev/null 2>&1 || true

Get the inviter name from git config user.name (derive short handle: lowercase first word).

Check Telegram + notify:

bash bin/graph.sh query "MATCH (p:Person) WHERE p.github = \$username OR p.name = \$username RETURN p.telegramId" '{"username": "USERNAME"}'

If they have a telegramId, prepare the exact invite notification without sending:

PLAN_JSON=$(bash bin/notify.sh plan send "USERNAME" "You've been invited to ORG_NAME on Egregore! Join here: INVITE_URL")

Show the organization, recipient, channel, and exact message in a dedicated Send / Edit / Cancel checkpoint. Dispatch only after the user approves that preview. If no direct destination exists, say so; never fall back to the group.

Step 4b: Provision Coder user (if remote hosting enabled)

Check if the org has remote hosting enabled. Run ONE bash call (fire-and-forget, must not delay response):

bash -c '
API_URL=$(jq -r ".api_url" egregore.json)
API_KEY=$(grep "^EGREGORE_API_KEY=" .env | cut -d"=" -f2-)
ORG_SLUG=$(jq -r ".slug" egregore.json)
USERNAME="$1"

# Check if hosting is enabled for this org
HOSTING=$(curl -sf "${API_URL}/api/hosting/info/${ORG_SLUG}" \
  -H "Authorization: Bearer $(grep "^GITHUB_TOKEN=" .env | cut -d"=" -f2-)" \
  --max-time 5 2>/dev/null || echo "{}")
ENABLED=$(echo "$HOSTING" | jq -r ".hosting_enabled // false")

if [ "$ENABLED" = "true" ]; then
  # Create Coder user (non-fatal, fire-and-forget)
  curl -sf -X POST "${API_URL}/api/hosting/user/${ORG_SLUG}" \
    -H "Authorization: Bearer $API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"username\": \"$USERNAME\"}" \
    --max-time 10 >/dev/null 2>&1 || true
  echo "coder_user_created"
else
  echo "no_hosting"
fi
' -- "USERNAME"

If the output is coder_user_created, include in the summary:

  Coder workspace:     user created

Step 5: Summary line

After all steps complete, show one truthful final status:

  Notification sent via Telegram

or

  Notification not sent — share the link manually

Example

> /invite newuser

Inviting newuser to Acme Org...

  GitHub org invitation: sent
  Memory repo access:    added
  Invite link:           created

Share this link with newuser:

  https://egregore.xyz/join?invite=inv_a1b2c3d4e5f6

They'll authenticate with GitHub, accept the org invite,
and get a one-line install command.

  Notified via Telegram

If already a member

> /invite newuser

newuser is already a member of acme-org.
They can join Egregore directly at: https://egregore.xyz/setup

Rules

  • Only org admins can invite — the API verifies this
  • GitHub invitation + Egregore invite are bundled — one command does both
  • Invite links expire in 7 days — if expired, just run /invite again
  • Memory repo access is granted automatically — invitee gets push access
  • Never expose tokens — all credential reads happen inside bash scripts, never as separate tool calls
  • Always use bin/graph.sh for Neo4j — never MCP
  • Always use the exact consent flow in bin/notify.sh — never construct API calls directly or treat the invite request as notification approval

Signals

GitHub stars
288
Forks
21
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
invite
Source
github.com/egregore-labs/egregore