@onlooker-community/ecosystem 0.4.0 → 0.6.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.
@@ -0,0 +1,5 @@
1
+ {
2
+ "enabledPlugins": {
3
+ "ecosystem@onlooker-community": true
4
+ }
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "enabledPlugins": {
3
+ "ecosystem@onlooker-community": true
4
+ }
5
+ }
@@ -77,30 +77,10 @@ jobs:
77
77
  echo "ERROR: package.json version ($PKG_VERSION) does not match tag ($TAG_VERSION)"
78
78
  exit 1
79
79
  fi
80
-
81
- test:
82
- name: Test
83
- runs-on: ubuntu-latest
84
- strategy:
85
- matrix:
86
- node: ['20', '22']
87
- steps:
88
- - uses: actions/checkout@v4
89
-
90
- - uses: actions/setup-node@v4
91
- with:
92
- node-version: ${{ matrix.node }}
93
- cache: npm
94
-
95
- - run: npm ci
96
- - run: npm run build
97
- - run: npm test
98
- - run: npm run typecheck
99
-
100
80
  publish:
101
81
  name: Publish to npm
102
82
  runs-on: ubuntu-latest
103
- needs: [validate, test]
83
+ needs: [validate]
104
84
  environment: npm-publish
105
85
  steps:
106
86
  - uses: actions/checkout@v4
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.4.0"
2
+ ".": "0.6.0"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.0](https://github.com/onlooker-community/ecosystem/compare/v0.5.0...v0.6.0) (2026-05-22)
4
+
5
+
6
+ ### Features
7
+
8
+ * add settings.json for plugin configuration ([67fbdfe](https://github.com/onlooker-community/ecosystem/commit/67fbdfe37f067a45801e7d0355c4a533b687f6b2))
9
+
10
+ ## [0.5.0](https://github.com/onlooker-community/ecosystem/compare/v0.4.0...v0.5.0) (2026-05-22)
11
+
12
+
13
+ ### Features
14
+
15
+ * **hooks:** add UserPromptSubmit turn and session duration trackers ([#12](https://github.com/onlooker-community/ecosystem/issues/12)) ([cbb7657](https://github.com/onlooker-community/ecosystem/commit/cbb7657979ed144efce506e6b487e037679b9462))
16
+
3
17
  ## [0.4.0](https://github.com/onlooker-community/ecosystem/compare/v0.3.3...v0.4.0) (2026-05-22)
4
18
 
5
19
 
package/hooks/hooks.json CHANGED
@@ -40,6 +40,20 @@
40
40
  ]
41
41
  }
42
42
  ],
43
+ "UserPromptSubmit": [
44
+ {
45
+ "hooks": [
46
+ {
47
+ "type": "command",
48
+ "command": "\"$CLAUDE_PLUGIN_ROOT\"/scripts/hooks/turn-tracker.sh"
49
+ },
50
+ {
51
+ "type": "command",
52
+ "command": "\"$CLAUDE_PLUGIN_ROOT\"/scripts/hooks/session-duration-tracker.sh"
53
+ }
54
+ ]
55
+ }
56
+ ],
43
57
  "PostToolUse": [
44
58
  {
45
59
  "matcher": "*",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlooker-community/ecosystem",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Agents, skills, hooks, commands, rules, and MCP configurations that power [Onlooker](https://onlooker.dev)",
5
5
  "author": {
6
6
  "name": "Onlooker Community",
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env bash
2
+ # Onlooker Session Duration Tracker
3
+ # Invoked by UserPromptSubmit when the user submits a prompt.
4
+ #
5
+ # Updates session_duration_ms on the tracker and injects turn + elapsed time
6
+ # into Claude's context via UserPromptSubmit additionalContext.
7
+ #
8
+ # Usage:
9
+ # echo "$INPUT" | session-duration-tracker.sh
10
+
11
+ set -uo pipefail # No -e: never block prompt submission
12
+
13
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ source "$SCRIPT_DIR/../lib/validate-path.sh"
15
+ source "$SCRIPT_DIR/../lib/session-tracker.sh"
16
+
17
+ hook_register "session-duration-tracker" "Session Duration Tracker" "Surfaces session elapsed time on each user prompt"
18
+
19
+ INPUT=$(cat)
20
+ hook_set_context "$INPUT" "UserPromptSubmit"
21
+
22
+ SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
23
+
24
+ session_tracker_update_duration "$SESSION_ID" || hook_failure "Failed to update session duration"
25
+
26
+ CONTEXT=$(session_tracker_build_duration_context "$SESSION_ID")
27
+ if [[ -n "$CONTEXT" ]]; then
28
+ jq -n \
29
+ --arg ctx "$CONTEXT" \
30
+ '{
31
+ hookSpecificOutput: {
32
+ hookEventName: "UserPromptSubmit",
33
+ additionalContext: $ctx
34
+ }
35
+ }'
36
+ fi
37
+
38
+ hook_success
39
+ exit 0
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bash
2
+ # Onlooker Turn Tracker
3
+ # Invoked by UserPromptSubmit when the user submits a prompt.
4
+ #
5
+ # Advances per-session turn_number (first prompt stays at turn 1) and emits
6
+ # canonical session.prompt for telemetry.
7
+ #
8
+ # Usage:
9
+ # echo "$INPUT" | turn-tracker.sh
10
+
11
+ set -uo pipefail # No -e: never block prompt submission
12
+
13
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ source "$SCRIPT_DIR/../lib/validate-path.sh"
15
+ source "$SCRIPT_DIR/../lib/onlooker-schema.sh"
16
+ source "$SCRIPT_DIR/../lib/tool-history.sh"
17
+ source "$SCRIPT_DIR/../lib/session-tracker.sh"
18
+ source "$SCRIPT_DIR/../lib/turn-tracker.sh"
19
+
20
+ hook_register "turn-tracker" "Turn Tracker" "Tracks conversation turns and emits session.prompt"
21
+
22
+ INPUT=$(cat)
23
+ hook_set_context "$INPUT" "UserPromptSubmit"
24
+
25
+ SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
26
+ PROMPT=$(echo "$INPUT" | jq -r '.prompt // ""')
27
+
28
+ turn_tracker_on_user_prompt "$SESSION_ID" || hook_failure "Failed to advance turn state"
29
+ turn_state_export "$SESSION_ID"
30
+
31
+ PAYLOAD=$(turn_tracker_build_prompt_payload "$SESSION_ID" "$PROMPT")
32
+ if [[ -n "$PAYLOAD" ]]; then
33
+ session_tracker_emit "$SESSION_ID" "session.prompt" "$PAYLOAD" \
34
+ || hook_failure "Failed to emit session.prompt"
35
+ fi
36
+
37
+ hook_success
38
+ exit 0
@@ -142,6 +142,92 @@ session_tracker_build_end_payload() {
142
142
  }'
143
143
  }
144
144
 
145
+ # Elapsed session time in milliseconds (0 when start_time_ms is unset).
146
+ # Usage: duration_ms=$(session_tracker_duration_ms "$SESSION_ID")
147
+ session_tracker_duration_ms() {
148
+ local session_id="${1:-}"
149
+ [[ -z "$session_id" || "$session_id" == "null" ]] && echo 0 && return 0
150
+
151
+ local tracker_file="$ONLOOKER_SESSION_TRACKERS_DIR/$session_id"
152
+ local now_ms start_ms duration_ms
153
+ now_ms=$(session_tracker_now_ms)
154
+
155
+ if [[ ! -f "$tracker_file" ]]; then
156
+ echo 0
157
+ return 0
158
+ fi
159
+
160
+ start_ms=$(jq -r '.start_time_ms // 0' "$tracker_file" 2>/dev/null) || start_ms=0
161
+ if [[ ! "$start_ms" =~ ^[0-9]+$ ]] || (( start_ms <= 0 )); then
162
+ echo 0
163
+ return 0
164
+ fi
165
+
166
+ duration_ms=$((now_ms - start_ms))
167
+ (( duration_ms < 0 )) && duration_ms=0
168
+ echo "$duration_ms"
169
+ }
170
+
171
+ # Human-readable duration for hook context (e.g. "12m 34s", "45s").
172
+ # Usage: label=$(session_tracker_format_duration 754000)
173
+ session_tracker_format_duration() {
174
+ local duration_ms="${1:-0}"
175
+ [[ ! "$duration_ms" =~ ^[0-9]+$ ]] && duration_ms=0
176
+
177
+ local total_sec=$((duration_ms / 1000))
178
+ local hours=$((total_sec / 3600))
179
+ local minutes=$(((total_sec % 3600) / 60))
180
+ local seconds=$((total_sec % 60))
181
+
182
+ if (( hours > 0 )); then
183
+ printf '%dh %dm' "$hours" "$minutes"
184
+ elif (( minutes > 0 )); then
185
+ printf '%dm %ds' "$minutes" "$seconds"
186
+ else
187
+ printf '%ds' "$seconds"
188
+ fi
189
+ }
190
+
191
+ # Persist session_duration_ms on the per-session tracker file.
192
+ # Usage: session_tracker_update_duration "$SESSION_ID"
193
+ session_tracker_update_duration() {
194
+ local session_id="${1:-}"
195
+ [[ -z "$session_id" || "$session_id" == "null" ]] && return 0
196
+
197
+ turn_state_ensure_session "$session_id" || return 1
198
+
199
+ local tracker_file="$ONLOOKER_SESSION_TRACKERS_DIR/$session_id"
200
+ local duration_ms
201
+ duration_ms=$(session_tracker_duration_ms "$session_id")
202
+
203
+ local temp_file
204
+ temp_file=$(mktemp)
205
+ if ! jq --argjson duration_ms "$duration_ms" \
206
+ '.session_duration_ms = $duration_ms' \
207
+ "$tracker_file" >"$temp_file" 2>/dev/null; then
208
+ rm -f "$temp_file"
209
+ return 1
210
+ fi
211
+ mv "$temp_file" "$tracker_file"
212
+ }
213
+
214
+ # Build additionalContext line for UserPromptSubmit (turn + elapsed time).
215
+ # Usage: context=$(session_tracker_build_duration_context "$SESSION_ID")
216
+ session_tracker_build_duration_context() {
217
+ local session_id="${1:-}"
218
+ local duration_ms turn_number elapsed
219
+ duration_ms=$(session_tracker_duration_ms "$session_id")
220
+ elapsed=$(session_tracker_format_duration "$duration_ms")
221
+
222
+ if [[ -f "$ONLOOKER_SESSION_TRACKERS_DIR/$session_id" ]]; then
223
+ turn_number=$(jq -r '.turn_number // 1' "$ONLOOKER_SESSION_TRACKERS_DIR/$session_id" 2>/dev/null) || turn_number=1
224
+ else
225
+ turn_number=1
226
+ fi
227
+
228
+ printf 'Onlooker session: turn %s · elapsed %s' "$turn_number" "$elapsed"
229
+ }
230
+
145
231
  # Emit a validated canonical session event and append to logs.
146
232
  # Usage: session_tracker_emit "$SESSION_ID" "session.start" "$payload_json"
147
233
  session_tracker_emit() {
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env bash
2
+ # Turn tracking helpers for UserPromptSubmit hooks.
3
+ #
4
+ # Source after validate-path.sh, onlooker-schema.sh, tool-history.sh, session-tracker.sh:
5
+ # source "$CLAUDE_PLUGIN_ROOT/scripts/lib/turn-tracker.sh"
6
+
7
+ # Truncate prompt text for session.prompt input_summary.
8
+ # Usage: summary=$(turn_tracker_summarize_prompt "$PROMPT")
9
+ turn_tracker_summarize_prompt() {
10
+ local prompt="${1:-}"
11
+ [[ -z "$prompt" ]] && return 0
12
+
13
+ local summary
14
+ summary=$(printf '%s' "$prompt" | tr '\n' ' ' | sed 's/ */ /g' | sed 's/^ //;s/ $//')
15
+ if ((${#summary} > 200)); then
16
+ summary="${summary:0:200}…"
17
+ fi
18
+ printf '%s' "$summary"
19
+ }
20
+
21
+ # Advance turn state when the user submits a prompt (first prompt stays at turn 1).
22
+ # Usage: turn_tracker_on_user_prompt "$SESSION_ID"
23
+ turn_tracker_on_user_prompt() {
24
+ local session_id="${1:-}"
25
+ [[ -z "$session_id" || "$session_id" == "null" ]] && return 0
26
+
27
+ turn_state_ensure_session "$session_id" || return 1
28
+
29
+ local tracker_file="$ONLOOKER_SESSION_TRACKERS_DIR/$session_id"
30
+ local seen
31
+ seen=$(jq -r '.user_prompts_seen // false' "$tracker_file" 2>/dev/null) || seen="false"
32
+
33
+ if [[ "$seen" == "true" ]]; then
34
+ turn_state_next_turn "$session_id" || return 1
35
+ else
36
+ local temp_file
37
+ temp_file=$(mktemp)
38
+ if ! jq '.user_prompts_seen = true | .turn_tool_seq = 0' \
39
+ "$tracker_file" >"$temp_file" 2>/dev/null; then
40
+ rm -f "$temp_file"
41
+ return 1
42
+ fi
43
+ mv "$temp_file" "$tracker_file"
44
+ fi
45
+ }
46
+
47
+ # Build session.prompt payload for the current turn.
48
+ # Usage: payload=$(turn_tracker_build_prompt_payload "$SESSION_ID" "$PROMPT")
49
+ turn_tracker_build_prompt_payload() {
50
+ local session_id="${1:-}"
51
+ local prompt="${2:-}"
52
+ [[ -z "$session_id" || "$session_id" == "null" ]] && return 1
53
+
54
+ local turn_number summary
55
+ if [[ -f "$ONLOOKER_SESSION_TRACKERS_DIR/$session_id" ]]; then
56
+ turn_number=$(jq -r '.turn_number // 1' "$ONLOOKER_SESSION_TRACKERS_DIR/$session_id" 2>/dev/null) || turn_number=1
57
+ else
58
+ turn_number=1
59
+ fi
60
+
61
+ summary=$(turn_tracker_summarize_prompt "$prompt")
62
+
63
+ jq -n \
64
+ --argjson turn_number "$turn_number" \
65
+ --arg summary "$summary" \
66
+ '{turn_number: $turn_number}
67
+ + (if $summary != "" then {input_summary: $summary} else {} end)'
68
+ }
@@ -84,6 +84,24 @@ setup_file() {
84
84
  [[ "$hook_cmd" == *tool-history-tracker.sh ]]
85
85
  }
86
86
 
87
+ @test "hooks.json UserPromptSubmit references turn and session-duration trackers" {
88
+ run jq -e '.hooks.UserPromptSubmit[0].hooks | length == 2' "${REPO_ROOT}/hooks/hooks.json"
89
+ [ "$status" -eq 0 ]
90
+
91
+ local turn_cmd duration_cmd
92
+ turn_cmd=$(jq -r '.hooks.UserPromptSubmit[0].hooks[0].command' "${REPO_ROOT}/hooks/hooks.json")
93
+ duration_cmd=$(jq -r '.hooks.UserPromptSubmit[0].hooks[1].command' "${REPO_ROOT}/hooks/hooks.json")
94
+ [[ "$turn_cmd" == *turn-tracker.sh ]]
95
+ [[ "$duration_cmd" == *session-duration-tracker.sh ]]
96
+
97
+ for hook_cmd in "$turn_cmd" "$duration_cmd"; do
98
+ local script_path="${hook_cmd//\$CLAUDE_PLUGIN_ROOT/$REPO_ROOT}"
99
+ script_path="${script_path//\"/}"
100
+ run test -x "$script_path"
101
+ [ "$status" -eq 0 ]
102
+ done
103
+ }
104
+
87
105
  @test "hooks.json SessionStart references session-start-tracker" {
88
106
  run jq -e '.hooks.SessionStart[0].matcher == "*"' "${REPO_ROOT}/hooks/hooks.json"
89
107
  [ "$status" -eq 0 ]
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env bats
2
+
3
+ setup() {
4
+ # shellcheck source=../helpers/setup.bash
5
+ source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
6
+ load_validate_path
7
+ export CLAUDE_PLUGIN_ROOT="${REPO_ROOT}"
8
+ source "${REPO_ROOT}/scripts/lib/onlooker-schema.sh"
9
+ source "${REPO_ROOT}/scripts/lib/tool-history.sh"
10
+ source "${REPO_ROOT}/scripts/lib/session-tracker.sh"
11
+ source "${REPO_ROOT}/scripts/lib/turn-tracker.sh"
12
+ }
13
+
14
+ @test "turn-tracker emits session.prompt on first user prompt at turn 1" {
15
+ local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/user-prompt-submit.json"
16
+ local session_id="prompt-session-001"
17
+ local tracker="${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
18
+ local history_file="${ONLOOKER_SESSION_HISTORY_DIR}/${session_id}.jsonl"
19
+ rm -f "$tracker" "$history_file"
20
+
21
+ turn_state_ensure_session "$session_id"
22
+ jq '.start_time_ms = 1000' "$tracker" >"${tracker}.tmp" && mv "${tracker}.tmp" "$tracker"
23
+
24
+ run bash -c "cat '${fixture}' | '${REPO_ROOT}/scripts/hooks/turn-tracker.sh' 2>/dev/null"
25
+ [ "$status" -eq 0 ]
26
+
27
+ jq -e '.turn_number == 1 and .user_prompts_seen == true' "$tracker" >/dev/null
28
+ jq -e '.event_type == "session.prompt"
29
+ and .payload.turn_number == 1
30
+ and (.payload.input_summary | contains("UserPromptSubmit"))' \
31
+ "$history_file" >/dev/null
32
+ }
33
+
34
+ @test "turn-tracker increments turn on second user prompt" {
35
+ local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/user-prompt-submit-turn2.json"
36
+ local session_id="prompt-session-002"
37
+ local tracker="${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
38
+ rm -f "$tracker"
39
+
40
+ turn_state_ensure_session "$session_id"
41
+ jq '.user_prompts_seen = true | .turn_number = 1' "$tracker" >"${tracker}.tmp" && mv "${tracker}.tmp" "$tracker"
42
+
43
+ cat "$fixture" | "${REPO_ROOT}/scripts/hooks/turn-tracker.sh" >/dev/null 2>&1
44
+
45
+ jq -e '.turn_number == 2 and .turn_tool_seq == 0' "$tracker" >/dev/null
46
+ }
47
+
48
+ @test "session-duration-tracker outputs additionalContext with elapsed time" {
49
+ local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/user-prompt-submit.json"
50
+ local session_id="prompt-session-001"
51
+ local tracker="${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
52
+ rm -f "$tracker"
53
+
54
+ turn_state_ensure_session "$session_id"
55
+ local past_ms
56
+ past_ms=$(python3 -c 'import time; print(int((time.time() - 65) * 1000))' 2>/dev/null || echo 0)
57
+ jq --argjson start_ms "$past_ms" \
58
+ '.start_time_ms = $start_ms | .turn_number = 2' \
59
+ "$tracker" >"${tracker}.tmp" && mv "${tracker}.tmp" "$tracker"
60
+
61
+ run bash -c "cat '${fixture}' | '${REPO_ROOT}/scripts/hooks/session-duration-tracker.sh' 2>/dev/null"
62
+ [ "$status" -eq 0 ]
63
+
64
+ echo "$output" | jq -e \
65
+ '.hookSpecificOutput.hookEventName == "UserPromptSubmit"
66
+ and (.hookSpecificOutput.additionalContext | contains("turn 2"))
67
+ and (.hookSpecificOutput.additionalContext | contains("elapsed"))' >/dev/null
68
+
69
+ jq -e '(.session_duration_ms | type) == "number" and .session_duration_ms >= 60000' "$tracker" >/dev/null
70
+ }
71
+
72
+ @test "session_tracker_format_duration renders minutes and seconds" {
73
+ [ "$(session_tracker_format_duration 65000)" = "1m 5s" ]
74
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "session_id": "prompt-session-002",
3
+ "transcript_path": "/tmp/transcript.jsonl",
4
+ "cwd": "/project/repo",
5
+ "permission_mode": "default",
6
+ "hook_event_name": "UserPromptSubmit",
7
+ "prompt": "Follow-up prompt for turn increment test"
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "session_id": "prompt-session-001",
3
+ "transcript_path": "/tmp/transcript.jsonl",
4
+ "cwd": "/project/repo",
5
+ "permission_mode": "default",
6
+ "hook_event_name": "UserPromptSubmit",
7
+ "prompt": "Add turn tracking hooks for UserPromptSubmit"
8
+ }