sessionboard 0.1.0 → 0.2.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +5 -3
- package/bin/sessionboard.mjs +99 -11
- package/commands/setup.md +32 -0
- package/commands/top.md +1 -1
- package/hooks/hooks.json +2 -2
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sessionboard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "AI-judged leaderboard of your Claude Code sessions: rates each session when it ends, keeps your top 3, and exports them redacted for sharing.",
|
|
5
5
|
"author": { "name": "Kush Bang" },
|
|
6
6
|
"keywords": ["sessions", "portfolio", "sharing", "leaderboard"]
|
package/README.md
CHANGED
|
@@ -14,11 +14,12 @@ Companies now ask you to share your best AI agent session. After hundreds of cha
|
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
17
|
-
As a Claude Code plugin
|
|
17
|
+
As a Claude Code plugin:
|
|
18
18
|
|
|
19
19
|
```
|
|
20
20
|
/plugin marketplace add kbiam/sessionboard
|
|
21
21
|
/plugin install sessionboard@sessionboard
|
|
22
|
+
/sessionboard:setup
|
|
22
23
|
```
|
|
23
24
|
|
|
24
25
|
Or as a CLI:
|
|
@@ -28,7 +29,7 @@ npm install -g sessionboard
|
|
|
28
29
|
sessionboard init
|
|
29
30
|
```
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
Setup asks a few questions (judge model, board size, words to hide) and asks before it changes any Claude Code settings. Both routes share one rating hook: if one is already installed, setup leaves it alone, so installing both never rates a session twice.
|
|
32
33
|
|
|
33
34
|
## Usage
|
|
34
35
|
|
|
@@ -37,9 +38,10 @@ sessionboard dashboard # open the dashboard at localhost:4317
|
|
|
37
38
|
sessionboard top # show your top sessions
|
|
38
39
|
sessionboard share 1 --html # export your #1 session as a redacted page
|
|
39
40
|
sessionboard config # change settings
|
|
41
|
+
sessionboard uninstall # remove the rating hook
|
|
40
42
|
```
|
|
41
43
|
|
|
42
|
-
Inside Claude Code: `/sessionboard:dashboard`, `/sessionboard:top`, `/sessionboard:share 1`.
|
|
44
|
+
Inside Claude Code: `/sessionboard:setup`, `/sessionboard:dashboard`, `/sessionboard:top`, `/sessionboard:share 1`.
|
|
43
45
|
|
|
44
46
|
## How it works
|
|
45
47
|
|
package/bin/sessionboard.mjs
CHANGED
|
@@ -19,6 +19,9 @@ const INDEX_FILE = path.join(DATA_DIR, 'index.json');
|
|
|
19
19
|
const CONFIG_FILE = path.join(DATA_DIR, 'config.json');
|
|
20
20
|
const TOP_FILE = path.join(DATA_DIR, 'TOP3.md');
|
|
21
21
|
const EXPORT_DIR = path.join(DATA_DIR, 'exports');
|
|
22
|
+
// The rating hook always runs this copy, so its path survives plugin updates and npm/repo moves.
|
|
23
|
+
const APP_DIR = path.join(DATA_DIR, 'app');
|
|
24
|
+
const APP_FILES = ['bin/sessionboard.mjs', 'lib/dashboard.html', 'package.json'];
|
|
22
25
|
const LOCK_FILE = path.join(DATA_DIR, 'index.lock');
|
|
23
26
|
const CHILD_ENV = 'SESSIONBOARD_CHILD';
|
|
24
27
|
const PARSER_VERSION = 1;
|
|
@@ -555,9 +558,34 @@ function patchClaudeSettings(mutate) {
|
|
|
555
558
|
writeJSON(CLAUDE_SETTINGS, settings);
|
|
556
559
|
}
|
|
557
560
|
|
|
558
|
-
const pluginInstalled = (settings) => Object.keys(settings.enabledPlugins || {}).some((k) => k.startsWith('sessionboard@'));
|
|
559
|
-
const hookCommand = () => SELF.includes(`${path.sep}_npx${path.sep}`) ? 'npx -y sessionboard hook' : `node "${SELF}" hook`;
|
|
560
561
|
const hasOurHook = (settings) => JSON.stringify(settings.hooks?.SessionEnd || []).includes('sessionboard');
|
|
562
|
+
const hookCommand = () => `node "${path.join(APP_DIR, 'bin', 'sessionboard.mjs')}" hook`;
|
|
563
|
+
|
|
564
|
+
const versionOf = (dir) => readJSON(path.join(dir, 'package.json'), {}).version || '0.0.0';
|
|
565
|
+
const newer = (a, b) => { const x = a.split('.').map(Number), y = b.split('.').map(Number); for (let i = 0; i < 3; i++) if ((x[i] || 0) !== (y[i] || 0)) return (x[i] || 0) > (y[i] || 0); return false; };
|
|
566
|
+
|
|
567
|
+
function installAppCopy() {
|
|
568
|
+
if (path.resolve(ROOT) === path.resolve(APP_DIR)) return;
|
|
569
|
+
for (const rel of APP_FILES) {
|
|
570
|
+
fs.mkdirSync(path.dirname(path.join(APP_DIR, rel)), { recursive: true });
|
|
571
|
+
fs.copyFileSync(path.join(ROOT, rel), path.join(APP_DIR, rel));
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Any newer sessionboard (plugin update, npm upgrade) that runs refreshes the copy the hook uses.
|
|
576
|
+
function refreshAppCopy() {
|
|
577
|
+
try { if (fs.existsSync(APP_DIR) && newer(versionOf(ROOT), versionOf(APP_DIR))) installAppCopy(); } catch {}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Adds the rating hook to settings.json unless one is already there (from any install route).
|
|
581
|
+
function ensureHook(st) {
|
|
582
|
+
installAppCopy();
|
|
583
|
+
if (hasOurHook(st)) return false;
|
|
584
|
+
st.hooks ??= {};
|
|
585
|
+
st.hooks.SessionEnd ??= [];
|
|
586
|
+
st.hooks.SessionEnd.push({ hooks: [{ type: 'command', command: hookCommand(), timeout: 10 }] });
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
561
589
|
|
|
562
590
|
// ---------- dashboard ----------
|
|
563
591
|
|
|
@@ -752,7 +780,7 @@ async function configure(flags, { firstRun }) {
|
|
|
752
780
|
], 0, 2);
|
|
753
781
|
}
|
|
754
782
|
let hook = false;
|
|
755
|
-
if (!
|
|
783
|
+
if (!hasOurHook(settings)) {
|
|
756
784
|
hook = await ask('Rate sessions automatically when they end? (adds a hook to ~/.claude/settings.json)', [
|
|
757
785
|
{ label: 'Yes', value: true, hint: 'recommended' },
|
|
758
786
|
{ label: 'No', value: false, hint: 'run `sessionboard update` yourself' },
|
|
@@ -760,7 +788,7 @@ async function configure(flags, { firstRun }) {
|
|
|
760
788
|
}
|
|
761
789
|
|
|
762
790
|
const cfg = { judge: model !== false, judgeModel: model || cur.judgeModel, rejudgeGrowth, topN, minPrompts, minToolCalls, redactEmails, redactTerms };
|
|
763
|
-
const hookState =
|
|
791
|
+
const hookState = hasOurHook(settings) ? 'on (hook already installed)' : hook ? 'add hook' : 'off (manual updates)';
|
|
764
792
|
const keepState = days >= 365 ? `${days} days` : retention ? `${retention} days ${dim(`(was ${days || 30})`)}` : `${days || 30} days ${dim('(unchanged)')}`;
|
|
765
793
|
const row = (k, v) => `${padEnd(dim(k), 20)}${v}`;
|
|
766
794
|
console.log(`\n${box([
|
|
@@ -785,11 +813,7 @@ function applySetup({ cfg, retention, hook }) {
|
|
|
785
813
|
if (retention || hook) {
|
|
786
814
|
patchClaudeSettings((st) => {
|
|
787
815
|
if (retention) st.cleanupPeriodDays = retention;
|
|
788
|
-
if (hook)
|
|
789
|
-
st.hooks ??= {};
|
|
790
|
-
st.hooks.SessionEnd ??= [];
|
|
791
|
-
st.hooks.SessionEnd.push({ hooks: [{ type: 'command', command: hookCommand(), timeout: 10 }] });
|
|
792
|
-
}
|
|
816
|
+
if (hook) ensureHook(st);
|
|
793
817
|
});
|
|
794
818
|
console.log(` ${green('✔')} Updated ${tildify(CLAUDE_SETTINGS)} ${dim(`(backup: ${path.basename(CLAUDE_SETTINGS)}.sessionboard-backup)`)}`);
|
|
795
819
|
}
|
|
@@ -919,6 +943,7 @@ const HELP = `sessionboard - an AI-judged leaderboard of your Claude Code sessio
|
|
|
919
943
|
sessionboard pin|unpin|hide|unhide <id> curate the board by hand
|
|
920
944
|
sessionboard show <rank|id> all stats for one session
|
|
921
945
|
sessionboard config change settings interactively (or: config key=value)
|
|
946
|
+
sessionboard uninstall [--purge] remove the rating hook (and with --purge, all data)
|
|
922
947
|
|
|
923
948
|
Data lives in ${tildify(DATA_DIR)}; TOP3.md there is always current.`;
|
|
924
949
|
|
|
@@ -939,6 +964,69 @@ async function main() {
|
|
|
939
964
|
return;
|
|
940
965
|
}
|
|
941
966
|
|
|
967
|
+
if (cmd === 'plugin-start') {
|
|
968
|
+
// Plugin SessionStart hook: until setup has run, point the user to /sessionboard:setup (once).
|
|
969
|
+
if (process.env[CHILD_ENV] || loadConfig().initialized) return;
|
|
970
|
+
const marker = path.join(DATA_DIR, '.setup-nudged');
|
|
971
|
+
if (fs.existsSync(marker)) return;
|
|
972
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
973
|
+
fs.writeFileSync(marker, new Date().toISOString());
|
|
974
|
+
console.log(JSON.stringify({ systemMessage: 'sessionboard is installed. Run /sessionboard:setup to turn on AI rating of your sessions.' }));
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
refreshAppCopy();
|
|
979
|
+
|
|
980
|
+
// Non-interactive setup used by /sessionboard:setup: Claude asks the questions, then calls these.
|
|
981
|
+
if (cmd === 'setup-info') {
|
|
982
|
+
const settings = readJSON(CLAUDE_SETTINGS, {});
|
|
983
|
+
const index = scan();
|
|
984
|
+
const sessions = Object.values(index.sessions);
|
|
985
|
+
return console.log(JSON.stringify({
|
|
986
|
+
config: loadConfig(),
|
|
987
|
+
cleanupPeriodDays: settings.cleanupPeriodDays ?? 30,
|
|
988
|
+
hookInstalled: hasOurHook(settings),
|
|
989
|
+
sessions: sessions.length,
|
|
990
|
+
eligible: sessions.filter((x) => x.eligible && !x.missing).length,
|
|
991
|
+
unrated: sessions.filter((x) => x.eligible && !x.missing && !x.judge).length,
|
|
992
|
+
}, null, 2));
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (cmd === 'setup-apply') {
|
|
996
|
+
const cur = loadConfig();
|
|
997
|
+
const num = (v, d) => (v === undefined || v === true ? d : Number(v));
|
|
998
|
+
const model = flags.judgeModel ?? cur.judgeModel;
|
|
999
|
+
const cfg = {
|
|
1000
|
+
judge: model !== 'off',
|
|
1001
|
+
judgeModel: model === 'off' ? cur.judgeModel : model,
|
|
1002
|
+
rejudgeGrowth: num(flags.rejudgeGrowth, cur.rejudgeGrowth),
|
|
1003
|
+
topN: num(flags.topN, cur.topN),
|
|
1004
|
+
minPrompts: num(flags.minPrompts, cur.minPrompts),
|
|
1005
|
+
minToolCalls: num(flags.minToolCalls, cur.minToolCalls),
|
|
1006
|
+
redactEmails: flags.redactEmails === undefined ? cur.redactEmails : flags.redactEmails !== 'false',
|
|
1007
|
+
redactTerms: flags.redactTerms === undefined ? cur.redactTerms : String(flags.redactTerms === true ? '' : flags.redactTerms).split(',').map((x) => x.trim()).filter(Boolean),
|
|
1008
|
+
};
|
|
1009
|
+
const retention = flags.retention && flags.retention !== 'keep' ? Number(flags.retention) : null;
|
|
1010
|
+
applySetup({ cfg, retention, hook: flags.hook === 'yes' });
|
|
1011
|
+
scan();
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
if (cmd === 'uninstall') {
|
|
1016
|
+
// Removes the rating hook and the app copy; your scores stay in ~/.sessionboard unless --purge.
|
|
1017
|
+
const settings = readJSON(CLAUDE_SETTINGS, null);
|
|
1018
|
+
if (settings && hasOurHook(settings)) {
|
|
1019
|
+
patchClaudeSettings((st) => {
|
|
1020
|
+
const kept = st.hooks.SessionEnd.filter((h) => !JSON.stringify(h).includes('sessionboard'));
|
|
1021
|
+
if (kept.length) st.hooks.SessionEnd = kept; else delete st.hooks.SessionEnd;
|
|
1022
|
+
if (!Object.keys(st.hooks).length) delete st.hooks;
|
|
1023
|
+
});
|
|
1024
|
+
console.log(`Removed the sessionboard hook from ${tildify(CLAUDE_SETTINGS)}.`);
|
|
1025
|
+
}
|
|
1026
|
+
fs.rmSync(flags.purge ? DATA_DIR : APP_DIR, { recursive: true, force: true });
|
|
1027
|
+
return console.log(flags.purge ? `Deleted ${tildify(DATA_DIR)}.` : `Your scores are kept in ${tildify(DATA_DIR)} (use --purge to delete them).`);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
942
1030
|
if (cmd === 'help' || flags.help || flags.h) return console.log(HELP);
|
|
943
1031
|
if (cmd === 'init') return init(flags);
|
|
944
1032
|
if (cmd === 'dashboard' || cmd === 'ui') { scan(); return dashboard(flags); }
|
|
@@ -976,13 +1064,13 @@ async function main() {
|
|
|
976
1064
|
|
|
977
1065
|
if (cmd === 'update') {
|
|
978
1066
|
if (canJudge) await judgePending({ log });
|
|
979
|
-
else if (!cfg.initialized) log('Not
|
|
1067
|
+
else if (!cfg.initialized) log('Not set up yet; run `sessionboard init` (or /sessionboard:setup in Claude Code) to enable AI rating.\n');
|
|
980
1068
|
if (!flags.quiet) console.log(`Updated. ${Object.keys(readIndex().sessions).length} sessions indexed → ${tildify(TOP_FILE)}`);
|
|
981
1069
|
return;
|
|
982
1070
|
}
|
|
983
1071
|
|
|
984
1072
|
if (cmd === 'top' || cmd === 'ls') {
|
|
985
|
-
if (!cfg.initialized) console.log(`\n ${yellow('!')} AI rating is off until you run ${accent('sessionboard init')}`);
|
|
1073
|
+
if (!cfg.initialized) console.log(`\n ${yellow('!')} AI rating is off until you run ${accent('sessionboard init')} ${dim('(or /sessionboard:setup in Claude Code)')}`);
|
|
986
1074
|
console.log(`\n ${bold(`Your top ${Number(flags.n) || cfg.topN} agent sessions`)}\n`);
|
|
987
1075
|
printTop(readIndex(), Number(flags.n) || cfg.topN);
|
|
988
1076
|
console.log(`\n ${dim('share one:')} ${accent('sessionboard share 1 --html')} ${dim('dashboard:')} ${accent('sessionboard dashboard')}\n`);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Set up sessionboard - choose the judge model and settings, then rate your sessions
|
|
3
|
+
allowed-tools: Bash(node:*), AskUserQuestion
|
|
4
|
+
---
|
|
5
|
+
Current state:
|
|
6
|
+
!`node "${CLAUDE_PLUGIN_ROOT}/bin/sessionboard.mjs" setup-info`
|
|
7
|
+
|
|
8
|
+
Walk the user through setup. Briefly explain first, in 2-3 lines: when a session ends, a background hook gives it one AI rating (0-10, with a summary and the reason), the best sessions make their board, and any session can be exported as a redacted page to share.
|
|
9
|
+
|
|
10
|
+
Then ask with AskUserQuestion, preselecting the user's current values where the state above shows them (otherwise put the recommended option first):
|
|
11
|
+
|
|
12
|
+
Call 1 (four questions):
|
|
13
|
+
1. "Which model should judge your sessions?" - Haiku (Recommended: fast, cheapest) / Sonnet (sharper, ~3x cost) / Opus (best, slowest) / Off (free heuristic only)
|
|
14
|
+
2. "When should a resumed session be re-rated?" - When it grows 25% (Recommended) / When it doubles / Never
|
|
15
|
+
3. "How big is your board?" - Top 3 (Recommended) / Top 5 / Top 10
|
|
16
|
+
4. "Which sessions count?" - Standard: 2+ prompts and 5+ tool calls (Recommended) / Strict: 5+ prompts and 20+ tool calls / Everything
|
|
17
|
+
|
|
18
|
+
Call 2 (ask only the questions that apply):
|
|
19
|
+
5. Only if cleanupPeriodDays is under 365: "Claude Code deletes transcripts older than N days, so older sessions drop off your board. Keep them longer? This edits ~/.claude/settings.json." - Keep for 10 years (Recommended) / Keep for 1 year / Leave as is
|
|
20
|
+
6. Only if hookInstalled is false: "Rate sessions automatically when they end? This adds one hook to ~/.claude/settings.json." - Yes (Recommended) / No, I'll refresh myself
|
|
21
|
+
7. "Words to hide in exports, like company, product, client or internal host names?" - None / Type them (via Other, comma-separated)
|
|
22
|
+
8. "Redact email addresses in exports?" - Yes (Recommended) / No
|
|
23
|
+
|
|
24
|
+
Then apply exactly what they chose. Only pass --retention and --hook=yes if they explicitly picked those options:
|
|
25
|
+
|
|
26
|
+
`node "${CLAUDE_PLUGIN_ROOT}/bin/sessionboard.mjs" setup-apply --judgeModel=<haiku|sonnet|opus|off> --rejudgeGrowth=<1.25|2|0> --topN=<3|5|10> --minPrompts=<2|5|1> --minToolCalls=<5|20|1> --redactEmails=<true|false> --redactTerms="<comma,separated or empty>" --retention=<3650|365|keep> --hook=<yes|no>`
|
|
27
|
+
|
|
28
|
+
If the judge is on and there are unrated sessions, rate them (one call per session, roughly 8 seconds each; tell the user it's running):
|
|
29
|
+
|
|
30
|
+
`node "${CLAUDE_PLUGIN_ROOT}/bin/sessionboard.mjs" update`
|
|
31
|
+
|
|
32
|
+
Finish by running `node "${CLAUDE_PLUGIN_ROOT}/bin/sessionboard.mjs" top` and showing the board as-is, then mention /sessionboard:dashboard and /sessionboard:share 1.
|
package/commands/top.md
CHANGED
|
@@ -4,4 +4,4 @@ allowed-tools: Bash(node:*)
|
|
|
4
4
|
---
|
|
5
5
|
!`node "${CLAUDE_PLUGIN_ROOT}/bin/sessionboard.mjs" top`
|
|
6
6
|
|
|
7
|
-
Show the leaderboard above to the user as-is. If it says AI rating is off,
|
|
7
|
+
Show the leaderboard above to the user as-is. If it says AI rating is off, suggest running /sessionboard:setup.
|
package/hooks/hooks.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"hooks": {
|
|
3
|
-
"
|
|
3
|
+
"SessionStart": [
|
|
4
4
|
{
|
|
5
5
|
"hooks": [
|
|
6
|
-
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/bin/sessionboard.mjs\"
|
|
6
|
+
{ "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/bin/sessionboard.mjs\" plugin-start", "timeout": 10 }
|
|
7
7
|
]
|
|
8
8
|
}
|
|
9
9
|
]
|
package/package.json
CHANGED