@onlooker-community/ecosystem 0.2.1 → 0.3.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.
@@ -12,7 +12,7 @@
12
12
  "name": "ecosystem",
13
13
  "source": "./",
14
14
  "description": "Fill this out",
15
- "version": "0.2.1",
15
+ "version": "0.3.1",
16
16
  "author": {
17
17
  "name": "Onlooker Community"
18
18
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ecosystem",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "TODO fill this out",
5
5
  "author": {
6
6
  "name": "Onlooker Community",
@@ -12,6 +12,7 @@ permissions:
12
12
  contents: write
13
13
  pull-requests: write
14
14
  issues: write
15
+ id-token: write
15
16
 
16
17
  jobs:
17
18
  release-please:
@@ -48,7 +49,8 @@ jobs:
48
49
  - name: Publish to npm
49
50
  if: ${{ steps.release.outputs.releases_created == 'true' }}
50
51
  env:
51
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
52
+ NPM_CONFIG_PROVENANCE: true
52
53
  run: |
54
+ npm install -g npm@latest
53
55
  npm ci
54
- npm publish --access public
56
+ npm publish --access=public
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.2.1"
2
+ ".": "0.3.1"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.1](https://github.com/onlooker-community/ecosystem/compare/v0.3.0...v0.3.1) (2026-05-22)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **ci:** use HTTPS repository URL for npm provenance ([a7e8927](https://github.com/onlooker-community/ecosystem/commit/a7e89275c5a025a8afee009853265b717091f6ca))
9
+
10
+ ## [0.3.0](https://github.com/onlooker-community/ecosystem/compare/v0.2.1...v0.3.0) (2026-05-21)
11
+
12
+
13
+ ### Features
14
+
15
+ * **hooks:** track skill usage via skill.invoked events ([23fff0f](https://github.com/onlooker-community/ecosystem/commit/23fff0f0bfad8ab91788d8c45a0457d099d2e870))
16
+
17
+
18
+ ### Chores
19
+
20
+ * update GitHub Actions permissions to include id-token ([ca18e61](https://github.com/onlooker-community/ecosystem/commit/ca18e61571b173d1aa6e69cf9031d2daaae1ff72))
21
+ * update npm publish configuration in release workflow ([261fa2d](https://github.com/onlooker-community/ecosystem/commit/261fa2d5c9d656ce74f52193be615b860bc78075))
22
+
3
23
  ## [0.2.1](https://github.com/onlooker-community/ecosystem/compare/v0.2.0...v0.2.1) (2026-05-21)
4
24
 
5
25
 
package/hooks/hooks.json CHANGED
@@ -18,6 +18,26 @@
18
18
  "command": "\"$CLAUDE_PLUGIN_ROOT\"/scripts/hooks/agent-spawn-tracker.sh"
19
19
  }
20
20
  ]
21
+ },
22
+ {
23
+ "matcher": "Skill",
24
+ "hooks": [
25
+ {
26
+ "type": "command",
27
+ "command": "\"$CLAUDE_PLUGIN_ROOT\"/scripts/hooks/skill-usage-tracker.sh"
28
+ }
29
+ ]
30
+ }
31
+ ],
32
+ "UserPromptExpansion": [
33
+ {
34
+ "matcher": "",
35
+ "hooks": [
36
+ {
37
+ "type": "command",
38
+ "command": "\"$CLAUDE_PLUGIN_ROOT\"/scripts/hooks/skill-usage-tracker.sh"
39
+ }
40
+ ]
21
41
  }
22
42
  ],
23
43
  "PostToolUse": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlooker-community/ecosystem",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
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",
@@ -9,7 +9,7 @@
9
9
  "license": "MIT",
10
10
  "repository": {
11
11
  "type": "git",
12
- "url": "git:https://github.com/onlooker-community/ecosystem.git"
12
+ "url": "https://github.com/onlooker-community/ecosystem"
13
13
  },
14
14
  "homepage": "https://github.com/onlooker-community/ecosystem#readme",
15
15
  "bugs": {
@@ -19,7 +19,7 @@
19
19
  "onlooker-install": "install.sh"
20
20
  },
21
21
  "dependencies": {
22
- "@onlooker-community/schema": "^1.3.0"
22
+ "@onlooker-community/schema": "^1.4.0"
23
23
  },
24
24
  "scripts": {
25
25
  "postinstall": "echo '\\n onlooker-ecosystem installed!\\n Run: npx onlooker-install typescript\\n Docs: https://github.com/onlooker-community/ecosystem\\n'",
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env bash
2
+ # Onlooker Skill Usage Tracker
3
+ # Invoked by UserPromptExpansion (slash commands) and PreToolUse (matcher: Skill).
4
+ #
5
+ # Records canonical skill.invoked events to:
6
+ # ~/.onlooker/session-history/<session_id>.jsonl
7
+ # ~/.onlooker/logs/onlooker-events.jsonl
8
+ #
9
+ # Usage:
10
+ # echo "$INPUT" | skill-usage-tracker.sh
11
+
12
+ set -uo pipefail # No -e: never block skill invocation
13
+
14
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
+ source "$SCRIPT_DIR/../lib/validate-path.sh"
16
+ source "$SCRIPT_DIR/../lib/onlooker-schema.sh"
17
+ source "$SCRIPT_DIR/../lib/tool-history.sh"
18
+ source "$SCRIPT_DIR/../lib/skill-usage.sh"
19
+
20
+ hook_register "skill-usage-tracker" "Skill Usage Tracker" "Records skill.invoked canonical events"
21
+
22
+ INPUT=$(cat)
23
+
24
+ HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // "UserPromptExpansion"')
25
+ hook_set_context "$INPUT" "$HOOK_EVENT"
26
+
27
+ SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
28
+ turn_state_export "$SESSION_ID"
29
+
30
+ # PreToolUse Skill hooks must approve the tool call
31
+ if [[ "$HOOK_EVENT" == "PreToolUse" ]]; then
32
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
33
+ if [[ "$TOOL_NAME" != "Skill" ]]; then
34
+ hook_success
35
+ exit 0
36
+ fi
37
+ fi
38
+
39
+ RECORD=$(skill_usage_build_record "$INPUT")
40
+ if [[ -n "$RECORD" ]]; then
41
+ skill_usage_append "$SESSION_ID" "$RECORD" || hook_failure "Failed to append session history"
42
+ onlooker_append_event "$RECORD" || hook_failure "Failed to append global event log"
43
+ fi
44
+
45
+ if [[ "$HOOK_EVENT" == "PreToolUse" ]]; then
46
+ SKILL_NAME=$(echo "$RECORD" | jq -r '.payload.skill_name // empty' 2>/dev/null)
47
+ jq -n --arg msg "Skill tracked${SKILL_NAME:+: $SKILL_NAME}" '{ "decision": "approve", "reason": $msg }'
48
+ fi
49
+
50
+ hook_success
51
+ exit 0
@@ -8,6 +8,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
8
8
  import { join } from 'node:path';
9
9
  import {
10
10
  createEvent,
11
+ SKILL_INVOKED,
11
12
  TOOL_AGENT_COMPLETE,
12
13
  TOOL_AGENT_SPAWN,
13
14
  TOOL_FILE_EDIT,
@@ -78,11 +79,80 @@ function extractPath(toolInput, toolResponse) {
78
79
  return toolInput?.file_path ?? toolInput?.path ?? toolResponse?.filePath ?? toolResponse?.path ?? undefined;
79
80
  }
80
81
 
82
+ function stripUndefined(obj) {
83
+ return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined && v !== null && v !== ''));
84
+ }
85
+
86
+ function parseTurnNumber() {
87
+ const raw = process.env.ONLOOKER_TURN_NUMBER;
88
+ if (raw == null || raw === '') return undefined;
89
+ const n = Number.parseInt(String(raw), 10);
90
+ return Number.isFinite(n) && n >= 1 ? n : undefined;
91
+ }
92
+
81
93
  /**
82
- * Map Claude Code PostToolUse / PostToolUseFailure hook input to a canonical event.
83
- * Returns null when the tool is not mapped to a schema event type.
94
+ * Map UserPromptExpansion or PreToolUse (Skill) hook input to skill.invoked.
95
+ * Returns null when the hook input is not a skill invocation.
96
+ */
97
+ export function mapSkillHookInput(hookInput, options) {
98
+ const { onlookerDir, plugin, runtime = 'claude-code', adapter_id = 'ecosystem.hooks' } = options;
99
+ const hookEvent = hookInput?.hook_event_name;
100
+ const sessionId = hookInput?.session_id ?? 'unknown';
101
+
102
+ let payload;
103
+
104
+ if (hookEvent === 'UserPromptExpansion') {
105
+ const skillName = hookInput?.command_name;
106
+ if (!skillName) return null;
107
+ payload = stripUndefined({
108
+ skill_name: skillName,
109
+ invocation_source: 'slash_command',
110
+ command_args: hookInput?.command_args,
111
+ command_source: hookInput?.command_source,
112
+ expansion_type: hookInput?.expansion_type,
113
+ turn_number: parseTurnNumber(),
114
+ });
115
+ } else if (hookEvent === 'PreToolUse' && hookInput?.tool_name === 'Skill') {
116
+ const toolInput = hookInput?.tool_input ?? {};
117
+ const skillName =
118
+ toolInput.skill ?? toolInput.skill_name ?? toolInput.name ?? toolInput.command ?? toolInput.skill_id;
119
+ if (!skillName) return null;
120
+ const args = toolInput.args ?? toolInput.command_args;
121
+ payload = stripUndefined({
122
+ skill_name: String(skillName),
123
+ invocation_source: 'tool',
124
+ command_args: typeof args === 'string' ? args : args != null ? JSON.stringify(args) : undefined,
125
+ turn_number: parseTurnNumber(),
126
+ });
127
+ } else {
128
+ return null;
129
+ }
130
+
131
+ const event = buildCanonicalEvent({
132
+ onlookerDir,
133
+ runtime,
134
+ adapter_id,
135
+ plugin,
136
+ session_id: sessionId,
137
+ event_type: SKILL_INVOKED,
138
+ payload,
139
+ });
140
+
141
+ const result = validate(event);
142
+ if (!result.valid) {
143
+ return { valid: false, errors: result.errors, event_type: SKILL_INVOKED };
144
+ }
145
+ return { valid: true, event: result.event };
146
+ }
147
+
148
+ /**
149
+ * Map Claude Code hook input to a canonical event.
150
+ * Returns null when the hook input is not mapped to a schema event type.
84
151
  */
85
152
  export function mapHookInputToCanonical(hookInput, options) {
153
+ const skillMapped = mapSkillHookInput(hookInput, options);
154
+ if (skillMapped) return skillMapped;
155
+
86
156
  const { onlookerDir, plugin, runtime = 'claude-code', adapter_id = 'ecosystem.hooks' } = options;
87
157
 
88
158
  const toolName = hookInput?.tool_name;
@@ -25,6 +25,7 @@ onlooker_event_from_hook() {
25
25
  fi
26
26
 
27
27
  printf '%s' "$hook_input" | ONLOOKER_DIR="$ONLOOKER_DIR" ONLOOKER_PLUGIN_NAME="$ONLOOKER_PLUGIN_NAME" \
28
+ ONLOOKER_TURN_NUMBER="${ONLOOKER_TURN_NUMBER:-}" \
28
29
  node "$_ONLOOKER_EVENT_JS" emit-from-hook 2>/dev/null
29
30
  }
30
31
 
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env bash
2
+ # Skill usage helpers — canonical session JSONL via @onlooker-community/schema.
3
+ #
4
+ # Source after validate-path.sh and onlooker-schema.sh:
5
+ # source "$CLAUDE_PLUGIN_ROOT/scripts/lib/validate-path.sh"
6
+ # source "$CLAUDE_PLUGIN_ROOT/scripts/lib/onlooker-schema.sh"
7
+ # source "$CLAUDE_PLUGIN_ROOT/scripts/lib/skill-usage.sh"
8
+ # source "$CLAUDE_PLUGIN_ROOT/scripts/lib/tool-history.sh"
9
+
10
+ # Build a canonical skill.invoked event from hook stdin (empty when unmapped).
11
+ # Usage: record=$(skill_usage_build_record "$INPUT")
12
+ skill_usage_build_record() {
13
+ local input_json="${1:-}"
14
+ onlooker_event_from_hook "$input_json"
15
+ }
16
+
17
+ # Append a canonical skill event to session history (reuses tool-history flock).
18
+ # Usage: skill_usage_append "$SESSION_ID" "$event_json"
19
+ skill_usage_append() {
20
+ tool_history_append "$1" "$2"
21
+ }
@@ -38,6 +38,29 @@ setup_file() {
38
38
  [ "$status" -eq 0 ]
39
39
  }
40
40
 
41
+ @test "hooks.json Skill matcher references skill-usage-tracker" {
42
+ run jq -e '.hooks.PreToolUse[2].matcher == "Skill"' "${REPO_ROOT}/hooks/hooks.json"
43
+ [ "$status" -eq 0 ]
44
+
45
+ local hook_cmd
46
+ hook_cmd=$(jq -r '.hooks.PreToolUse[2].hooks[0].command' "${REPO_ROOT}/hooks/hooks.json")
47
+ [[ "$hook_cmd" == *skill-usage-tracker.sh ]]
48
+
49
+ local script_path="${hook_cmd//\$CLAUDE_PLUGIN_ROOT/$REPO_ROOT}"
50
+ script_path="${script_path//\"/}"
51
+ run test -x "$script_path"
52
+ [ "$status" -eq 0 ]
53
+ }
54
+
55
+ @test "hooks.json UserPromptExpansion references skill-usage-tracker" {
56
+ run jq -e '.hooks.UserPromptExpansion[0].matcher == ""' "${REPO_ROOT}/hooks/hooks.json"
57
+ [ "$status" -eq 0 ]
58
+
59
+ local hook_cmd
60
+ hook_cmd=$(jq -r '.hooks.UserPromptExpansion[0].hooks[0].command' "${REPO_ROOT}/hooks/hooks.json")
61
+ [[ "$hook_cmd" == *skill-usage-tracker.sh ]]
62
+ }
63
+
41
64
  @test "hooks.json PostToolUse references tool-history-tracker" {
42
65
  run jq -e '.hooks.PostToolUse[0].matcher == "*"' "${REPO_ROOT}/hooks/hooks.json"
43
66
  [ "$status" -eq 0 ]
@@ -0,0 +1,76 @@
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
+ # shellcheck source=../../scripts/lib/onlooker-schema.sh
8
+ source "${REPO_ROOT}/scripts/lib/onlooker-schema.sh"
9
+ # shellcheck source=../../scripts/lib/tool-history.sh
10
+ source "${REPO_ROOT}/scripts/lib/tool-history.sh"
11
+ # shellcheck source=../../scripts/lib/skill-usage.sh
12
+ source "${REPO_ROOT}/scripts/lib/skill-usage.sh"
13
+ export CLAUDE_PLUGIN_ROOT="${REPO_ROOT}"
14
+ }
15
+
16
+ @test "skill_usage_build_record maps UserPromptExpansion to skill.invoked" {
17
+ local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/user-prompt-expansion-skill.json"
18
+ local record
19
+ record=$(skill_usage_build_record "$(cat "$fixture")")
20
+ echo "$record" | jq -e \
21
+ '.schema_version == "1.0"
22
+ and .event_type == "skill.invoked"
23
+ and .payload.skill_name == "code-review"
24
+ and .payload.invocation_source == "slash_command"
25
+ and .payload.command_args == "src/main.ts"
26
+ and .payload.expansion_type == "slash_command"
27
+ and .session_id == "skill-session-001"' \
28
+ >/dev/null
29
+ echo "$record" | onlooker_validate_event
30
+ }
31
+
32
+ @test "skill_usage_build_record maps PreToolUse Skill to skill.invoked" {
33
+ local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/pre-tool-use-skill.json"
34
+ local record
35
+ record=$(skill_usage_build_record "$(cat "$fixture")")
36
+ echo "$record" | jq -e \
37
+ '.event_type == "skill.invoked"
38
+ and .payload.skill_name == "code-review"
39
+ and .payload.invocation_source == "tool"
40
+ and .payload.command_args == "src/main.ts"' \
41
+ >/dev/null
42
+ echo "$record" | onlooker_validate_event
43
+ }
44
+
45
+ @test "skill-usage-tracker appends slash command skill to session JSONL" {
46
+ local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/user-prompt-expansion-skill.json"
47
+ local history_file="${ONLOOKER_SESSION_HISTORY_DIR}/skill-session-001.jsonl"
48
+ rm -f "$history_file" "${history_file}.lock"
49
+
50
+ run bash -c "cat '${fixture}' | '${REPO_ROOT}/scripts/hooks/skill-usage-tracker.sh' 2>/dev/null"
51
+ [ "$status" -eq 0 ]
52
+ [ -f "$history_file" ]
53
+ tail -n 1 "$history_file" | jq -e '.event_type == "skill.invoked"' >/dev/null
54
+ tail -n 1 "$history_file" | onlooker_validate_event
55
+ }
56
+
57
+ @test "skill-usage-tracker approves PreToolUse Skill and records event" {
58
+ local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/pre-tool-use-skill.json"
59
+ local history_file="${ONLOOKER_SESSION_HISTORY_DIR}/skill-session-001.jsonl"
60
+ rm -f "$history_file" "${history_file}.lock"
61
+
62
+ run bash -c "cat '${fixture}' | '${REPO_ROOT}/scripts/hooks/skill-usage-tracker.sh' 2>/dev/null"
63
+ [ "$status" -eq 0 ]
64
+ echo "$output" | jq -e '.decision == "approve"' >/dev/null
65
+ tail -n 1 "$history_file" | jq -e '.payload.invocation_source == "tool"' >/dev/null
66
+ }
67
+
68
+ @test "skill-usage-tracker mirrors skill.invoked to global events log" {
69
+ local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/user-prompt-expansion-skill.json"
70
+ : >"$ONLOOKER_EVENTS_LOG"
71
+
72
+ cat "$fixture" | "${REPO_ROOT}/scripts/hooks/skill-usage-tracker.sh" >/dev/null 2>&1
73
+
74
+ tail -n 1 "$ONLOOKER_EVENTS_LOG" | jq -e '.event_type == "skill.invoked"' >/dev/null
75
+ tail -n 1 "$ONLOOKER_EVENTS_LOG" | onlooker_validate_event
76
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "session_id": "skill-session-001",
3
+ "cwd": "/project",
4
+ "hook_event_name": "PreToolUse",
5
+ "tool_name": "Skill",
6
+ "tool_use_id": "skill-use-001",
7
+ "tool_input": {
8
+ "skill": "code-review",
9
+ "args": "src/main.ts"
10
+ }
11
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "session_id": "skill-session-001",
3
+ "cwd": "/project",
4
+ "hook_event_name": "UserPromptExpansion",
5
+ "expansion_type": "slash_command",
6
+ "command_name": "code-review",
7
+ "command_args": "src/main.ts",
8
+ "command_source": "plugin",
9
+ "prompt": "/code-review src/main.ts"
10
+ }
@@ -5,7 +5,7 @@ import { join } from 'node:path';
5
5
  import { test } from 'node:test';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { validate } from '@onlooker-community/schema';
8
- import { buildCanonicalEvent, mapHookInputToCanonical } from '../../scripts/lib/onlooker-event.mjs';
8
+ import { buildCanonicalEvent, mapHookInputToCanonical, mapSkillHookInput } from '../../scripts/lib/onlooker-event.mjs';
9
9
 
10
10
  const REPO_ROOT = join(fileURLToPath(new URL('../..', import.meta.url)));
11
11
  const FIXTURES = join(REPO_ROOT, 'test/fixtures/hook-inputs');
@@ -44,6 +44,34 @@ test('mapHookInputToCanonical maps PostToolUseFailure Bash to tool.shell.exec',
44
44
  assert.equal(validate(mapped.event).valid, true);
45
45
  });
46
46
 
47
+ test('mapSkillHookInput maps UserPromptExpansion to skill.invoked', () => {
48
+ const hookInput = loadFixture('user-prompt-expansion-skill.json');
49
+ const tmpDir = join(REPO_ROOT, 'test/tmp-schema-events');
50
+ const mapped = mapSkillHookInput(hookInput, {
51
+ onlookerDir: tmpDir,
52
+ plugin: 'onlooker',
53
+ });
54
+
55
+ assert.equal(mapped.valid, true);
56
+ assert.equal(mapped.event.event_type, 'skill.invoked');
57
+ assert.equal(mapped.event.payload.skill_name, 'code-review');
58
+ assert.equal(mapped.event.payload.invocation_source, 'slash_command');
59
+ assert.equal(validate(mapped.event).valid, true);
60
+ });
61
+
62
+ test('mapSkillHookInput maps PreToolUse Skill to skill.invoked', () => {
63
+ const hookInput = loadFixture('pre-tool-use-skill.json');
64
+ const tmpDir = join(REPO_ROOT, 'test/tmp-schema-events');
65
+ const mapped = mapSkillHookInput(hookInput, {
66
+ onlookerDir: tmpDir,
67
+ plugin: 'onlooker',
68
+ });
69
+
70
+ assert.equal(mapped.valid, true);
71
+ assert.equal(mapped.event.payload.invocation_source, 'tool');
72
+ assert.equal(validate(mapped.event).valid, true);
73
+ });
74
+
47
75
  test('buildCanonicalEvent assigns monotonic file-backed sequence', () => {
48
76
  const tmpDir = mkdtempSync(join(tmpdir(), 'onlooker-seq-'));
49
77
  const a = buildCanonicalEvent({