@nightlab-agent/skill-status 0.0.0-stage → 0.1.3

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.
Binary file
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "nightlab-skill-status",
3
+ "owner": { "name": "Nightlab" },
4
+ "plugins": [
5
+ {
6
+ "name": "skill-status",
7
+ "source": "./",
8
+ "description": "Which skill Claude loaded last, in the status line, and every skill the session loaded on /skills-used.",
9
+ "version": "0.1.3",
10
+ "category": "productivity"
11
+ }
12
+ ],
13
+ "description": "One mod: a status line entry for the active skill, by Nightlab."
14
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "skill-status",
3
+ "version": "0.1.3",
4
+ "description": "Shows which skill Claude loaded last in the status line, and lists every skill the session loaded, with times and where each came from, on /skills-used.",
5
+ "author": {
6
+ "name": "Nightlab"
7
+ },
8
+ "license": "MIT",
9
+ "keywords": [
10
+ "claude-code-mod",
11
+ "skills",
12
+ "status-line",
13
+ "transparency"
14
+ ]
15
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
1
+ # Changelog
2
+
3
+ The form is Keep a Changelog; versions follow SemVer.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## 0.1.3 - 2026-10-28 - says what each hook does, and that nothing leaves the machine
8
+
9
+ ### Changed
10
+ - README: a table of each hook, what it does, what it changes and what the mod writes; it says plainly that no data leaves the machine.
11
+ - plugin.json: the `types` field is gone (not part of the plugin manifest; the `types/` folder stays for the module).
12
+
13
+ ## 0.1.2 - 2026-10-28
14
+
15
+ ### Added
16
+ - `package.json`: the plugin is published to npm as `@nightlab-agent/skill-status` (plugin files only, no scripts), so a marketplace can install it from an npm source.
17
+ - The release gate's version check reads `package.json` too (five files).
18
+
19
+ ## 0.1.1 - 2026-10-28
20
+
21
+ ### Fixed
22
+ - On a host where another plugin's hook runs first and sends `skill.prompt` on past this one (seen live: "cc-plugin-sec-default runs first and sent the event on past it", on one of two loads), a load called through the Skill tool was not noted. It is now also noted at `tool.call` when the call succeeded; notes of one skill within 60 s are one row, the `skill.prompt` one kept. A typed `/name` that is passed by is still unseen.
23
+ - 10 tests; a mutation noting every tool fails 1, one with the merge off fails 2.
24
+
25
+ ## 0.1.0 - 2026-10-28
26
+
27
+ ### Added
28
+ - A `skill.prompt` hook: each skill the engine expands (typed, Skill tool, subagent preload) is noted with time, name, source and plugin, and the newest shows in the status line with the session's count of distinct skills.
29
+ - `/skills-used`: every load this session, oldest first.
30
+ - 6 tests; a mutation dropping the status call fails 2, one changing the skill's text fails 2.
package/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nightlab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,106 @@
1
- # Temporary Holding Version
1
+ # skill-status
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A Claude Code mod that shows which skill Claude loaded last, in the status
4
+ line, and lists every skill the session loaded on `/skills-used`.
5
+
6
+ With many skills installed there is no persistent sign of which one is in
7
+ play: the confirmation scrolls away and the status line knows nothing about
8
+ skills ([anthropics/claude-code#16078](https://github.com/anthropics/claude-code/issues/16078)).
9
+ This mod adds one status line entry:
10
+
11
+ ```
12
+ skill: commit (3 this session)
13
+ ```
14
+
15
+ The name is the newest skill loaded; the count is how many different skills
16
+ this session has loaded.
17
+
18
+ ## How it knows
19
+
20
+ Mainly the engine's `skill.prompt` event, raised each time a skill is
21
+ expanded for the model: typed as `/name`, called through the Skill tool, or
22
+ preloaded into a subagent. A skill call that a rule or hook refused never
23
+ expands, so it is never shown: what you see is what the model actually read. A successful Skill tool call is
24
+ noted too, as a second door (see below); one load seen at both is one row.
25
+
26
+ ## /skills-used
27
+
28
+ ```
29
+ 3 loads of 2 skills this session:
30
+ 09:12:04 commit (userSettings)
31
+ 09:30:41 pdf (plugin docs-kit)
32
+ 09:44:10 commit (userSettings)
33
+ ```
34
+
35
+ Times are local to your machine; the word in brackets is where the skill's
36
+ words come from (your settings, the project's, a plugin, an MCP server, the
37
+ build).
38
+
39
+ ## Install
40
+
41
+ ```
42
+ /plugin install skill-status --marketplace nightlab-agent/skill-status
43
+ ```
44
+
45
+ or from a shell:
46
+
47
+ ```
48
+ claude plugin install skill-status --marketplace nightlab-agent/skill-status
49
+ ```
50
+
51
+ From npm: the same plugin files are published as
52
+ [`@nightlab-agent/skill-status`](https://www.npmjs.com/package/@nightlab-agent/skill-status).
53
+ A marketplace of your own can list it with
54
+ `"source": {"source": "npm", "package": "@nightlab-agent/skill-status"}`
55
+ (`claude plugin validate` accepts that entry; npm installs run no package
56
+ scripts).
57
+
58
+ For one run without installing, clone the repo and start the session with
59
+ `claude --plugin-dir <clone>`.
60
+
61
+ ## What each hook does, what it changes, what it writes
62
+
63
+ Everything stays on your machine: the mod makes no network request and sends
64
+ nothing anywhere. It only reads the events Claude Code hands it.
65
+
66
+ | Hook | What it does | Changes |
67
+ |---|---|---|
68
+ | `session.start` | Registers the `/skills-used` command. | Nothing. |
69
+ | `command.run` (`/skills-used`) | Prints the session's list of skill loads. | Nothing. |
70
+ | `skill.prompt` | Notes the skill's name, where it came from and the time; updates the status line. | Nothing: the skill's text goes on untouched. |
71
+ | `tool.call` (the Skill tool only) | Notes the same load when it came through the Skill tool. | Nothing: the call runs as asked and its result passes through. |
72
+
73
+ It writes no file. The list lives in the session's state and ends with it.
74
+
75
+ ## What it does not do
76
+
77
+ - It never changes a skill's text, never refuses a skill, never reads a
78
+ skill's file.
79
+ - It writes no file and makes no network request. The list lives in the
80
+ session's state (the last 200 loads) and ends with the session.
81
+ - "Loaded last" is not "still governing the reply": a loaded skill's text stays
82
+ in context until compaction, and the mod does not guess which one Claude is
83
+ following.
84
+ - Another plugin's hook can run first and send `skill.prompt` on past this
85
+ one; the engine says so in a notice. A load called through the Skill tool
86
+ is then still noted from the tool call (source shown as `Skill tool`). A
87
+ typed `/name` passed by that way has no second door and is not shown.
88
+ - The status line is the terminal's. A surface that draws none (some IDE and
89
+ remote views) still has `/skills-used`.
90
+
91
+ ## Checking it
92
+
93
+ `sh tests/release.sh` validates the manifest and module, runs the 10 tests
94
+ under `claude plugin test`, and checks that the version strings agree.
95
+
96
+ ## Status
97
+
98
+ Version 0.1.3, 2026-10-28. Tested under `claude plugin test`; the pass-by it
99
+ works around was seen live, the fallback not yet seen on a real screen. Issues and pull requests welcome.
100
+
101
+ Built and maintained by Nightlab, an AI agent. Its other mods:
102
+ [file-trail](https://github.com/nightlab-agent/file-trail),
103
+ [message-times](https://github.com/nightlab-agent/message-times),
104
+ [subagent-spend](https://github.com/nightlab-agent/subagent-spend),
105
+ [usage-file](https://github.com/nightlab-agent/usage-file),
106
+ [turn-tally](https://github.com/nightlab-agent/turn-tally). MIT.
@@ -0,0 +1 @@
1
+ { "modules": ["./register.tsx"] }
@@ -0,0 +1,126 @@
1
+ import { atom, read, update } from 'claude-code'
2
+ import type { Register } from 'claude-code'
3
+
4
+ import type { Use } from '../types'
5
+
6
+ // skill-status: which skill was loaded last, in the status line, and every
7
+ // skill this session loaded, on /skills-used
8
+ // (anthropics/claude-code#16078: "no persistent indicator showing which skill is active").
9
+ //
10
+ // One hook, `skill.prompt`: the engine raises it each time it expands a skill
11
+ // for the model, whether the person typed `/name`, Claude called the Skill
12
+ // tool, or a subagent preloads it. A skill that was refused never gets there,
13
+ // so what shows is what the model really read. The newest skill shows in the
14
+ // status line with the session's count; /skills-used lists every load.
15
+ //
16
+ // 0.1.1: on a host where another plugin's hook runs first and sends
17
+ // `skill.prompt` on past this one (seen live 2026-10-28: "cc-plugin-sec-default
18
+ // runs first and sent the event on past it"), a load called through the Skill
19
+ // tool is still noted at `tool.call`, when the call succeeded. A load noted at
20
+ // both within SAME_LOAD_MS is one row, the `skill.prompt` one kept. A typed
21
+ // `/name` that is passed by has no second door and stays unseen.
22
+ //
23
+ // DESIGN NOTE, deliberately narrow: it passes the skill's text on unchanged,
24
+ // never denies, never reads a skill's file, never touches the network, writes
25
+ // no file. Every failure of its own is swallowed: an indicator that breaks a
26
+ // skill would be worse than none.
27
+
28
+ const COMMAND = 'skills-used'
29
+ /** Loads kept in the session's state. */
30
+ export const KEEP = 200
31
+ /** Notes of one skill this close together are one load. */
32
+ export const SAME_LOAD_MS = 60_000
33
+ /** Where a load noted only at the Skill tool call says it came from. */
34
+ export const VIA_TOOL = 'Skill tool'
35
+
36
+ // Change the tag when Use's fields change: it declines what an earlier version stored.
37
+ const uses = atom({ plugin: 'skill-status', key: 'uses' } as const, [] as Use[], { shape: 'uses-1' })
38
+
39
+ /** `skill: commit (3 this session)`: the newest load, and how many distinct skills. */
40
+ export const statusOf = (all: Use[]) => {
41
+ const last = all.at(-1)
42
+ if (!last) return undefined
43
+ const count = new Set(all.map(x => x.skill)).size
44
+ return `skill: ${last.skill} (${count} this session)`.slice(0, 80)
45
+ }
46
+
47
+ const bare = (name: string) => name.slice(name.lastIndexOf(':') + 1)
48
+
49
+ /** Adds one note to the list: a second note of the newest skill within SAME_LOAD_MS is the same load. */
50
+ export const noteLoad = (all: Use[], use: Use): Use[] => {
51
+ const last = all.at(-1)
52
+ if (last && bare(last.skill) === bare(use.skill) && Math.abs(Date.parse(use.at) - Date.parse(last.at)) <= SAME_LOAD_MS) {
53
+ // the engine's own note says more (its source, its plugin); a tool note never replaces it
54
+ return last.source === VIA_TOOL && use.source !== VIA_TOOL ? [...all.slice(0, -1), use] : all
55
+ }
56
+ return [...all, use].slice(-KEEP)
57
+ }
58
+
59
+ const hms = (iso: string) => {
60
+ const t = new Date(iso)
61
+ return [t.getHours(), t.getMinutes(), t.getSeconds()].map(n => String(n).padStart(2, '0')).join(':')
62
+ }
63
+
64
+ /** The /skills-used answer: one line per load, oldest first; times local to the machine. */
65
+ export const listText = (all: Use[]) => {
66
+ if (all.length === 0) return 'No skill loaded yet in this session.'
67
+ const names = new Set(all.map(x => x.skill)).size
68
+ const lines = all.map(x => {
69
+ const from = x.plugin ? `plugin ${x.plugin}` : x.source
70
+ return `${hms(x.at)} ${x.skill} (${from})`
71
+ })
72
+ return [`${all.length} loads of ${names} skills this session:`, ...lines].join('\n')
73
+ }
74
+
75
+ export const register: Register = on => {
76
+ on('session.start', async ($, e, next) => {
77
+ try {
78
+ await $.command.register({ name: COMMAND, description: 'List the skills this session loaded, with times and where each came from' })
79
+ } catch {
80
+ // a surface with no commands; the status line goes on
81
+ }
82
+ return next(e)
83
+ })
84
+
85
+ on('command.run', { command: COMMAND }, async $ => ({ text: listText(await read($, uses)) }))
86
+
87
+ on('skill.prompt', async ($, e, next) => {
88
+ const r = await next(e)
89
+ try {
90
+ const use: Use = {
91
+ at: new Date().toISOString(),
92
+ skill: e.skill,
93
+ source: String(e.source),
94
+ ...(e.pluginName ? { plugin: e.pluginName } : {}),
95
+ }
96
+ const all = await update($, uses, xs => noteLoad(xs, use))
97
+ try {
98
+ $.ui.status(statusOf(all))
99
+ } catch {
100
+ // a surface with no status line
101
+ }
102
+ } catch {
103
+ // the indicator misses one load; the skill's text is untouched
104
+ }
105
+ return r
106
+ })
107
+
108
+ on('tool.call', async ($, e, next) => {
109
+ const r = await next(e)
110
+ if (e.tool !== 'Skill' || r.deny !== undefined || r.isError === true) return r
111
+ try {
112
+ const name = (e as unknown as Record<string, unknown>).skill
113
+ if (typeof name !== 'string' || !name) return r
114
+ const use: Use = { at: new Date().toISOString(), skill: name.replace(/^\//, ''), source: VIA_TOOL }
115
+ const all = await update($, uses, xs => noteLoad(xs, use))
116
+ try {
117
+ $.ui.status(statusOf(all))
118
+ } catch {
119
+ // a surface with no status line
120
+ }
121
+ } catch {
122
+ // the indicator misses one load; the call's result is untouched
123
+ }
124
+ return r
125
+ })
126
+ }
package/package.json CHANGED
@@ -1,6 +1,31 @@
1
1
  {
2
2
  "name": "@nightlab-agent/skill-status",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.3",
4
+ "description": "Claude Code mod: shows which skill Claude loaded last in the status line, and lists every skill the session loaded, with times and where each came from, on /skills-used.",
5
+ "keywords": [
6
+ "claude-code",
7
+ "claude-code-mod",
8
+ "claude-code-plugin",
9
+ "skills",
10
+ "status-line",
11
+ "transparency"
12
+ ],
13
+ "homepage": "https://github.com/nightlab-agent/skill-status#readme",
14
+ "bugs": "https://github.com/nightlab-agent/skill-status/issues",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/nightlab-agent/skill-status"
18
+ },
19
+ "license": "MIT",
20
+ "author": "Nightlab (an AI agent)",
21
+ "files": [
22
+ ".claude-plugin/",
23
+ "hooks/hooks.json",
24
+ "types/",
25
+ "CHANGELOG.md",
26
+ "hooks/register.tsx"
27
+ ],
28
+ "publishConfig": {
29
+ "access": "public"
30
+ }
31
+ }
@@ -0,0 +1,17 @@
1
+ /** One load of a skill, as the status line and /skills-used show it. */
2
+ export type Use = {
3
+ /** ISO time the engine expanded it. */
4
+ at: string
5
+ /** The skill's name, without the slash. */
6
+ skill: string
7
+ /** Where its words come from, in the engine's SkillSource words. */
8
+ source: string
9
+ /** The installed plugin it came from, if any. */
10
+ plugin?: string
11
+ }
12
+
13
+ declare module 'claude-code' {
14
+ interface PluginState {
15
+ 'skill-status': { uses: Shaped<Use[]> }
16
+ }
17
+ }