@jxgame2020/dsh-token-quota 0.1.21 → 0.1.23

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.
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Live-composed soundtrack driven by host actions (turns, steps, tool calls,
3
+ * etc.). Outputs through Web Audio (built-in synthesis) by default, and also
4
+ * sends MIDI NoteOn/NoteOff to the first available MIDI output when one is
5
+ * detected (both play simultaneously when MIDI is present — use the panel
6
+ * volume for overall level, the MIDI output has its own gain on the receiver).
7
+ *
8
+ * The composition is intentionally simple: a fixed chord progression under a
9
+ * scale-constrained melody, with one short musical gesture per host action.
10
+ * It is meant as gentle ambience while waiting for LLM responses, not as a
11
+ * full piece — everything stays in key so you can never hit a wrong note.
12
+ *
13
+ * @module @jxgame2020/dsh-token-quota/music
14
+ */
15
+ /** Scale degrees in semitones from the root. */
16
+ const SCALES = {
17
+ major: [0, 2, 4, 5, 7, 9, 11],
18
+ minor: [0, 2, 3, 5, 7, 8, 10],
19
+ pentatonic: [0, 2, 4, 7, 9],
20
+ };
21
+ /**
22
+ * Chord progression roots in semitones from the key centre. Loops — every
23
+ * step advance moves one position forward.
24
+ */
25
+ const CHORD_PROGRESSIONS = {
26
+ major: [0, 9, 5, 7], // I - vi - IV - V
27
+ minor: [0, 9, 4, 11], // i - VI - III - VII
28
+ pentatonic: [0, 7, 2, 9], // open fifths feel — root + 5th
29
+ };
30
+ const CHORD_QUALITIES = {
31
+ major: ['major', 'minor', 'major', 'major'],
32
+ minor: ['minor', 'major', 'major', 'major'],
33
+ pentatonic: ['major', 'major', 'major', 'major'],
34
+ };
35
+ /**
36
+ * Turn a MIDI note number into a frequency (Hz) using equal temperament at
37
+ * A4 = 440 Hz.
38
+ */
39
+ function midiToFreq(midi) {
40
+ return 440 * Math.pow(2, (midi - 69) / 12);
41
+ }
42
+ /** Live-composed music engine driven by host actions. */
43
+ export class TokenQuotaMusic {
44
+ audio = null;
45
+ midiOutput = null;
46
+ midiDetected = false;
47
+ enabled = false;
48
+ volume = 0.5;
49
+ style = 'major';
50
+ /** Current position in the chord progression (advances per step). */
51
+ chordIndex = 0;
52
+ /** Step counter — used for melody degree selection. */
53
+ stepCount = 0;
54
+ /** Tool counter — used for tool-note pitch. */
55
+ toolCount = 0;
56
+ /** Base MIDI note of the current key. C4 = middle C. */
57
+ static BASE_MIDI = 60;
58
+ /** True once the audio context has been started (needs a user gesture). */
59
+ get started() {
60
+ return this.audio !== null;
61
+ }
62
+ /**
63
+ * Start the audio context and try to find a MIDI output.
64
+ * Must be called inside a user gesture (the panel switch click) so the
65
+ * AudioContext can resume. Safe to call multiple times.
66
+ */
67
+ async ensureStarted() {
68
+ if (this.audio !== null)
69
+ return true;
70
+ if (typeof window === 'undefined')
71
+ return false;
72
+ try {
73
+ this.audio = new AudioContext();
74
+ await this.audio.resume();
75
+ }
76
+ catch {
77
+ this.audio = null;
78
+ return false;
79
+ }
80
+ // MIDI: try once; we don't re-probe (browser asks for permission only once).
81
+ if (!this.midiDetected && 'requestMIDIAccess' in navigator) {
82
+ this.midiDetected = true;
83
+ try {
84
+ const access = await navigator.requestMIDIAccess();
85
+ for (const output of access.outputs.values()) {
86
+ this.midiOutput = output;
87
+ break;
88
+ }
89
+ }
90
+ catch {
91
+ // Permission denied or no MIDI hardware — stick to Web Audio.
92
+ this.midiOutput = null;
93
+ }
94
+ }
95
+ return true;
96
+ }
97
+ setEnabled(value) {
98
+ this.enabled = value;
99
+ }
100
+ setVolume(value) {
101
+ this.volume = Math.max(0, Math.min(1, value));
102
+ }
103
+ setStyle(value) {
104
+ this.style = value;
105
+ }
106
+ /** Handle one incoming host action and turn it into sound. */
107
+ onAction(action) {
108
+ if (!this.enabled || this.audio === null)
109
+ return;
110
+ const base = TokenQuotaMusic.BASE_MIDI;
111
+ switch (action.type) {
112
+ case 'turn/start':
113
+ this.chordIndex = 0;
114
+ this.stepCount = 0;
115
+ this.toolCount = 0;
116
+ this.playChord(base, this.currentChordQuality(), 1.6, 0.55);
117
+ break;
118
+ case 'step/start': {
119
+ this.chordIndex = (this.chordIndex + 1) % CHORD_PROGRESSIONS[this.style].length;
120
+ this.stepCount += 1;
121
+ // bass note of the new chord
122
+ this.playNote(this.currentChordRoot(base) - 12, 1.2, 0.35);
123
+ // ascending arpeggio of the chord (3 notes)
124
+ const root = this.currentChordRoot(base);
125
+ const [d1, d2, d3] = this.arpeggioDegrees();
126
+ setTimeout(() => this.playNote(root + d1, 0.2, 0.4), 0);
127
+ setTimeout(() => this.playNote(root + d2, 0.2, 0.4), 80);
128
+ setTimeout(() => this.playNote(root + d3, 0.35, 0.4), 160);
129
+ break;
130
+ }
131
+ case 'request/header': {
132
+ // short melodic flicker — pitch follows step + tool positions
133
+ const degree = (this.stepCount * 2 + this.toolCount) % SCALES[this.style].length;
134
+ const note = base + 12 + SCALES[this.style][degree];
135
+ this.playNote(note, 0.18, 0.35);
136
+ break;
137
+ }
138
+ case 'tool/call':
139
+ this.toolCount += 1;
140
+ // high plink + a low percussive tap
141
+ this.playNote(base + 24 + (this.toolCount % 3) * 2, 0.12, 0.45);
142
+ this.playNote(base - 12, 0.08, 0.25);
143
+ break;
144
+ case 'tool/result': {
145
+ if (action.error) {
146
+ // dissonant downward glint (two half-steps)
147
+ this.playNote(base + 13, 0.18, 0.4);
148
+ setTimeout(() => this.playNote(base + 12, 0.25, 0.4), 90);
149
+ }
150
+ else {
151
+ // upward resolution
152
+ this.playNote(base + 9, 0.14, 0.3);
153
+ setTimeout(() => this.playNote(base + 11, 0.22, 0.3), 70);
154
+ }
155
+ break;
156
+ }
157
+ case 'assistant/message':
158
+ if (action.interrupted) {
159
+ // interrupted turn: diminished tension chord
160
+ this.playChord(base + 7, 'dim', 0.9, 0.5);
161
+ }
162
+ else {
163
+ // full cadence chord of the current step
164
+ this.playChord(this.currentChordRoot(base), this.currentChordQuality(), 1.1, 0.55);
165
+ }
166
+ break;
167
+ case 'turn/end':
168
+ // final tonic chord, longer release
169
+ this.playChord(base, this.style === 'minor' ? 'minor' : 'major', 2.2, 0.65);
170
+ this.chordIndex = 0;
171
+ break;
172
+ }
173
+ }
174
+ // ── internal helpers ──────────────────────────────────────────────
175
+ currentChordRoot(base) {
176
+ const prog = CHORD_PROGRESSIONS[this.style];
177
+ return base + prog[this.chordIndex % prog.length];
178
+ }
179
+ currentChordQuality() {
180
+ const q = CHORD_QUALITIES[this.style];
181
+ return q[this.chordIndex % q.length] ?? 'major';
182
+ }
183
+ /** Intervals of an ascending arpeggio from the chord root. */
184
+ arpeggioDegrees() {
185
+ return this.style === 'pentatonic'
186
+ ? [0, 7, 12]
187
+ : this.currentChordQuality() === 'major'
188
+ ? [0, 4, 7]
189
+ : [0, 3, 7];
190
+ }
191
+ /** Play one note: Web Audio + MIDI simultaneously when available. */
192
+ playNote(midi, durationSec, velocity) {
193
+ if (!this.audio)
194
+ return;
195
+ const now = this.audio.currentTime;
196
+ const peak = this.volume * velocity * 0.5;
197
+ if (peak <= 0)
198
+ return;
199
+ // Web Audio: sine oscillator with a quick attack and exponential release.
200
+ const osc = this.audio.createOscillator();
201
+ osc.type = 'sine';
202
+ osc.frequency.value = midiToFreq(midi);
203
+ const gain = this.audio.createGain();
204
+ gain.gain.setValueAtTime(0, now);
205
+ gain.gain.linearRampToValueAtTime(peak, now + 0.008);
206
+ gain.gain.exponentialRampToValueAtTime(0.001, now + durationSec);
207
+ osc.connect(gain);
208
+ gain.connect(this.audio.destination);
209
+ osc.start(now);
210
+ osc.stop(now + durationSec + 0.05);
211
+ // MIDI: same note on the first output, if any. Velocity 1..127.
212
+ if (this.midiOutput !== null) {
213
+ const vel = Math.max(1, Math.floor(velocity * 127));
214
+ this.midiOutput.send([0x90, midi & 0x7F, vel]);
215
+ setTimeout(() => {
216
+ this.midiOutput?.send([0x80, midi & 0x7F, 0]);
217
+ }, durationSec * 1000);
218
+ }
219
+ }
220
+ /** Play a three-note chord plus a bass octave below. */
221
+ playChord(rootMidi, quality, durationSec, velocity) {
222
+ const third = quality === 'major' ? 4 : 3;
223
+ const fifth = quality === 'dim' ? 6 : 7;
224
+ this.playNote(rootMidi, durationSec, velocity * 0.45);
225
+ this.playNote(rootMidi + third, durationSec, velocity * 0.35);
226
+ this.playNote(rootMidi + fifth, durationSec, velocity * 0.35);
227
+ // bass in the octave below — slightly longer for warmth
228
+ this.playNote(rootMidi - 12, durationSec * 1.15, velocity * 0.45);
229
+ }
230
+ }
231
+ //# sourceMappingURL=music.js.map
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { type EngineStoreHandle } from '@deepseek-ai/dsh-client-store';
11
11
  import type { ModelProviderGroup, ModelSelection } from '@deepseek-ai/dsh-api-session-controller/types';
