create-egregore 0.17.4 → 0.18.1

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 (106) 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 +2 -2
  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 +616 -189
  10. package/lib/setup.js +822 -84
  11. package/lib/ui.js +780 -84
  12. package/package.json +12 -3
  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/commit/SKILL.md +25 -0
  25. package/runtime/codex/.codex/skills/contribute/SKILL.md +25 -0
  26. package/runtime/codex/.codex/skills/dark-mode/SKILL.md +25 -0
  27. package/runtime/codex/.codex/skills/dashboard/SKILL.md +59 -0
  28. package/runtime/codex/.codex/skills/deep-reflect/SKILL.md +70 -0
  29. package/runtime/codex/.codex/skills/delete-user/SKILL.md +25 -0
  30. package/runtime/codex/.codex/skills/egregore-voice/SKILL.md +25 -0
  31. package/runtime/codex/.codex/skills/emissary/SKILL.md +41 -0
  32. package/runtime/codex/.codex/skills/env/SKILL.md +25 -0
  33. package/runtime/codex/.codex/skills/handoff/SKILL.md +67 -0
  34. package/runtime/codex/.codex/skills/harvest/SKILL.md +107 -0
  35. package/runtime/codex/.codex/skills/infra/SKILL.md +41 -0
  36. package/runtime/codex/.codex/skills/ingest/SKILL.md +25 -0
  37. package/runtime/codex/.codex/skills/ingest-google/SKILL.md +25 -0
  38. package/runtime/codex/.codex/skills/ingest-user-interview/SKILL.md +41 -0
  39. package/runtime/codex/.codex/skills/invite/SKILL.md +40 -0
  40. package/runtime/codex/.codex/skills/issue/SKILL.md +41 -0
  41. package/runtime/codex/.codex/skills/me/SKILL.md +25 -0
  42. package/runtime/codex/.codex/skills/meeting/SKILL.md +41 -0
  43. package/runtime/codex/.codex/skills/note/SKILL.md +25 -0
  44. package/runtime/codex/.codex/skills/onboarding/SKILL.md +41 -0
  45. package/runtime/codex/.codex/skills/pr/SKILL.md +25 -0
  46. package/runtime/codex/.codex/skills/project/SKILL.md +41 -0
  47. package/runtime/codex/.codex/skills/pull/SKILL.md +25 -0
  48. package/runtime/codex/.codex/skills/push/SKILL.md +25 -0
  49. package/runtime/codex/.codex/skills/quest/SKILL.md +77 -0
  50. package/runtime/codex/.codex/skills/quest-suggest/SKILL.md +25 -0
  51. package/runtime/codex/.codex/skills/reflect/SKILL.md +41 -0
  52. package/runtime/codex/.codex/skills/review-pr/SKILL.md +41 -0
  53. package/runtime/codex/.codex/skills/save/SKILL.md +62 -0
  54. package/runtime/codex/.codex/skills/setup/SKILL.md +25 -0
  55. package/runtime/codex/.codex/skills/summon/SKILL.md +41 -0
  56. package/runtime/codex/.codex/skills/sync-repos/SKILL.md +25 -0
  57. package/runtime/codex/.codex/skills/telegram-connect/SKILL.md +25 -0
  58. package/runtime/codex/.codex/skills/telemetry/SKILL.md +25 -0
  59. package/runtime/codex/.codex/skills/test/SKILL.md +41 -0
  60. package/runtime/codex/.codex/skills/the-spiral/SKILL.md +84 -0
  61. package/runtime/codex/.codex/skills/todo/SKILL.md +41 -0
  62. package/runtime/codex/.codex/skills/tui-design/SKILL.md +41 -0
  63. package/runtime/codex/.codex/skills/tutorial/SKILL.md +41 -0
  64. package/runtime/codex/.codex/skills/update/SKILL.md +25 -0
  65. package/runtime/codex/.codex/skills/view/SKILL.md +41 -0
  66. package/runtime/codex/.codex/skills/visual-explain/SKILL.md +41 -0
  67. package/runtime/codex/.codex/skills/wrap/SKILL.md +55 -0
  68. package/runtime/codex/.codex/spec-manifest.json +108 -0
  69. package/runtime/codex/AGENTS.md +40 -0
  70. package/runtime/codex/bin/activity-data.sh +177 -0
  71. package/runtime/codex/bin/agent.sh +669 -0
  72. package/runtime/codex/bin/codex-install-skills.sh +78 -0
  73. package/runtime/codex/bin/codex-render-spec.mjs +308 -0
  74. package/runtime/codex/bin/codex-session-start.sh +238 -0
  75. package/runtime/codex/bin/codex-skill-render.mjs +315 -0
  76. package/runtime/codex/bin/codex-sync-skills.sh +223 -0
  77. package/runtime/codex/bin/dashboard-data.sh +174 -0
  78. package/runtime/codex/bin/graph-batch.sh +60 -0
  79. package/runtime/codex/bin/graph-op.sh +380 -0
  80. package/runtime/codex/bin/graph-wal.sh +228 -0
  81. package/runtime/codex/bin/graph.sh +141 -0
  82. package/runtime/codex/bin/handoff-pr-backfill.sh +86 -0
  83. package/runtime/codex/bin/handoff-run.sh +431 -0
  84. package/runtime/codex/bin/handoff-save-egregore.sh +131 -0
  85. package/runtime/codex/bin/index-handoff.sh +306 -0
  86. package/runtime/codex/bin/lib/artifact-id.sh +46 -0
  87. package/runtime/codex/bin/lib/config.sh +71 -0
  88. package/runtime/codex/bin/lib/context.sh +463 -0
  89. package/runtime/codex/bin/lib/dashboard-artifact.sh +142 -0
  90. package/runtime/codex/bin/lib/ensure-develop.sh +183 -0
  91. package/runtime/codex/bin/lib/git-sync.sh +262 -0
  92. package/runtime/codex/bin/lib/greeting.sh +414 -0
  93. package/runtime/codex/bin/lib/hash.sh +13 -0
  94. package/runtime/codex/bin/lib/identity.sh +164 -0
  95. package/runtime/codex/bin/lib/permissions.sh +106 -0
  96. package/runtime/codex/bin/lib/time.sh +56 -0
  97. package/runtime/codex/bin/lib/worktree-links.sh +54 -0
  98. package/runtime/codex/bin/notify.sh +207 -0
  99. package/runtime/codex/bin/publish-artifact.sh +173 -0
  100. package/runtime/codex/bin/publish-references.sh +144 -0
  101. package/runtime/codex/bin/repo-state.sh +99 -0
  102. package/runtime/codex/bin/telemetry.sh +350 -0
  103. package/runtime/codex/bin/worktree-create.sh +118 -0
  104. package/runtime/codex/bin/worktree-remove.sh +50 -0
  105. package/runtime/codex/bin/worktree.sh +198 -0
  106. package/runtime/codex/manifest.json +155 -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