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,144 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
|
|
4
|
+
# publish-references.sh — Depth-1 auto-publish for referenced memory files.
|
|
5
|
+
#
|
|
6
|
+
# Usage: publish-references.sh <source-markdown-file>
|
|
7
|
+
#
|
|
8
|
+
# Reads the source markdown, extracts backtick-wrapped references to
|
|
9
|
+
# `memory/**/*.{md,html}` paths, and publishes each one in parallel with a
|
|
10
|
+
# deterministic artifact id (see bin/lib/artifact-id.sh). The deterministic id
|
|
11
|
+
# means the rendered parent view can construct a predictable link to each
|
|
12
|
+
# referenced file *before* this script finishes — so even a slight delay
|
|
13
|
+
# produces clickable links that resolve once the referenced publish completes.
|
|
14
|
+
#
|
|
15
|
+
# Connected mode only. Skips silently if EGREGORE_API_KEY is missing, because
|
|
16
|
+
# the OSS relay ignores artifact ids and would assign random slugs.
|
|
17
|
+
#
|
|
18
|
+
# Fire-and-forget: silent on errors, does not block the caller, bounded
|
|
19
|
+
# parallelism via EGREGORE_REF_PUBLISH_PARALLELISM (default 8).
|
|
20
|
+
|
|
21
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
22
|
+
SOURCE_FILE="${1:-}"
|
|
23
|
+
|
|
24
|
+
[ -n "$SOURCE_FILE" ] || exit 0
|
|
25
|
+
[ -f "$SOURCE_FILE" ] || exit 0
|
|
26
|
+
|
|
27
|
+
# --- Connected-mode check ---
|
|
28
|
+
API_KEY="${EGREGORE_API_KEY:-}"
|
|
29
|
+
if [ -z "$API_KEY" ] && [ -f "$SCRIPT_DIR/.env" ]; then
|
|
30
|
+
API_KEY="$(grep '^EGREGORE_API_KEY=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2- || true)"
|
|
31
|
+
fi
|
|
32
|
+
[ -n "$API_KEY" ] || exit 0
|
|
33
|
+
|
|
34
|
+
# --- Helpers ---
|
|
35
|
+
# shellcheck disable=SC1091
|
|
36
|
+
. "$SCRIPT_DIR/bin/lib/artifact-id.sh"
|
|
37
|
+
|
|
38
|
+
PARALLELISM="${EGREGORE_REF_PUBLISH_PARALLELISM:-8}"
|
|
39
|
+
|
|
40
|
+
GIT_ROOT="$(git -C "$(dirname "$SOURCE_FILE")" rev-parse --show-toplevel 2>/dev/null || echo "$SCRIPT_DIR")"
|
|
41
|
+
|
|
42
|
+
# Absolute, symlink-resolved path of the source file. Used below to compare
|
|
43
|
+
# each resolved ref against the source and skip self-references. Using
|
|
44
|
+
# symlink-resolved absolute paths means `memory/` as a symlink into a sibling
|
|
45
|
+
# repo (and macOS's `/var` → `/private/var`) compare correctly — basename
|
|
46
|
+
# comparison alone would falsely skip unrelated files that happen to share
|
|
47
|
+
# a filename (e.g. date-prefixed handoffs, README.md, index.md).
|
|
48
|
+
SOURCE_ABS="$(cd "$(dirname "$SOURCE_FILE")" 2>/dev/null && pwd -P)/$(basename "$SOURCE_FILE")"
|
|
49
|
+
|
|
50
|
+
# Infer artifact type from path — mirrors egregore-artifacts/bin/cli.js inferType.
|
|
51
|
+
infer_type() {
|
|
52
|
+
local fp="$1"
|
|
53
|
+
case "$fp" in
|
|
54
|
+
*/quests/*) echo "quest" ;;
|
|
55
|
+
*/handoffs/*) echo "handoff" ;;
|
|
56
|
+
*) echo "document" ;;
|
|
57
|
+
esac
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# Resolve a backtick-referenced path to a real file on disk.
|
|
61
|
+
resolve_ref() {
|
|
62
|
+
local ref="$1"
|
|
63
|
+
local src_dir
|
|
64
|
+
src_dir="$(dirname "$SOURCE_FILE")"
|
|
65
|
+
# Candidates: source file's repo-root resolution, then git root, then cwd.
|
|
66
|
+
local candidates=(
|
|
67
|
+
"$src_dir/../../$ref"
|
|
68
|
+
"$GIT_ROOT/$ref"
|
|
69
|
+
"$ref"
|
|
70
|
+
)
|
|
71
|
+
local c
|
|
72
|
+
for c in "${candidates[@]}"; do
|
|
73
|
+
if [ -f "$c" ]; then
|
|
74
|
+
printf '%s' "$c"
|
|
75
|
+
return 0
|
|
76
|
+
fi
|
|
77
|
+
done
|
|
78
|
+
return 1
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
publish_one() {
|
|
82
|
+
local ref="$1"
|
|
83
|
+
local resolved
|
|
84
|
+
resolved="$(resolve_ref "$ref")" || return 0
|
|
85
|
+
[ -n "$resolved" ] || return 0
|
|
86
|
+
|
|
87
|
+
# Self-ref guard: skip refs that resolve to the source file itself.
|
|
88
|
+
# Compared as symlink-resolved absolute paths so symlinked memory layouts
|
|
89
|
+
# (and unrelated files with the same basename) are handled correctly.
|
|
90
|
+
local resolved_abs
|
|
91
|
+
resolved_abs="$(cd "$(dirname "$resolved")" 2>/dev/null && pwd -P)/$(basename "$resolved")"
|
|
92
|
+
if [ "$resolved_abs" = "$SOURCE_ABS" ]; then
|
|
93
|
+
return 0
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
local id
|
|
97
|
+
id="$(artifact_id_from_path "$ref")" || return 0
|
|
98
|
+
[ -n "$id" ] || return 0
|
|
99
|
+
|
|
100
|
+
case "$ref" in
|
|
101
|
+
*.md)
|
|
102
|
+
local type
|
|
103
|
+
type="$(infer_type "$ref")"
|
|
104
|
+
bash "$SCRIPT_DIR/bin/publish-artifact.sh" "$type" "$resolved" --id "$id" --no-references >/dev/null 2>&1 || true
|
|
105
|
+
;;
|
|
106
|
+
*.html)
|
|
107
|
+
bash "$SCRIPT_DIR/bin/publish-artifact.sh" document "$resolved" --id "$id" --raw-html --no-references >/dev/null 2>&1 || true
|
|
108
|
+
;;
|
|
109
|
+
esac
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# --- Extract refs, dedupe, publish in parallel ---
|
|
113
|
+
# grep -Eo prints each match on its own line; sed strips the surrounding
|
|
114
|
+
# backticks; sort -u dedupes. No dependency on GNU grep -P.
|
|
115
|
+
REFS="$(grep -Eo '`memory/[^`[:space:]]+\.(md|html)`' "$SOURCE_FILE" 2>/dev/null \
|
|
116
|
+
| sed 's/^`//; s/`$//' \
|
|
117
|
+
| sort -u)"
|
|
118
|
+
|
|
119
|
+
[ -n "$REFS" ] || exit 0
|
|
120
|
+
|
|
121
|
+
# Bounded parallelism — cap concurrent jobs at PARALLELISM.
|
|
122
|
+
# On bash ≥ 4 (`wait -n`) this is rolling parallelism. On bash 3.2 (macOS
|
|
123
|
+
# default /bin/bash), `wait -n` is unsupported and the `|| wait` fallback
|
|
124
|
+
# drains the whole batch before spawning the next — effectively batch-parallel
|
|
125
|
+
# of size PARALLELISM. Correct in both cases, just slower on bash 3.2.
|
|
126
|
+
active=0
|
|
127
|
+
while IFS= read -r ref; do
|
|
128
|
+
[ -n "$ref" ] || continue
|
|
129
|
+
# Self-reference filtering happens inside publish_one, after the ref is
|
|
130
|
+
# resolved to an absolute path — see the SOURCE_ABS check there.
|
|
131
|
+
publish_one "$ref" &
|
|
132
|
+
active=$((active + 1))
|
|
133
|
+
if [ "$active" -ge "$PARALLELISM" ]; then
|
|
134
|
+
if wait -n 2>/dev/null; then
|
|
135
|
+
active=$((active - 1))
|
|
136
|
+
else
|
|
137
|
+
wait
|
|
138
|
+
active=0
|
|
139
|
+
fi
|
|
140
|
+
fi
|
|
141
|
+
done <<< "$REFS"
|
|
142
|
+
|
|
143
|
+
wait 2>/dev/null || true
|
|
144
|
+
exit 0
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# repo-state.sh — Capture current branch state across all repos.
|
|
3
|
+
# Outputs a markdown table (## Repo State) for handoff files.
|
|
4
|
+
# Only includes repos on non-base branches or with uncommitted changes.
|
|
5
|
+
#
|
|
6
|
+
# Usage: bash bin/repo-state.sh [--no-pr]
|
|
7
|
+
# --no-pr Skip the `gh pr list` lookup per repo (~400–600ms each).
|
|
8
|
+
# PR column will be `—`. Useful on the hot path where a detached
|
|
9
|
+
# backfill will fill PR numbers in later.
|
|
10
|
+
# Outputs the markdown section to stdout. Empty output = nothing to report.
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
NO_PR=0
|
|
14
|
+
while [ $# -gt 0 ]; do
|
|
15
|
+
case "$1" in
|
|
16
|
+
--no-pr) NO_PR=1; shift ;;
|
|
17
|
+
*) shift ;;
|
|
18
|
+
esac
|
|
19
|
+
done
|
|
20
|
+
|
|
21
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
22
|
+
CONFIG="$SCRIPT_DIR/egregore.json"
|
|
23
|
+
PARENT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
24
|
+
|
|
25
|
+
# Source config helpers for _get_base_branch
|
|
26
|
+
source "$SCRIPT_DIR/bin/lib/config.sh" 2>/dev/null || true
|
|
27
|
+
|
|
28
|
+
CORE_REPO=$(jq -r '.repo_name // "egregore"' "$CONFIG" 2>/dev/null)
|
|
29
|
+
GITHUB_ORG=$(jq -r '.github_org // empty' "$CONFIG" 2>/dev/null)
|
|
30
|
+
MANAGED_REPOS=$(jq -r '(.repos[]? // empty) | if type == "object" then .name else . end' "$CONFIG" 2>/dev/null)
|
|
31
|
+
|
|
32
|
+
ROWS=""
|
|
33
|
+
|
|
34
|
+
check_repo() {
|
|
35
|
+
local repo_name="$1"
|
|
36
|
+
local repo_dir="$2"
|
|
37
|
+
local base_branch="$3"
|
|
38
|
+
|
|
39
|
+
[ -d "$repo_dir/.git" ] || [ -f "$repo_dir/.git" ] || return 0
|
|
40
|
+
|
|
41
|
+
local branch
|
|
42
|
+
branch=$(git -C "$repo_dir" branch --show-current 2>/dev/null || echo "")
|
|
43
|
+
[ -z "$branch" ] && return 0
|
|
44
|
+
|
|
45
|
+
local dirty=""
|
|
46
|
+
[ -n "$(git -C "$repo_dir" status --porcelain 2>/dev/null | head -1)" ] && dirty="yes"
|
|
47
|
+
|
|
48
|
+
# Skip if on base branch with no changes
|
|
49
|
+
if [ "$branch" = "$base_branch" ] && [ -z "$dirty" ]; then
|
|
50
|
+
return 0
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# Count commits ahead of base
|
|
54
|
+
local ahead=0
|
|
55
|
+
ahead=$(git -C "$repo_dir" rev-list "origin/${base_branch}..HEAD" --count 2>/dev/null || echo "0")
|
|
56
|
+
|
|
57
|
+
# Skip if on base branch, no dirty files, no commits ahead
|
|
58
|
+
if [ "$branch" = "$base_branch" ] && [ -z "$dirty" ] && [ "$ahead" = "0" ]; then
|
|
59
|
+
return 0
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# Check for open PR (skipped with --no-pr; a detached backfill fills it in)
|
|
63
|
+
local pr="—"
|
|
64
|
+
if [ "$NO_PR" = "0" ] && command -v gh &>/dev/null && [ -n "$GITHUB_ORG" ]; then
|
|
65
|
+
local pr_num
|
|
66
|
+
pr_num=$(gh pr list --repo "$GITHUB_ORG/$repo_name" --head "$branch" --json number -q '.[0].number' 2>/dev/null || true)
|
|
67
|
+
[ -n "$pr_num" ] && pr="#$pr_num"
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
ROWS="${ROWS}| ${repo_name} | ${branch} | ${pr} | ${base_branch} |\n"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Check core repo
|
|
74
|
+
CORE_BASE="develop"
|
|
75
|
+
if [ -f "$CONFIG" ]; then
|
|
76
|
+
# Core repo base: check if develop exists, otherwise main
|
|
77
|
+
if git -C "$SCRIPT_DIR" show-ref --verify --quiet refs/remotes/origin/develop 2>/dev/null; then
|
|
78
|
+
CORE_BASE="develop"
|
|
79
|
+
else
|
|
80
|
+
CORE_BASE="main"
|
|
81
|
+
fi
|
|
82
|
+
fi
|
|
83
|
+
check_repo "$CORE_REPO" "$SCRIPT_DIR" "$CORE_BASE"
|
|
84
|
+
|
|
85
|
+
# Check managed repos
|
|
86
|
+
for REPO in $MANAGED_REPOS; do
|
|
87
|
+
REPO_DIR="$PARENT_DIR/$REPO"
|
|
88
|
+
BASE=$(_get_base_branch "$REPO" 2>/dev/null || echo "develop")
|
|
89
|
+
check_repo "$REPO" "$REPO_DIR" "$BASE"
|
|
90
|
+
done
|
|
91
|
+
|
|
92
|
+
# Output
|
|
93
|
+
if [ -n "$ROWS" ]; then
|
|
94
|
+
echo "## Repo State"
|
|
95
|
+
echo ""
|
|
96
|
+
echo "| Repo | Branch | PR | Base |"
|
|
97
|
+
echo "|------|--------|----|------|"
|
|
98
|
+
printf '%b' "$ROWS"
|
|
99
|
+
fi
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Egregore telemetry — privacy-respecting product analytics.
|
|
3
|
+
# Mirrors bin/graph.sh and bin/notify.sh patterns.
|
|
4
|
+
#
|
|
5
|
+
# Subcommands:
|
|
6
|
+
# emit <type> <json> Append event to local JSONL buffer (no network)
|
|
7
|
+
# flush Send buffered events to API and clear buffer
|
|
8
|
+
# status Show telemetry status (enabled/disabled, buffer size)
|
|
9
|
+
# enable Enable telemetry
|
|
10
|
+
# disable Disable telemetry
|
|
11
|
+
# clear Clear local buffer
|
|
12
|
+
# show [n] Show last N events from buffer (default 10)
|
|
13
|
+
#
|
|
14
|
+
# Consent: opt-out via EGREGORE_NO_TELEMETRY=1, DO_NOT_TRACK=1,
|
|
15
|
+
# or .egregore-state.json → "telemetry": false
|
|
16
|
+
#
|
|
17
|
+
# Never collected: file paths, file contents, code, env var values,
|
|
18
|
+
# conversation content, command arguments with user content.
|
|
19
|
+
# Only collected: command names, timestamps, durations, error codes, branch names.
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
|
|
22
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
23
|
+
BUFFER_DIR="$HOME/.egregore"
|
|
24
|
+
BUFFER_FILE="$BUFFER_DIR/telemetry.jsonl"
|
|
25
|
+
STATE_FILE="$SCRIPT_DIR/.egregore-state.json"
|
|
26
|
+
CONFIG="$SCRIPT_DIR/egregore.json"
|
|
27
|
+
ENV_FILE="$SCRIPT_DIR/.env"
|
|
28
|
+
MAX_BUFFER_BYTES=1048576 # 1MB
|
|
29
|
+
MAX_EVENTS_AFTER_TRUNCATE=500
|
|
30
|
+
LOCK_DIR="$BUFFER_DIR/telemetry.lock"
|
|
31
|
+
# Public relay for local/OSS egregores without API keys
|
|
32
|
+
TELEMETRY_RELAY_URL="https://egregore-production-55f2.up.railway.app"
|
|
33
|
+
|
|
34
|
+
# --- Cross-platform file locking via mkdir (atomic on all POSIX systems) ---
|
|
35
|
+
_lock() {
|
|
36
|
+
local attempts=0
|
|
37
|
+
while ! mkdir "$LOCK_DIR" 2>/dev/null; do
|
|
38
|
+
attempts=$((attempts + 1))
|
|
39
|
+
if [ "$attempts" -gt 50 ]; then
|
|
40
|
+
local holder_pid
|
|
41
|
+
holder_pid=$(cat "$LOCK_DIR/pid" 2>/dev/null || echo "")
|
|
42
|
+
if [ -n "$holder_pid" ] && ! kill -0 "$holder_pid" 2>/dev/null; then
|
|
43
|
+
rm -rf "$LOCK_DIR" 2>/dev/null
|
|
44
|
+
continue
|
|
45
|
+
fi
|
|
46
|
+
return 1
|
|
47
|
+
fi
|
|
48
|
+
sleep 0.1
|
|
49
|
+
done
|
|
50
|
+
echo $$ > "$LOCK_DIR/pid" 2>/dev/null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_unlock() {
|
|
54
|
+
rm -rf "$LOCK_DIR" 2>/dev/null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
trap '_unlock' EXIT
|
|
58
|
+
|
|
59
|
+
# --- Consent check (fast path) ---
|
|
60
|
+
_check_consent() {
|
|
61
|
+
# Environment variable opt-outs (fastest check)
|
|
62
|
+
[ "${EGREGORE_NO_TELEMETRY:-}" = "1" ] && return 1
|
|
63
|
+
[ "${DO_NOT_TRACK:-}" = "1" ] && return 1
|
|
64
|
+
|
|
65
|
+
# State file opt-out
|
|
66
|
+
if [ -f "$STATE_FILE" ]; then
|
|
67
|
+
local val
|
|
68
|
+
val=$(jq -r '.telemetry // "true"' "$STATE_FILE" 2>/dev/null || echo "true")
|
|
69
|
+
[ "$val" = "false" ] && return 1
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
return 0
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# --- Debug mode ---
|
|
76
|
+
_is_debug() {
|
|
77
|
+
[ "${EGREGORE_TELEMETRY_DEBUG:-}" = "1" ]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# --- Resolve identity (from env vars first, fallback to config files) ---
|
|
81
|
+
_resolve_user() {
|
|
82
|
+
if [ -n "${EGREGORE_USER:-}" ]; then
|
|
83
|
+
echo "$EGREGORE_USER"
|
|
84
|
+
elif [ -f "$STATE_FILE" ]; then
|
|
85
|
+
jq -r '.github_username // .name // "unknown"' "$STATE_FILE" 2>/dev/null || echo "unknown"
|
|
86
|
+
else
|
|
87
|
+
echo "unknown"
|
|
88
|
+
fi
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
_resolve_org() {
|
|
92
|
+
if [ -n "${EGREGORE_ORG:-}" ]; then
|
|
93
|
+
echo "$EGREGORE_ORG"
|
|
94
|
+
elif [ -f "$CONFIG" ]; then
|
|
95
|
+
jq -r '.slug // .github_org // "unknown"' "$CONFIG" 2>/dev/null || echo "unknown"
|
|
96
|
+
else
|
|
97
|
+
echo "unknown"
|
|
98
|
+
fi
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
_resolve_session_id() {
|
|
102
|
+
if [ -n "${EGREGORE_SESSION_ID:-}" ]; then
|
|
103
|
+
echo "$EGREGORE_SESSION_ID"
|
|
104
|
+
return
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
# Fallback: read from file written by session-start.sh
|
|
108
|
+
# (env vars from hooks don't propagate into the Claude Code agent)
|
|
109
|
+
# Use git common dir so worktrees hash to the same path as the main checkout
|
|
110
|
+
local repo_root
|
|
111
|
+
repo_root=$(git -C "$SCRIPT_DIR" rev-parse --path-format=absolute --git-common-dir 2>/dev/null | sed 's|/\.git$||') || repo_root="$SCRIPT_DIR"
|
|
112
|
+
local proj_hash
|
|
113
|
+
proj_hash=$(echo -n "$repo_root" | md5 2>/dev/null || echo -n "$repo_root" | md5sum 2>/dev/null | cut -d' ' -f1)
|
|
114
|
+
local sid_file="$HOME/.egregore/session-${proj_hash}.id"
|
|
115
|
+
if [ -f "$sid_file" ]; then
|
|
116
|
+
cat "$sid_file" 2>/dev/null || echo "unknown"
|
|
117
|
+
else
|
|
118
|
+
echo "unknown"
|
|
119
|
+
fi
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# --- emit: append event to local JSONL buffer ---
|
|
123
|
+
cmd_emit() {
|
|
124
|
+
local event_type="${1:?Usage: telemetry.sh emit <type> <json>}"
|
|
125
|
+
local data="${2:-"{}"}"
|
|
126
|
+
|
|
127
|
+
# Consent gate
|
|
128
|
+
if ! _check_consent; then
|
|
129
|
+
return 0
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
# Debug mode: print to stderr instead of writing
|
|
133
|
+
if _is_debug; then
|
|
134
|
+
local ts
|
|
135
|
+
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
136
|
+
local compact_data
|
|
137
|
+
compact_data=$(printf '%s' "$data" | tr -d '\n' | tr -s ' ')
|
|
138
|
+
printf '{"ts":"%s","type":"%s","sid":"%s","org":"%s","user":"%s","data":%s}\n' \
|
|
139
|
+
"$ts" "$event_type" "$(_resolve_session_id)" "$(_resolve_org)" "$(_resolve_user)" "$compact_data" >&2
|
|
140
|
+
return 0
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
# Ensure buffer directory exists
|
|
144
|
+
mkdir -p "$BUFFER_DIR"
|
|
145
|
+
|
|
146
|
+
# Build event line (outside lock for minimal hold time)
|
|
147
|
+
local ts
|
|
148
|
+
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
149
|
+
# Compact data to single line — callers may pass pretty-printed JSON
|
|
150
|
+
local compact_data
|
|
151
|
+
compact_data=$(printf '%s' "$data" | tr -d '\n' | tr -s ' ')
|
|
152
|
+
local line
|
|
153
|
+
line=$(printf '{"ts":"%s","type":"%s","sid":"%s","org":"%s","user":"%s","data":%s}' \
|
|
154
|
+
"$ts" "$event_type" "$(_resolve_session_id)" "$(_resolve_org)" "$(_resolve_user)" "$compact_data")
|
|
155
|
+
|
|
156
|
+
# Append under lock
|
|
157
|
+
_lock || { echo "$line" >> "$BUFFER_FILE" 2>/dev/null; return 0; }
|
|
158
|
+
echo "$line" >> "$BUFFER_FILE"
|
|
159
|
+
|
|
160
|
+
# Buffer guard: if file exceeds max size, truncate to last N events
|
|
161
|
+
if [ -f "$BUFFER_FILE" ]; then
|
|
162
|
+
local size
|
|
163
|
+
size=$(wc -c < "$BUFFER_FILE" 2>/dev/null | tr -d ' ')
|
|
164
|
+
if [ "$size" -gt "$MAX_BUFFER_BYTES" ] 2>/dev/null; then
|
|
165
|
+
local tmp="$BUFFER_FILE.truncate.$$"
|
|
166
|
+
tail -n "$MAX_EVENTS_AFTER_TRUNCATE" "$BUFFER_FILE" > "$tmp" 2>/dev/null \
|
|
167
|
+
&& mv "$tmp" "$BUFFER_FILE" \
|
|
168
|
+
|| rm -f "$tmp"
|
|
169
|
+
fi
|
|
170
|
+
fi
|
|
171
|
+
_unlock
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# --- flush: send buffered events to API ---
|
|
175
|
+
cmd_flush() {
|
|
176
|
+
if [ ! -f "$BUFFER_FILE" ] || [ ! -s "$BUFFER_FILE" ]; then
|
|
177
|
+
return 0
|
|
178
|
+
fi
|
|
179
|
+
|
|
180
|
+
# Read API config
|
|
181
|
+
local api_url=""
|
|
182
|
+
local api_key=""
|
|
183
|
+
local endpoint=""
|
|
184
|
+
local auth_header=""
|
|
185
|
+
if [ -f "$CONFIG" ]; then
|
|
186
|
+
api_url=$(jq -r '.api_url // empty' "$CONFIG" 2>/dev/null || true)
|
|
187
|
+
fi
|
|
188
|
+
if [ -f "$ENV_FILE" ]; then
|
|
189
|
+
api_key=$(grep '^EGREGORE_API_KEY=' "$ENV_FILE" 2>/dev/null | cut -d'=' -f2- || true)
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
if [ -n "$api_url" ] && [ -n "$api_key" ]; then
|
|
193
|
+
# Connected mode: authenticated ingest
|
|
194
|
+
endpoint="${api_url}/api/telemetry/ingest"
|
|
195
|
+
auth_header="Authorization: Bearer $api_key"
|
|
196
|
+
else
|
|
197
|
+
# Local/OSS mode: public relay (no auth needed)
|
|
198
|
+
endpoint="${TELEMETRY_RELAY_URL}/api/telemetry/relay"
|
|
199
|
+
auth_header=""
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
# Atomic rotate under lock: move buffer so new events can keep writing
|
|
203
|
+
_lock || return 0
|
|
204
|
+
local flush_file="$BUFFER_FILE.flush.$$"
|
|
205
|
+
mv "$BUFFER_FILE" "$flush_file" 2>/dev/null || { _unlock; return 0; }
|
|
206
|
+
_unlock
|
|
207
|
+
|
|
208
|
+
# POST to API
|
|
209
|
+
local response
|
|
210
|
+
local http_code
|
|
211
|
+
local curl_args=(-s -o /dev/null -w '%{http_code}' -X POST "$endpoint"
|
|
212
|
+
-H "Content-Type: application/x-ndjson"
|
|
213
|
+
--data-binary "@$flush_file"
|
|
214
|
+
--max-time 15)
|
|
215
|
+
if [ -n "$auth_header" ]; then
|
|
216
|
+
curl_args+=(-H "$auth_header")
|
|
217
|
+
fi
|
|
218
|
+
http_code=$(curl "${curl_args[@]}" 2>/dev/null || echo "000")
|
|
219
|
+
|
|
220
|
+
if [ "$http_code" -ge 200 ] 2>/dev/null && [ "$http_code" -lt 300 ] 2>/dev/null; then
|
|
221
|
+
# Success — remove flush file
|
|
222
|
+
rm -f "$flush_file"
|
|
223
|
+
else
|
|
224
|
+
# Failure — move events back so they aren't lost (under lock to prevent races)
|
|
225
|
+
_lock || { mv "$flush_file" "$BUFFER_FILE" 2>/dev/null || true; return 1; }
|
|
226
|
+
if [ -f "$BUFFER_FILE" ]; then
|
|
227
|
+
# New events were written while we were flushing; prepend old events
|
|
228
|
+
cat "$flush_file" "$BUFFER_FILE" > "$BUFFER_FILE.merge.$$" 2>/dev/null \
|
|
229
|
+
&& mv "$BUFFER_FILE.merge.$$" "$BUFFER_FILE" \
|
|
230
|
+
&& rm -f "$flush_file" \
|
|
231
|
+
|| rm -f "$BUFFER_FILE.merge.$$"
|
|
232
|
+
else
|
|
233
|
+
mv "$flush_file" "$BUFFER_FILE" 2>/dev/null || true
|
|
234
|
+
fi
|
|
235
|
+
_unlock
|
|
236
|
+
fi
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
# --- status: show telemetry status ---
|
|
240
|
+
cmd_status() {
|
|
241
|
+
echo "Telemetry status:"
|
|
242
|
+
echo ""
|
|
243
|
+
|
|
244
|
+
# Consent
|
|
245
|
+
if _check_consent; then
|
|
246
|
+
echo " Enabled: yes"
|
|
247
|
+
else
|
|
248
|
+
echo " Enabled: no"
|
|
249
|
+
if [ "${EGREGORE_NO_TELEMETRY:-}" = "1" ]; then
|
|
250
|
+
echo " Reason: EGREGORE_NO_TELEMETRY=1"
|
|
251
|
+
elif [ "${DO_NOT_TRACK:-}" = "1" ]; then
|
|
252
|
+
echo " Reason: DO_NOT_TRACK=1"
|
|
253
|
+
else
|
|
254
|
+
echo " Reason: telemetry=false in .egregore-state.json"
|
|
255
|
+
fi
|
|
256
|
+
fi
|
|
257
|
+
|
|
258
|
+
# Buffer
|
|
259
|
+
if [ -f "$BUFFER_FILE" ]; then
|
|
260
|
+
local count size
|
|
261
|
+
count=$(grep -c '^{' "$BUFFER_FILE" 2>/dev/null || echo 0)
|
|
262
|
+
size=$(wc -c < "$BUFFER_FILE" 2>/dev/null | tr -d ' ')
|
|
263
|
+
echo " Buffer: $count events ($size bytes)"
|
|
264
|
+
echo " Path: $BUFFER_FILE"
|
|
265
|
+
else
|
|
266
|
+
echo " Buffer: empty"
|
|
267
|
+
fi
|
|
268
|
+
|
|
269
|
+
echo ""
|
|
270
|
+
echo " Opt-out: set EGREGORE_NO_TELEMETRY=1 in .env, or run /telemetry off"
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
# --- enable: turn telemetry on ---
|
|
274
|
+
cmd_enable() {
|
|
275
|
+
if [ -f "$STATE_FILE" ]; then
|
|
276
|
+
jq '.telemetry = true' "$STATE_FILE" > "$STATE_FILE.tmp" \
|
|
277
|
+
&& mv "$STATE_FILE.tmp" "$STATE_FILE"
|
|
278
|
+
fi
|
|
279
|
+
echo "Telemetry enabled."
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
# --- disable: turn telemetry off ---
|
|
283
|
+
cmd_disable() {
|
|
284
|
+
if [ -f "$STATE_FILE" ]; then
|
|
285
|
+
jq '.telemetry = false' "$STATE_FILE" > "$STATE_FILE.tmp" \
|
|
286
|
+
&& mv "$STATE_FILE.tmp" "$STATE_FILE"
|
|
287
|
+
else
|
|
288
|
+
echo '{"telemetry": false}' > "$STATE_FILE"
|
|
289
|
+
fi
|
|
290
|
+
echo "Telemetry disabled."
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
# --- clear: delete local buffer ---
|
|
294
|
+
cmd_clear() {
|
|
295
|
+
rm -f "$BUFFER_FILE"
|
|
296
|
+
echo "Telemetry buffer cleared."
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
# --- show: display last N events ---
|
|
300
|
+
cmd_show() {
|
|
301
|
+
local n="${1:-10}"
|
|
302
|
+
if [ ! -f "$BUFFER_FILE" ] || [ ! -s "$BUFFER_FILE" ]; then
|
|
303
|
+
echo "No buffered events."
|
|
304
|
+
return 0
|
|
305
|
+
fi
|
|
306
|
+
echo "Last $n events:"
|
|
307
|
+
echo ""
|
|
308
|
+
# Extract JSON objects (handles both single-line and legacy multi-line format)
|
|
309
|
+
grep '^{' "$BUFFER_FILE" | tail -n "$n" | jq -c . 2>/dev/null \
|
|
310
|
+
|| grep '^{' "$BUFFER_FILE" | tail -n "$n"
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
# --- Main dispatch ---
|
|
314
|
+
case "${1:-status}" in
|
|
315
|
+
emit)
|
|
316
|
+
shift
|
|
317
|
+
cmd_emit "$@"
|
|
318
|
+
;;
|
|
319
|
+
flush)
|
|
320
|
+
cmd_flush
|
|
321
|
+
;;
|
|
322
|
+
status)
|
|
323
|
+
cmd_status
|
|
324
|
+
;;
|
|
325
|
+
enable)
|
|
326
|
+
cmd_enable
|
|
327
|
+
;;
|
|
328
|
+
disable)
|
|
329
|
+
cmd_disable
|
|
330
|
+
;;
|
|
331
|
+
clear)
|
|
332
|
+
cmd_clear
|
|
333
|
+
;;
|
|
334
|
+
show)
|
|
335
|
+
shift
|
|
336
|
+
cmd_show "${1:-10}"
|
|
337
|
+
;;
|
|
338
|
+
help|*)
|
|
339
|
+
echo "Usage: telemetry.sh <command>"
|
|
340
|
+
echo ""
|
|
341
|
+
echo "Commands:"
|
|
342
|
+
echo " emit <type> <json> Append event to local buffer (no network)"
|
|
343
|
+
echo " flush Send buffered events to API"
|
|
344
|
+
echo " status Show telemetry status"
|
|
345
|
+
echo " enable Enable telemetry"
|
|
346
|
+
echo " disable Disable telemetry"
|
|
347
|
+
echo " clear Clear local buffer"
|
|
348
|
+
echo " show [n] Show last N events (default 10)"
|
|
349
|
+
;;
|
|
350
|
+
esac
|