@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.
- package/.claude-plugin/icon.png +0 -0
- package/.claude-plugin/marketplace.json +14 -0
- package/.claude-plugin/plugin.json +15 -0
- package/CHANGELOG.md +30 -0
- package/LICENSE +9 -0
- package/README.md +105 -2
- package/hooks/hooks.json +1 -0
- package/hooks/register.tsx +126 -0
- package/package.json +29 -4
- package/types/index.d.ts +17 -0
|
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
|
-
#
|
|
1
|
+
# skill-status
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
package/hooks/hooks.json
ADDED
|
@@ -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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -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
|
+
}
|