burner-cc 0.3.1 → 0.3.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/README.md +3 -4
- package/package.json +1 -1
- package/src/cli.js +4 -4
- package/src/config.js +4 -5
- package/src/install.js +45 -51
- package/src/receipt.js +1 -1
- package/src/report.js +1 -1
- package/src/settings-page.js +9 -19
- package/src/tag-end.js +34 -55
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ burner serve # same; --port N, --no-open
|
|
|
29
29
|
burner install # global /burner-tag skill + tagging level from ~/.burner/config.yaml
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
Settings covers **projects**, **categories**, **pricing** (pin/refresh per model), and **tagging** (slider,
|
|
32
|
+
Settings covers **projects**, **categories**, **pricing** (pin/refresh per model), and **tagging** (slider, train your intent model).
|
|
33
33
|
|
|
34
34
|
## Commands
|
|
35
35
|
|
|
@@ -67,10 +67,9 @@ Resolution order:
|
|
|
67
67
|
|--------|----------|
|
|
68
68
|
| Off | Removes global skill / hooks |
|
|
69
69
|
| Manual | `/burner-tag` only; skill stays installed |
|
|
70
|
-
| Session end | SessionEnd hook
|
|
71
|
-
| Every task | CLAUDE.md asks Claude to tag each task (`--task`) |
|
|
70
|
+
| Session end | SessionEnd hook tags with your **local** trained model (no API, no CLAUDE.md) |
|
|
72
71
|
|
|
73
|
-
Moving the slider
|
|
72
|
+
Moving the tagging slider updates `~/.claude` automatically. Use **Train from my tags** in Settings for a local naive-Bayes model (`~/.burner/model/intent-nb.json`). Training is pure Node — no GPU or Python. CLI `burner model train` does the same; `--prior` / `--swe-chat` are optional extras.
|
|
74
73
|
|
|
75
74
|
## Tokens & pricing
|
|
76
75
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -25,7 +25,7 @@ const HELP = `
|
|
|
25
25
|
burner tag <category> [--session ID] [--reason TEXT] [--task]
|
|
26
26
|
burner tag --clear [--session ID]
|
|
27
27
|
burner install install /${SKILL_NAME} globally + apply the tagging level
|
|
28
|
-
burner uninstall remove the skill and
|
|
28
|
+
burner uninstall remove the skill and SessionEnd hook
|
|
29
29
|
burner status
|
|
30
30
|
|
|
31
31
|
Pricing & model
|
|
@@ -119,13 +119,13 @@ async function main() {
|
|
|
119
119
|
if (cmd === 'install') {
|
|
120
120
|
const st = applyTagging(loadConfig());
|
|
121
121
|
console.log(` /${st.skillName} -> ${st.skillPath}${st.skillInstalled ? '' : ' (not installed: level is off)'}`);
|
|
122
|
-
console.log(
|
|
123
|
-
if (st.sessionEndHook) console.log(
|
|
122
|
+
if (st.legacyClaudeMd) console.log(' legacy CLAUDE.md burner block: still present (run install to strip)');
|
|
123
|
+
if (st.sessionEndHook) console.log(' SessionEnd hook: on (local auto-tag)');
|
|
124
124
|
return;
|
|
125
125
|
}
|
|
126
126
|
if (cmd === 'uninstall') {
|
|
127
127
|
uninstall();
|
|
128
|
-
return console.log(' removed skill and CLAUDE.md block');
|
|
128
|
+
return console.log(' removed skill, hook, and legacy CLAUDE.md block');
|
|
129
129
|
}
|
|
130
130
|
if (cmd === 'status') {
|
|
131
131
|
const cfg = loadConfig();
|
package/src/config.js
CHANGED
|
@@ -40,9 +40,8 @@ export const UNTAGGED = { id: 'untagged', label: 'Untagged', color: '#a8a29e', d
|
|
|
40
40
|
// Tagging levels, lowest to highest — mirrors Claude Code's effort slider.
|
|
41
41
|
export const TAGGING_LEVELS = [
|
|
42
42
|
{ id: 'off', label: 'Off', description: 'Never tag. Sessions stay untagged unless the project has a default. Burner removes the global /burner-tag skill and hooks from ~/.claude.' },
|
|
43
|
-
{ id: 'manual', label: 'Manual', description: 'Only when you run /burner-tag. Burner keeps the skill installed globally
|
|
44
|
-
{ id: 'session', label: 'Session end', description: '
|
|
45
|
-
{ id: 'task', label: 'Every task', description: 'Claude tags after each distinct task (/burner-tag with --task). Burner adds a CLAUDE.md block and keeps the skill installed.' },
|
|
43
|
+
{ id: 'manual', label: 'Manual', description: 'Only when you run /burner-tag. Burner keeps the skill installed globally.' },
|
|
44
|
+
{ id: 'session', label: 'Session end', description: 'SessionEnd hook tags using your trained local model (and project default). No API calls and no CLAUDE.md edits.' },
|
|
46
45
|
];
|
|
47
46
|
|
|
48
47
|
export function defaultConfig() {
|
|
@@ -53,7 +52,6 @@ export function defaultConfig() {
|
|
|
53
52
|
tagging: {
|
|
54
53
|
level: 'manual',
|
|
55
54
|
learned: true,
|
|
56
|
-
endModel: '',
|
|
57
55
|
},
|
|
58
56
|
ui: { theme: 'paper', range: 'all', customFrom: '', customTo: '' },
|
|
59
57
|
};
|
|
@@ -94,7 +92,8 @@ function normalizeConfig(raw) {
|
|
|
94
92
|
cfg.tagging = { ...base.tagging, ...(cfg.tagging ?? {}) };
|
|
95
93
|
delete cfg.tagging.auto;
|
|
96
94
|
if (cfg.tagging.level === 'auto') cfg.tagging.level = 'manual';
|
|
97
|
-
cfg.tagging.
|
|
95
|
+
if (cfg.tagging.level === 'task') cfg.tagging.level = 'manual';
|
|
96
|
+
delete cfg.tagging.endModel;
|
|
98
97
|
const ui = cfg.ui ?? {};
|
|
99
98
|
cfg.ui = {
|
|
100
99
|
theme: THEMES.includes(ui.theme) ? ui.theme : 'paper',
|
package/src/install.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Wires burner into Claude Code globally (~/.claude), never per-repo:
|
|
2
2
|
// skills/<SKILL_NAME>/SKILL.md — the tag skill, available in every project
|
|
3
|
-
//
|
|
3
|
+
// SessionEnd hook — session level: local auto-tag (no API, no CLAUDE.md)
|
|
4
4
|
|
|
5
5
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync } from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
+
import process from 'node:process';
|
|
7
8
|
import { fileURLToPath } from 'node:url';
|
|
8
9
|
import { CLAUDE_HOME } from './config.js';
|
|
9
10
|
|
|
@@ -31,16 +32,18 @@ function pathNorm(p) {
|
|
|
31
32
|
return path.resolve(p).replace(/\\/g, '/').toLowerCase();
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
/** True when
|
|
35
|
+
/** True when this process is the burner repo checkout, not npm's installed `burner-cc` package. */
|
|
35
36
|
export function isDevCheckout() {
|
|
36
37
|
const entry = process.argv[1] ? path.resolve(process.argv[1]) : '';
|
|
37
|
-
|
|
38
|
+
if (!entry || pathNorm(entry) !== pathNorm(BIN)) return false;
|
|
39
|
+
const pkgRoot = pathNorm(path.resolve(__dirname, '..'));
|
|
40
|
+
return !/\/node_modules\/burner-cc$/.test(pkgRoot);
|
|
38
41
|
}
|
|
39
42
|
|
|
40
|
-
/**
|
|
43
|
+
/** Command for skills/hooks: always `node` + absolute burner.js (works without `burner` on PATH). */
|
|
41
44
|
export function burnerShell(sub = '') {
|
|
42
45
|
const tail = sub ? ` ${sub}` : '';
|
|
43
|
-
return
|
|
46
|
+
return `${nodeCmd()}${tail}`;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
/** Paste into Claude Code / Cursor chat if /burner-tag is missing after you changed categories. */
|
|
@@ -51,7 +54,7 @@ export function skillSetupPrompt() {
|
|
|
51
54
|
return `Set up burner session tagging on this machine (global, not per-repo):
|
|
52
55
|
1. Run in a terminal: ${cmd}${dev}
|
|
53
56
|
(After npm i -g burner-cc, the same command is just: burner install)
|
|
54
|
-
2. That writes ${path.join(home, 'skills', SKILL_NAME, 'SKILL.md')} and applies the tagging level from ~/.burner/config.yaml (
|
|
57
|
+
2. That writes ${path.join(home, 'skills', SKILL_NAME, 'SKILL.md')} and applies the tagging level from ~/.burner/config.yaml (SessionEnd hook when level is session).
|
|
55
58
|
3. Re-run install after you change categories or tagging level on the Settings page.
|
|
56
59
|
Do not edit project repos for this — only files under ${home}.`;
|
|
57
60
|
}
|
|
@@ -60,7 +63,7 @@ export function skillMarkdown(cfg) {
|
|
|
60
63
|
const cats = cfg.categories.map((c) => `- \`${c.id}\` — ${c.description || c.label}`).join('\n');
|
|
61
64
|
return `---
|
|
62
65
|
name: ${SKILL_NAME}
|
|
63
|
-
description: Tag the current Claude Code session with what it was used for (coding, debugging, planning, research, writing, …) for burner usage analytics. Use when the user runs /${SKILL_NAME} or asks to tag/categorize this session
|
|
66
|
+
description: Tag the current Claude Code session with what it was used for (coding, debugging, planning, research, writing, …) for burner usage analytics. Use when the user runs /${SKILL_NAME} or asks to tag/categorize this session.
|
|
64
67
|
---
|
|
65
68
|
|
|
66
69
|
# ${SKILL_NAME}
|
|
@@ -81,22 +84,6 @@ ${burnerShell('tag')} <category> --session "\${CLAUDE_SESSION_ID}" --reason "<un
|
|
|
81
84
|
`;
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
function claudeMdBlock(level) {
|
|
85
|
-
if (level === 'session') {
|
|
86
|
-
return `${START}
|
|
87
|
-
## burner
|
|
88
|
-
When you've finished the work the user asked for in this session — before your final summary — use the \`${SKILL_NAME}\` skill once to tag the session. Tag again only if the session's main purpose changed. Keep it silent.
|
|
89
|
-
${END}`;
|
|
90
|
-
}
|
|
91
|
-
if (level === 'task') {
|
|
92
|
-
return `${START}
|
|
93
|
-
## burner
|
|
94
|
-
Each time you finish a distinct task the user asked for, use the \`${SKILL_NAME}\` skill with \`--task\` to tag that task before replying. One tag per task, silent.
|
|
95
|
-
${END}`;
|
|
96
|
-
}
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
87
|
function readJsonFile(file) {
|
|
101
88
|
if (!existsSync(file)) return {};
|
|
102
89
|
try {
|
|
@@ -106,19 +93,36 @@ function readJsonFile(file) {
|
|
|
106
93
|
}
|
|
107
94
|
}
|
|
108
95
|
|
|
96
|
+
/** Claude Code command hook (exec form): node + burner.js, no shell/PATH/.cmd shims. */
|
|
97
|
+
export function sessionEndHookEntry() {
|
|
98
|
+
return {
|
|
99
|
+
type: 'command',
|
|
100
|
+
command: process.execPath,
|
|
101
|
+
args: [BIN, 'hook', 'session-end'],
|
|
102
|
+
timeout: 60,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
109
106
|
function isOurHook(h) {
|
|
110
|
-
|
|
107
|
+
if (h?.type !== 'command') return false;
|
|
108
|
+
const args = h.args;
|
|
109
|
+
if (Array.isArray(args) && args.includes('hook') && args[args.length - 1] === 'session-end') return true;
|
|
110
|
+
return typeof h.command === 'string' && h.command.includes('hook session-end');
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
function ensureSessionEndHook() {
|
|
114
114
|
const { settings: file } = paths();
|
|
115
115
|
let settings;
|
|
116
116
|
try { settings = readJsonFile(file); } catch { settings = {}; }
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
groups
|
|
121
|
-
|
|
117
|
+
const entry = sessionEndHookEntry();
|
|
118
|
+
const hooks = settings.hooks ?? {};
|
|
119
|
+
const before = JSON.stringify(settings);
|
|
120
|
+
let groups = (hooks.SessionEnd ?? [])
|
|
121
|
+
.map((g) => ({ ...g, hooks: (g.hooks ?? []).filter((h) => !isOurHook(h)) }))
|
|
122
|
+
.filter((g) => g.hooks.length);
|
|
123
|
+
groups.push({ hooks: [entry] });
|
|
124
|
+
settings.hooks = { ...hooks, SessionEnd: groups };
|
|
125
|
+
if (JSON.stringify(settings) === before) return;
|
|
122
126
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
123
127
|
writeFileSync(file, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
124
128
|
}
|
|
@@ -142,18 +146,15 @@ function removeSessionEndHook() {
|
|
|
142
146
|
writeFileSync(file, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
143
147
|
}
|
|
144
148
|
|
|
145
|
-
|
|
149
|
+
/** Remove legacy burner blocks from ~/.claude/CLAUDE.md (we no longer install them). */
|
|
150
|
+
export function stripBurnerClaudeMd() {
|
|
146
151
|
const { claudeMd } = paths();
|
|
147
|
-
|
|
152
|
+
if (!existsSync(claudeMd)) return;
|
|
153
|
+
const current = readFileSync(claudeMd, 'utf-8');
|
|
148
154
|
const re = new RegExp(`\\n?${START}[\\s\\S]*?${END}\\n?`, 'g');
|
|
149
|
-
|
|
150
|
-
const block = claudeMdBlock(level);
|
|
151
|
-
if (block) next = (next ? next + '\n\n' : '') + block;
|
|
152
|
-
next = next ? next + '\n' : '';
|
|
155
|
+
const next = current.replace(re, '\n').replace(/\s+$/, '');
|
|
153
156
|
if (next.trim() === current.trim()) return;
|
|
154
|
-
|
|
155
|
-
mkdirSync(path.dirname(claudeMd), { recursive: true });
|
|
156
|
-
writeFileSync(claudeMd, next, 'utf-8');
|
|
157
|
+
writeFileSync(claudeMd, next ? next + '\n' : '', 'utf-8');
|
|
157
158
|
}
|
|
158
159
|
|
|
159
160
|
export function installSkill(cfg) {
|
|
@@ -166,40 +167,34 @@ export function installSkill(cfg) {
|
|
|
166
167
|
// Applies the tagging level from config to ~/.claude. Idempotent.
|
|
167
168
|
export function applyTagging(cfg) {
|
|
168
169
|
const level = cfg.tagging?.level ?? 'manual';
|
|
169
|
-
const endModel = String(cfg.tagging?.endModel ?? '').trim();
|
|
170
170
|
if (level === 'off') {
|
|
171
171
|
const p = paths();
|
|
172
172
|
if (existsSync(p.skillDir)) rmSync(p.skillDir, { recursive: true, force: true });
|
|
173
173
|
} else {
|
|
174
174
|
installSkill(cfg);
|
|
175
175
|
}
|
|
176
|
+
stripBurnerClaudeMd();
|
|
176
177
|
removeSessionEndHook();
|
|
177
|
-
if (level === 'session'
|
|
178
|
-
ensureSessionEndHook();
|
|
179
|
-
setClaudeMd('off');
|
|
180
|
-
} else {
|
|
181
|
-
setClaudeMd(level === 'off' ? 'off' : level);
|
|
182
|
-
}
|
|
178
|
+
if (level === 'session') ensureSessionEndHook();
|
|
183
179
|
return installStatus(cfg);
|
|
184
180
|
}
|
|
185
181
|
|
|
186
182
|
export function uninstall() {
|
|
187
183
|
const p = paths();
|
|
188
184
|
if (existsSync(p.skillDir)) rmSync(p.skillDir, { recursive: true, force: true });
|
|
189
|
-
|
|
185
|
+
stripBurnerClaudeMd();
|
|
190
186
|
removeSessionEndHook();
|
|
191
187
|
}
|
|
192
188
|
|
|
193
189
|
export function installStatus(cfg = null) {
|
|
194
190
|
const p = paths();
|
|
195
|
-
const md = existsSync(p.claudeMd) ? readFileSync(p.claudeMd, 'utf-8') : '';
|
|
196
|
-
const block = md.match(new RegExp(`${START}[\\s\\S]*?${END}`))?.[0] ?? '';
|
|
197
191
|
let hookInstalled = false;
|
|
198
192
|
try {
|
|
199
193
|
const settings = readJsonFile(p.settings);
|
|
200
194
|
hookInstalled = (settings.hooks?.SessionEnd ?? []).some((g) => (g.hooks ?? []).some(isOurHook));
|
|
201
195
|
} catch { /* no settings */ }
|
|
202
|
-
const
|
|
196
|
+
const md = existsSync(p.claudeMd) ? readFileSync(p.claudeMd, 'utf-8') : '';
|
|
197
|
+
const legacyClaudeMd = !!md.match(new RegExp(`${START}[\\s\\S]*?${END}`));
|
|
203
198
|
return {
|
|
204
199
|
skillName: SKILL_NAME,
|
|
205
200
|
skillInstalled: existsSync(p.skillFile),
|
|
@@ -207,9 +202,8 @@ export function installStatus(cfg = null) {
|
|
|
207
202
|
skillInstallCmd: burnerShell('install'),
|
|
208
203
|
skillInstallCmdNpm: `${NPM_CLI} install`,
|
|
209
204
|
devCheckout: isDevCheckout(),
|
|
210
|
-
|
|
205
|
+
legacyClaudeMd,
|
|
211
206
|
sessionEndHook: hookInstalled,
|
|
212
|
-
endModel,
|
|
213
207
|
claudeHome: CLAUDE_HOME,
|
|
214
208
|
};
|
|
215
209
|
}
|
package/src/receipt.js
CHANGED
|
@@ -62,7 +62,7 @@ const TAGLINES = [
|
|
|
62
62
|
'burned so you don\'t have to guess',
|
|
63
63
|
'classified and costed',
|
|
64
64
|
];
|
|
65
|
-
const SOURCE_LABEL = { skill: 'TAGGED BY CLAUDE', manual: 'TAGGED BY YOU', 'end-model': 'SESSION-END
|
|
65
|
+
const SOURCE_LABEL = { skill: 'TAGGED BY CLAUDE', manual: 'TAGGED BY YOU', 'end-model': 'SESSION-END (legacy)', 'session-end': 'SESSION-END AUTO', learned: 'LEARNED GUESS', project: 'PROJECT DEFAULT', none: 'NOT TAGGED' };
|
|
66
66
|
|
|
67
67
|
function money(n, unpriced) {
|
|
68
68
|
if (unpriced) return '—';
|
package/src/report.js
CHANGED
|
@@ -596,7 +596,7 @@ ${logoTextUri ? `<img class="page-brand" src="${logoTextUri}" alt="Burner" />` :
|
|
|
596
596
|
<h3>Tokens</h3>
|
|
597
597
|
<p>Claude Code logs one API response as several transcript lines (one per streamed content block) with growing <code>output_tokens</code>. Burner keeps only the final line per request ID, and drops requests a continued session re-logs from its parent. Totals include all four billed buckets: fresh input, cache write, cache read, output. Subagent transcripts are billed to their parent session.</p>
|
|
598
598
|
<h3>Categories</h3>
|
|
599
|
-
<p>A session's category comes from, in order: a tag (<code>/burner-tag</code>, the report UI,
|
|
599
|
+
<p>A session's category comes from, in order: a tag (<code>/burner-tag</code>, the report UI, SessionEnd auto-tag, or CLI), its project's default category, then the <strong>learned model</strong> if trained and confident (≥60%). Otherwise it stays <em>untagged</em>.</p>
|
|
600
600
|
<p><strong>Learned model</strong> — optional on-device classifier (multinomial naive Bayes) trained on your tagged sessions; guesses only when nothing else applied. <strong>/burner-tag skill</strong> — global skill under <code>~/.claude/skills/burner-tag</code>; run <code>burner install</code> (Settings → Tagging has copy blocks). <strong>Session end</strong> — with a model id on Settings, a SessionEnd hook asks that Anthropic model what the session was for; without a model id, the main agent tags via the skill before wrapping up.</p>
|
|
601
601
|
<p>Token totals follow the same idea as <a href="https://ccusage.com/guide/cost-modes" target="_blank" rel="noopener">ccusage</a>: one row per <code>requestId</code> (last streamed chunk), 5m/1h cache write split when present, global dedup for continued sessions. Burner always calculates from tokens (like ccusage <code>--mode calculate</code>); Claude Code's <code>cost-state</code> line is shown on the receipt for comparison when available.</p>
|
|
602
602
|
<h3>Projects</h3>
|
package/src/settings-page.js
CHANGED
|
@@ -598,9 +598,8 @@ function taggingSetupPills(level, ins, tagging, learned){
|
|
|
598
598
|
pills.appendChild(h('span', {class:'badge dimmed', text:'skill removed'}));
|
|
599
599
|
} else {
|
|
600
600
|
pills.appendChild(h('span', {class:'badge' + (ins.skillInstalled ? ' ok' : ''), text:'/' + (ins.skillName || 'burner-tag') + (ins.skillInstalled ? ' \u2713' : ' \u2717')}));
|
|
601
|
-
var
|
|
602
|
-
|
|
603
|
-
pills.appendChild(h('span', {class:'badge' + ((ins.claudeMdLevel || ins.sessionEndHook) ? ' ok' : ' dimmed'), text: md}));
|
|
601
|
+
var hookLbl = level === 'session' ? (ins.sessionEndHook ? 'SessionEnd hook \u2713' : 'SessionEnd hook \u2717') : 'SessionEnd off';
|
|
602
|
+
pills.appendChild(h('span', {class:'badge' + (level === 'session' && ins.sessionEndHook ? ' ok' : (level === 'session' ? '' : ' dimmed')), text: hookLbl}));
|
|
604
603
|
}
|
|
605
604
|
pills.appendChild(h('span', {class:'dim small', style:'margin-left:4px', text:'|'}));
|
|
606
605
|
var L = learned;
|
|
@@ -633,7 +632,6 @@ function rTagging(){
|
|
|
633
632
|
var lv = levels[i].id;
|
|
634
633
|
desc.textContent = levels[i].description;
|
|
635
634
|
Array.prototype.forEach.call(ticks.children, function(s, j){ s.className = j === i ? 'sel' : ''; });
|
|
636
|
-
sessionCard.style.display = lv === 'session' ? '' : 'none';
|
|
637
635
|
}
|
|
638
636
|
slider.addEventListener('input', function(){ syncLevelUi(Number(slider.value)); });
|
|
639
637
|
slider.addEventListener('change', function(){
|
|
@@ -647,28 +645,20 @@ function rTagging(){
|
|
|
647
645
|
levelCard.appendChild(setupPills);
|
|
648
646
|
wrap.appendChild(levelCard);
|
|
649
647
|
|
|
650
|
-
var
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
save(api('PUT', '/api/config', {tagging:{endModel: endIn.value.trim()}}), 'Claude setup updated');
|
|
656
|
-
});
|
|
657
|
-
var endField = h('div', {class:'field'});
|
|
658
|
-
endField.appendChild(h('label', {text:'Model id'}));
|
|
659
|
-
endField.appendChild(endIn);
|
|
660
|
-
sessionCard.appendChild(endField);
|
|
661
|
-
sessionCard.appendChild(h('p', {class:'dim small', style:'margin-top:8px', text: ins.sessionEndHook ? 'Hook installed' + (ins.endModel ? ' for ' + ins.endModel : '') + ' · needs ANTHROPIC_API_KEY' : 'Saving a model id installs the SessionEnd hook automatically.'}));
|
|
662
|
-
wrap.appendChild(sessionCard);
|
|
648
|
+
var sessionNote = h('div', {class:'card'});
|
|
649
|
+
sessionNote.appendChild(h('h3', {text:'Session end auto-tag'}));
|
|
650
|
+
sessionNote.appendChild(h('p', {class:'note', text:'Uses your trained local model (train below) and project defaults. Runs in the SessionEnd hook — no Anthropic API, no edits to CLAUDE.md.'}));
|
|
651
|
+
sessionNote.appendChild(h('p', {class:'dim small', text: ins.sessionEndHook ? 'Hook installed in ~/.claude/settings.json' : 'Move the slider to Session end to install the hook.'}));
|
|
652
|
+
wrap.appendChild(sessionNote);
|
|
663
653
|
|
|
664
654
|
var skillCard = h('div', {class:'card'});
|
|
665
655
|
skillCard.appendChild(h('h3', {text:'Troubleshooting / manual install'}));
|
|
666
|
-
skillCard.appendChild(h('p', {class:'note', text:'Moving the tagging slider
|
|
656
|
+
skillCard.appendChild(h('p', {class:'note', text:'Moving the tagging slider updates ~/.claude for you. Use this only if /burner-tag is missing or hooks look wrong.'}));
|
|
667
657
|
skillCard.appendChild(h('div', {class:'card-section', style:'margin-top:0;padding-top:0;border:none'}, [
|
|
668
658
|
h('h4', {text:'Status'}),
|
|
669
659
|
h('div', {text:'/' + ins.skillName + ' ' + (ins.skillInstalled ? '\u2713' : '\u2717')}),
|
|
670
660
|
ins.skillPath ? h('div', {class:'dim small', text:ins.skillPath}) : null,
|
|
671
|
-
h('div', {class:'dim small', text:'CLAUDE.md
|
|
661
|
+
ins.legacyClaudeMd ? h('div', {class:'dim small', text:'Legacy CLAUDE.md burner block still present — run Sync to remove'}) : null]));
|
|
672
662
|
var installCmd = ins.skillInstallCmdNpm || 'burner install';
|
|
673
663
|
if (ins.devCheckout && ins.skillInstallCmd && ins.skillInstallCmd !== installCmd) {
|
|
674
664
|
installCmd = installCmd + '\\n' + ins.skillInstallCmd;
|
package/src/tag-end.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
// SessionEnd hook:
|
|
1
|
+
// SessionEnd hook: tag from local signals only (learned model, project default). No API calls.
|
|
2
2
|
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
4
3
|
import { discoverSessions } from './discover.js';
|
|
5
4
|
import { parseTranscript } from './parse.js';
|
|
6
|
-
import { loadConfig } from './config.js';
|
|
5
|
+
import { loadConfig, UNTAGGED } from './config.js';
|
|
7
6
|
import { loadTags, setTag, resolveSessionProject } from './tags.js';
|
|
8
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
allowedCategories,
|
|
9
|
+
buildDigest,
|
|
10
|
+
loadLearned,
|
|
11
|
+
predictLearned,
|
|
12
|
+
resolveCategory,
|
|
13
|
+
} from './classifier.js';
|
|
14
|
+
|
|
15
|
+
const SESSION_END_CONFIDENCE = 0.45;
|
|
9
16
|
|
|
10
17
|
export async function readHookPayload() {
|
|
11
18
|
try {
|
|
@@ -19,53 +26,11 @@ export async function readHookPayload() {
|
|
|
19
26
|
}
|
|
20
27
|
}
|
|
21
28
|
|
|
22
|
-
function
|
|
23
|
-
const t = String(text ?? '').toLowerCase();
|
|
24
|
-
for (const id of allowed) {
|
|
25
|
-
if (t === id || t.includes(id)) return id;
|
|
26
|
-
}
|
|
27
|
-
const word = t.match(/[a-z][a-z0-9_-]*/)?.[0];
|
|
28
|
-
if (word && allowed.includes(word)) return word;
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function classifyWithModel(digest, categories, model) {
|
|
33
|
-
const key = process.env.ANTHROPIC_API_KEY;
|
|
34
|
-
if (!key) throw new Error('ANTHROPIC_API_KEY is not set (needed for session-end tagging)');
|
|
35
|
-
const lines = categories.map((c) => `${c.id}: ${c.description || c.label}`).join('\n');
|
|
36
|
-
const prompts = (digest.prompts ?? []).slice(0, 6).map((p, i) => `${i + 1}. ${p}`).join('\n');
|
|
37
|
-
const tools = (digest.tools ?? []).slice(0, 8).map(([s, n]) => `${s} x${n}`).join(', ');
|
|
38
|
-
const user = `Pick exactly one category id for this Claude Code session.
|
|
39
|
-
|
|
40
|
-
Categories (reply with only the id):
|
|
41
|
-
${lines}
|
|
42
|
-
|
|
43
|
-
Session title: ${digest.title ?? '—'}
|
|
44
|
-
Project: ${digest.project ?? '—'}
|
|
45
|
-
User prompts:
|
|
46
|
-
${prompts || '(none)'}
|
|
47
|
-
Tools used: ${tools || '(none)'}`;
|
|
48
|
-
|
|
49
|
-
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
50
|
-
method: 'POST',
|
|
51
|
-
headers: {
|
|
52
|
-
'x-api-key': key,
|
|
53
|
-
'anthropic-version': '2023-06-01',
|
|
54
|
-
'content-type': 'application/json',
|
|
55
|
-
},
|
|
56
|
-
body: JSON.stringify({ model, max_tokens: 40, messages: [{ role: 'user', content: user }] }),
|
|
57
|
-
signal: AbortSignal.timeout(45_000),
|
|
58
|
-
});
|
|
59
|
-
if (!res.ok) throw new Error(`Anthropic API ${res.status}: ${(await res.text()).slice(0, 300)}`);
|
|
60
|
-
const data = await res.json();
|
|
61
|
-
const text = data.content?.find((b) => b.type === 'text')?.text ?? '';
|
|
62
|
-
return text.trim();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export async function tagSessionAtEnd({ sessionId, model } = {}) {
|
|
29
|
+
export async function tagSessionAtEnd({ sessionId } = {}) {
|
|
66
30
|
const cfg = loadConfig();
|
|
67
|
-
|
|
68
|
-
|
|
31
|
+
if ((cfg.tagging?.level ?? 'manual') !== 'session') {
|
|
32
|
+
return { ok: false, skipped: true, reason: 'tagging level is not session' };
|
|
33
|
+
}
|
|
69
34
|
|
|
70
35
|
const id = sessionId ?? process.env.CLAUDE_SESSION_ID;
|
|
71
36
|
if (!id || id.includes('${')) return { ok: false, skipped: true, reason: 'no session id' };
|
|
@@ -84,11 +49,25 @@ export async function tagSessionAtEnd({ sessionId, model } = {}) {
|
|
|
84
49
|
const allowed = allowedCategories(project, cfg);
|
|
85
50
|
const allowedIds = allowed.map((c) => c.id);
|
|
86
51
|
const digest = buildDigest(parsed, { title: s.customTitle ?? parsed.digest.aiTitle, projectName: project.name });
|
|
52
|
+
const learned = loadLearned();
|
|
87
53
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
54
|
+
let resolved = resolveCategory({ tag: existing, project, cfg, digest, learned });
|
|
55
|
+
if (resolved.category === UNTAGGED.id && learned) {
|
|
56
|
+
const p = predictLearned(learned, digest, allowedIds);
|
|
57
|
+
if (p && p.confidence >= SESSION_END_CONFIDENCE) {
|
|
58
|
+
resolved = { category: p.category, source: 'session-end', confidence: p.confidence };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
91
61
|
|
|
92
|
-
|
|
93
|
-
|
|
62
|
+
if (resolved.category === UNTAGGED.id || resolved.source === 'none') {
|
|
63
|
+
return { ok: true, skipped: true, reason: 'no confident category' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setTag(s.sessionId, {
|
|
67
|
+
category: resolved.category,
|
|
68
|
+
source: 'session-end',
|
|
69
|
+
reason: resolved.source === 'project' ? 'project default' : `auto (${resolved.source})`,
|
|
70
|
+
confidence: resolved.confidence ?? null,
|
|
71
|
+
});
|
|
72
|
+
return { ok: true, sessionId: s.sessionId, category: resolved.category, source: resolved.source };
|
|
94
73
|
}
|