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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sessionboard",
3
- "version": "0.1.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, which rates sessions automatically:
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
- `init` walks you through setup and asks before it changes any Claude Code settings.
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
 
@@ -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 (!pluginInstalled(settings) && !hasOurHook(settings)) {
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 = pluginInstalled(settings) ? 'via plugin' : hasOurHook(settings) ? 'installed' : hook ? 'add hook' : 'off (manual updates)';
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 initialized yet; run `sessionboard init` to enable AI rating.\n');
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, tell them to run `node "${CLAUDE_PLUGIN_ROOT}/bin/sessionboard.mjs" init` in a terminal (it asks a couple of setup questions).
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
- "SessionEnd": [
3
+ "SessionStart": [
4
4
  {
5
5
  "hooks": [
6
- { "type": "command", "command": "node \"${CLAUDE_PLUGIN_ROOT}/bin/sessionboard.mjs\" hook", "timeout": 10 }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sessionboard",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "An AI-judged leaderboard of your Claude Code sessions. Keeps your top 3 current and exports them, redacted, for sharing.",
5
5
  "type": "module",
6
6
  "bin": {