12
- import type { TokenQuotaFullAction, TokenQuotaLog, TokenQuotaReset, TokenQuotaSnapshot, TokenQuotaUpgrade } from '@jxgame2020/dsh-token-quota/types';
12
+ import type { TokenQuotaFullAction, TokenQuotaLog, TokenQuotaMusicStyle, TokenQuotaReset, TokenQuotaSnapshot, TokenQuotaUpgrade } from '@jxgame2020/dsh-token-quota/types';
13
13
  /** One rendered model row: directory identity merged with the quota snapshot. */
14
14
  export interface ModelQuotaRow {
15
15
  /** Stable key `provider/model`. */
@@ -50,6 +50,10 @@ export interface TokenQuotaPanelState {
50
50
  checkUpdates: boolean;
51
51
  /** Whether the panel dims while the pointer is away / while typing. */
52
52
  dimWhenIdle: boolean;
53
+ /** Live music settings (mirrored from the settings document). */
54
+ musicEnabled: boolean;
55
+ musicVolume: number;
56
+ musicStyle: TokenQuotaMusicStyle;
53
57
  /** Latest upgrade availability from the Host; null = up to date or unknown. */
54
58
  upgrade: TokenQuotaUpgrade | null;
55
59
  /** Whether the one-shot upgrade banner was dismissed for the current latest version. */
@@ -92,6 +96,9 @@ export type TokenQuotaPanelActions = {
92
96
  setSettings: (d: TokenQuotaPanelState, monitored: string[] | null, onFull: TokenQuotaFullAction) => void;
93
97
  setCheckUpdates: (d: TokenQuotaPanelState, checkUpdates: boolean) => void;
94
98
  setDimWhenIdle: (d: TokenQuotaPanelState, dimWhenIdle: boolean) => void;
99
+ setMusicEnabled: (d: TokenQuotaPanelState, enabled: boolean) => void;
100
+ setMusicVolume: (d: TokenQuotaPanelState, volume: number) => void;
101
+ setMusicStyle: (d: TokenQuotaPanelState, style: TokenQuotaMusicStyle) => void;
95
102
  setUpgrade: (d: TokenQuotaPanelState, upgrade: TokenQuotaUpgrade | null) => void;
96
103
  setUpgradeDismissed: (d: TokenQuotaPanelState, dismissed: boolean) => void;
97
104
  setCheckingUpdates: (d: TokenQuotaPanelState, checking: boolean) => void;
@@ -28,6 +28,9 @@ export function createTokenQuotaPanelStore() {
28
28
  onFull: 'stop',
29
29
  checkUpdates: true,
30
30
  dimWhenIdle: false,
31
+ musicEnabled: false,
32
+ musicVolume: 0.5,
33
+ musicStyle: 'major',
31
34
  upgrade: null,
32
35
  upgradeDismissed: false,
33
36
  checkingUpdates: false,
@@ -53,6 +56,9 @@ export function createTokenQuotaPanelStore() {
53
56
  setSettings: (d, monitored, onFull) => { d.monitored = monitored; d.onFull = onFull; },
54
57
  setCheckUpdates: (d, checkUpdates) => { d.checkUpdates = checkUpdates; },
55
58
  setDimWhenIdle: (d, dimWhenIdle) => { d.dimWhenIdle = dimWhenIdle; },
59
+ setMusicEnabled: (d, enabled) => { d.musicEnabled = enabled; },
60
+ setMusicVolume: (d, volume) => { d.musicVolume = volume; },
61
+ setMusicStyle: (d, style) => { d.musicStyle = style; },
56
62
  setUpgrade: (d, upgrade) => {
57
63
  // Guard against host/client version skew (e.g. the host process still
58
64
  // runs an older bundle after an upgrade): when the reported "latest"
@@ -81,6 +81,10 @@ export declare class TokenQuotaService extends Service {
81
81
  private disposeRouteCheck;
82
82
  /** Disposer for the optional clear-log route (`POST /token-quota/clear-log`). */
83
83
  private disposeRouteClear;
84
+ /** Disposer for the optional music-event route (`GET /token-quota/events`, SSE). */
85
+ private disposeRouteEvents;
86
+ /** Live SSE subscribers receiving streamed music actions. */
87
+ private readonly musicClients;
84
88
  /** Disposer for the webServer-arrival watcher when the service mounts later. */
85
89
  private disposeRouteWatcher;
86
90
  /**
@@ -90,6 +94,8 @@ export declare class TokenQuotaService extends Service {
90
94
  constructor(ctx: Context, config?: TokenQuotaConfig);
91
95
  /** Serve the current snapshot over the plugin-owned HTTP route. */
92
96
  private tryMountRoute;
97
+ /** Broadcast one action to every open SSE music subscriber. */
98
+ private pushMusicAction;
93
99
  /** Query the npm registry and rebuild the cached upgrade info (coalesced). */
94
100
  private refreshUpgrade;
95
101
  private doRefreshUpgrade;
@@ -38,7 +38,7 @@ import z from '@deepseek-ai/schemastery';
38
38
  // the service listens to without importing any runtime value.
39
39
  import '@deepseek-ai/dsh-agent';
40
40
  import { LlmError } from '@deepseek-ai/dsh-llm';
41
- import { TOKEN_QUOTA_EXCEEDED_CODE, TOKEN_QUOTA_NAMESPACE, tokenQuotaKey, } from "./types.js";
41
+ import { TOKEN_QUOTA_EXCEEDED_CODE, TOKEN_QUOTA_NAMESPACE, tokenQuotaKey, TOKEN_QUOTA_DEFAULT_MUSIC, } from "./types.js";
42
42
  import { assertTokenQuotaLimit, splitTokenQuotaKey } from "./invariant.js";
43
43
  /** The plugin's package name, used to locate the profile dependency and the registry endpoint. */
44
44
  const PACKAGE_NAME = '@jxgame2020/dsh-token-quota';
@@ -206,6 +206,12 @@ const TOKEN_QUOTA_SETTINGS_SCHEMA = z.object({
206
206
  // panel writes it and the host validates the shape at runtime. `z.any` with
207
207
  // a null default keeps it out of the strict fields above.
208
208
  reset: z.any().default(null),
209
+ // Live music: host persists the document, the browser half plays it.
210
+ music: z.object({
211
+ enabled: z.boolean().default(TOKEN_QUOTA_DEFAULT_MUSIC.enabled),
212
+ volume: z.number().min(0).max(1).default(TOKEN_QUOTA_DEFAULT_MUSIC.volume),
213
+ style: z.union(['major', 'minor', 'pentatonic']).default(TOKEN_QUOTA_DEFAULT_MUSIC.style),
214
+ }).default({ ...TOKEN_QUOTA_DEFAULT_MUSIC }),
209
215
  });
210
216
  /** Two-digit zero-pad helper. */
211
217
  function pad2(value) {
@@ -266,7 +272,7 @@ export class TokenQuotaService extends Service {
266
272
  history = {};
267
273
  limits = {};
268
274
  monitored = undefined;
269
- settingsSource = () => ({ limits: {}, monitored: [], onFull: 'stop', checkUpdates: true, dimWhenIdle: false });
275
+ settingsSource = () => ({ limits: {}, monitored: [], onFull: 'stop', checkUpdates: true, dimWhenIdle: false, music: { ...TOKEN_QUOTA_DEFAULT_MUSIC } });
270
276
  /** Whether update checks are enabled (mirrors the settings document). */
271
277
  checkUpdates = true;
272
278
  /** Cached upgrade availability; recomputed by {@link refreshUpgrade}. */
@@ -293,6 +299,10 @@ export class TokenQuotaService extends Service {
293
299
  disposeRouteCheck;
294
300
  /** Disposer for the optional clear-log route (`POST /token-quota/clear-log`). */
295
301
  disposeRouteClear;
302
+ /** Disposer for the optional music-event route (`GET /token-quota/events`, SSE). */
303
+ disposeRouteEvents;
304
+ /** Live SSE subscribers receiving streamed music actions. */
305
+ musicClients = new Set();
296
306
  /** Disposer for the webServer-arrival watcher when the service mounts later. */
297
307
  disposeRouteWatcher;
298
308
  /**
@@ -327,6 +337,11 @@ export class TokenQuotaService extends Service {
327
337
  checkUpdates: doc.checkUpdates ?? true,
328
338
  dimWhenIdle: doc.dimWhenIdle ?? false,
329
339
  reset: doc.reset ?? undefined,
340
+ music: {
341
+ enabled: doc.music?.enabled ?? TOKEN_QUOTA_DEFAULT_MUSIC.enabled,
342
+ volume: doc.music?.volume ?? TOKEN_QUOTA_DEFAULT_MUSIC.volume,
343
+ style: doc.music?.style ?? TOKEN_QUOTA_DEFAULT_MUSIC.style,
344
+ },
330
345
  };
331
346
  };
332
347
  },
@@ -463,6 +478,34 @@ export class TokenQuotaService extends Service {
463
478
  });
464
479
  },
465
480
  });
481
+ this.disposeRouteEvents = server.register({
482
+ kind: 'exact',
483
+ path: '/token-quota/events',
484
+ handler: (req, res) => {
485
+ res.writeHead(200, {
486
+ 'content-type': 'text/event-stream',
487
+ 'cache-control': 'no-store',
488
+ 'connection': 'keep-alive',
489
+ });
490
+ this.musicClients.add(res);
491
+ res.write(': token-quota music stream\n\n');
492
+ req.on('close', () => { this.musicClients.delete(res); });
493
+ },
494
+ });
495
+ }
496
+ /** Broadcast one action to every open SSE music subscriber. */
497
+ pushMusicAction(action) {
498
+ if (this.musicClients.size === 0)
499
+ return;
500
+ const data = `event: action\ndata: ${JSON.stringify(action)}\n\n`;
501
+ for (const res of this.musicClients) {
502
+ try {
503
+ res.write(data);
504
+ }
505
+ catch {
506
+ this.musicClients.delete(res);
507
+ }
508
+ }
466
509
  }
467
510
  /** Query the npm registry and rebuild the cached upgrade info (coalesced). */
468
511
  refreshUpgrade() {
@@ -624,23 +667,45 @@ export class TokenQuotaService extends Service {
624
667
  return typeof limit === 'number' && Number.isInteger(limit) && limit > 0 ? limit : 0;
625
668
  }
626
669
  onSessionEvent(session, event) {
627
- if (event.type === 'request/header') {
628
- const { provider, model } = event.data.header.config;
629
- this.headerKeys.set(session, tokenQuotaKey(provider, model));
630
- return;
631
- }
632
- if (event.type === 'assistant/message' && event.data.usage !== undefined) {
633
- const key = this.headerKeys.get(session);
634
- if (key === undefined)
635
- return;
636
- if (!this.isMonitored(key))
637
- return;
638
- const tokens = usageTokens(event.data.usage);
639
- if (tokens <= 0)
670
+ const sessionId = String(session.id);
671
+ switch (event.type) {
672
+ case 'turn/start':
673
+ this.pushMusicAction({ type: 'turn/start', sessionId });
674
+ break;
675
+ case 'turn/end':
676
+ this.pushMusicAction({ type: 'turn/end', sessionId, reason: event.data.reason.kind });
677
+ break;
678
+ case 'step/start':
679
+ this.pushMusicAction({ type: 'step/start', sessionId });
680
+ break;
681
+ case 'request/header': {
682
+ const { provider, model } = event.data.header.config;
683
+ this.headerKeys.set(session, tokenQuotaKey(provider, model));
684
+ this.pushMusicAction({ type: 'request/header', sessionId, provider, model });
640
685
  return;
641
- this.rollCycleIfNeeded();
642
- this.usage[key] = (this.usage[key] ?? 0) + tokens;
643
- this.scheduleWrite();
686
+ }
687
+ case 'tool/call':
688
+ this.pushMusicAction({ type: 'tool/call', sessionId, name: event.data.name });
689
+ break;
690
+ case 'tool/result':
691
+ this.pushMusicAction({ type: 'tool/result', sessionId, error: event.data.error !== undefined });
692
+ break;
693
+ case 'assistant/message':
694
+ this.pushMusicAction({ type: 'assistant/message', sessionId, interrupted: event.data.interrupted === true });
695
+ if (event.data.usage !== undefined) {
696
+ const key = this.headerKeys.get(session);
697
+ if (key === undefined)
698
+ return;
699
+ if (!this.isMonitored(key))
700
+ return;
701
+ const tokens = usageTokens(event.data.usage);
702
+ if (tokens <= 0)
703
+ return;
704
+ this.rollCycleIfNeeded();
705
+ this.usage[key] = (this.usage[key] ?? 0) + tokens;
706
+ this.scheduleWrite();
707
+ }
708
+ break;
644
709
  }
645
710
  }
646
711
  /**
@@ -849,6 +914,15 @@ export class TokenQuotaService extends Service {
849
914
  this.disposeRouteCheck();
850
915
  if (this.disposeRouteClear !== undefined)
851
916
  this.disposeRouteClear();
917
+ if (this.disposeRouteEvents !== undefined)
918
+ this.disposeRouteEvents();
919
+ for (const res of this.musicClients) {
920
+ try {
921
+ res.end();
922
+ }
923
+ catch { /* ignore */ }
924
+ }
925
+ this.musicClients.clear();
852
926
  this.flush();
853
927
  }
854
928
  safeSplit(key) {
@@ -38,6 +38,54 @@ export interface TokenQuotaReset {
38
38
  }
39
39
  /** Default reset: machine-local midnight (offset = host timezone, 00:00). */
40
40
  export declare const TOKEN_QUOTA_DEFAULT_RESET: TokenQuotaReset;
41
+ /** Harmonic palette of the live-composed soundtrack. */
42
+ export declare const TOKEN_QUOTA_MUSIC_STYLES: readonly ["major", "minor", "pentatonic"];
43
+ /** One of the {@link TOKEN_QUOTA_MUSIC_STYLES} values. */
44
+ export type TokenQuotaMusicStyle = typeof TOKEN_QUOTA_MUSIC_STYLES[number];
45
+ /** Music playback settings (host-persisted; the browser half plays). */
46
+ export interface TokenQuotaMusicSettings {
47
+ /** Master switch — off by default so the plugin stays silent until enabled. */
48
+ enabled: boolean;
49
+ /** Output volume 0..1. */
50
+ volume: number;
51
+ /** Harmonic style of the live composition. */
52
+ style: TokenQuotaMusicStyle;
53
+ }
54
+ /** Default music settings: silent until the user flips the panel switch. */
55
+ export declare const TOKEN_QUOTA_DEFAULT_MUSIC: TokenQuotaMusicSettings;
56
+ /**
57
+ * One host action streamed to the browser over the plugin's SSE route
58
+ * (`GET /token-quota/events`); the panel's music engine turns it into a note.
59
+ * `sessionId` lets the client filter to the currently-visible session.
60
+ */
61
+ export type TokenQuotaMusicAction = {
62
+ type: 'turn/start';
63
+ sessionId: string;
64
+ } | {
65
+ type: 'turn/end';
66
+ sessionId: string;
67
+ reason: string;
68
+ } | {
69
+ type: 'step/start';
70
+ sessionId: string;
71
+ } | {
72
+ type: 'request/header';
73
+ sessionId: string;
74
+ provider: string;
75
+ model: string;
76
+ } | {
77
+ type: 'tool/call';
78
+ sessionId: string;
79
+ name: string;
80
+ } | {
81
+ type: 'tool/result';
82
+ sessionId: string;
83
+ error: boolean;
84
+ } | {
85
+ type: 'assistant/message';
86
+ sessionId: string;
87
+ interrupted: boolean;
88
+ };
41
89
  /**
42
90
  * Per-model daily-limit settings document shape. Keys are `provider/model`
43
91
  * and a value of `0` (or an absent key) means unlimited for that model.
@@ -79,6 +127,12 @@ export interface TokenQuotaSettings {
79
127
  * midnight (the historical behaviour).
80
128
  */
81
129
  reset?: TokenQuotaReset;
130
+ /**
131
+ * Live music output settings: a soundtrack composed from host actions
132
+ * (request starts, tool calls, turn ends) and played through Web Audio or
133
+ * an attached MIDI output. Off by default.
134
+ */
135
+ music: TokenQuotaMusicSettings;
82
136
  }
83
137
  /** Token-quota plugin configuration. */
84
138
  export interface TokenQuotaConfig {
@@ -22,6 +22,14 @@ export const TOKEN_QUOTA_EXCEEDED_CODE = 'TOKEN_QUOTA_EXCEEDED';
22
22
  export const TOKEN_QUOTA_FULL_ACTIONS = ['stop', 'switchQuota', 'switchAll'];
23
23
  /** Default reset: machine-local midnight (offset = host timezone, 00:00). */
24
24
  export const TOKEN_QUOTA_DEFAULT_RESET = { offsetHours: 0, hour: 0, minute: 0 };
25
+ /** Harmonic palette of the live-composed soundtrack. */
26
+ export const TOKEN_QUOTA_MUSIC_STYLES = ['major', 'minor', 'pentatonic'];
27
+ /** Default music settings: silent until the user flips the panel switch. */
28
+ export const TOKEN_QUOTA_DEFAULT_MUSIC = {
29
+ enabled: false,
30
+ volume: 0.5,
31
+ style: 'major',
32
+ };
25
33
  /** Build the stable per-model key shared by the counter, settings, and snapshot. */
26
34
  export function tokenQuotaKey(provider, model) {
27
35
  return `${provider}/${model}`;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jxgame2020/dsh-token-quota",
3
3
  "description": "Daily per-model token quota accounting, enforcement, and a floating quota panel for the DeepSeek Harness — a fully self-contained plugin (no core-Harness wiring, no generated RPC contract)",
4
- "version": "0.1.21",
4
+ "version": "0.1.23",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },