dsh-claude-style 0.15.0-dev.22 → 0.16.0-dev.27

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/lib/host/brand.js CHANGED
@@ -23,7 +23,7 @@ var PEAKRATE_PATH = `${ROUTE_PREFIX}/peakrate`;
23
23
  // packages/host/src/brand.ts
24
24
  import { mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
25
25
  import { join } from "node:path";
26
- import { harnessPath } from "./harness-home.js";
26
+ import { pluginPath } from "./harness-home.js";
27
27
  import { fenceRefused, methodRefused, readRequestBody, sendJson } from "./http.js";
28
28
  var IMAGE_PATHS = { mark: BRAND_MARK_PATH, wordmark: BRAND_WORDMARK_PATH };
29
29
  var IMAGE_HEADERS = {
@@ -36,7 +36,7 @@ var SIGNATURES = {
36
36
  "image/webp": [{ offset: 0, bytes: Buffer.from("RIFF", "latin1") }, { offset: 8, bytes: Buffer.from("WEBP", "latin1") }]
37
37
  };
38
38
  function createBrandStore(ctx) {
39
- const dir = () => harnessPath(ctx, "dsh-claude-style", "brand");
39
+ const dir = () => pluginPath(ctx, "brand");
40
40
  const find = (kind) => {
41
41
  for (const [type, extension] of Object.entries(BRAND_IMAGE_TYPES)) {
42
42
  const path = join(dir(), `${kind}.${extension}`);
@@ -20,8 +20,10 @@ function writeJsonDocument(ctx, path, label, document) {
20
20
  mkdirSync(dirname(path), { recursive: true });
21
21
  writeFileSync(temporary, JSON.stringify(document), "utf8");
22
22
  renameSync(temporary, path);
23
+ return true;
23
24
  } catch (error) {
24
25
  ctx.logger?.warn?.(`dsh-claude-style: ${label} not written: ${error.message}`);
26
+ return false;
25
27
  }
26
28
  }
27
29
  export {
@@ -6,6 +6,10 @@ function harnessPath(ctx, ...segments) {
6
6
  if (typeof resolvePath === "function") return resolvePath(...segments);
7
7
  return join(process.env.DSH_HOME ?? join(homedir(), ".dsh"), ...segments);
8
8
  }
9
+ function pluginPath(ctx, ...segments) {
10
+ return harnessPath(ctx, "dsh-claude-style", ...segments);
11
+ }
9
12
  export {
10
- harnessPath
13
+ harnessPath,
14
+ pluginPath
11
15
  };
@@ -17,7 +17,7 @@ import { userInfo } from "node:os";
17
17
  import { join, resolve, sep } from "node:path";
18
18
  import { brotliDecompressSync } from "node:zlib";
19
19
  import { brandRoutes } from "./brand.js";
20
- import { harnessPath } from "./harness-home.js";
20
+ import { harnessPath, pluginPath } from "./harness-home.js";
21
21
  import { fenceRefused, methodRefused, readRequestBody, refusalOf, sendJson } from "./http.js";
22
22
  import { createHdslAccount } from "./hdsl.js";
23
23
  import { packageRoot } from "./package-root.js";
@@ -110,7 +110,7 @@ function registerRoutes(ctx, scope) {
110
110
  const root = packageRoot();
111
111
  const file = join(root, "lib", COPY_FILE);
112
112
  const fontsDir = join(root, "lib", "fonts");
113
- const userFontsDir = () => harnessPath(ctx, "dsh-claude-style", "fonts");
113
+ const userFontsDir = () => pluginPath(ctx, "fonts");
114
114
  const assetsDir = join(root, "lib", "assets");
115
115
  const unpressed = /* @__PURE__ */ new Map();
116
116
  const sendFile = (res, method, path, headers, body = null) => {
@@ -0,0 +1,235 @@
1
+ // packages/host/src/usage-record.ts
2
+ import { existsSync, renameSync, unlinkSync } from "node:fs";
3
+ import { cachePath, readJsonDocument, writeJsonDocument } from "./cache-file.js";
4
+ import { pluginPath } from "./harness-home.js";
5
+ import { BUCKET_KEYS, addBuckets, emptyBuckets } from "./usage-ledger.js";
6
+ var RECORD_VERSION = 1;
7
+ var RECORD_FILE = "usage.json";
8
+ var LEGACY_CACHE_VERSION = 4;
9
+ var NO_ROUTE = "";
10
+ var CELL_KEYS = [...BUCKET_KEYS, "calls"];
11
+ function bucketsFromCell(cell) {
12
+ if (!Array.isArray(cell)) return null;
13
+ const buckets = emptyBuckets();
14
+ for (let at = 0; at < CELL_KEYS.length; at += 1) {
15
+ const value = Number(cell[at]);
16
+ if (Number.isFinite(value)) buckets[CELL_KEYS[at]] = value;
17
+ }
18
+ return buckets;
19
+ }
20
+ function cellFromBuckets(buckets) {
21
+ return CELL_KEYS.map((key) => buckets[key]);
22
+ }
23
+ function hoursFromSlots(raw) {
24
+ if (raw === null || typeof raw !== "object") return null;
25
+ const hours = new Array(24).fill(0);
26
+ let settled = false;
27
+ for (const [slot, count] of Object.entries(raw)) {
28
+ const hour = Number(slot);
29
+ const value = Number(count);
30
+ if (!Number.isInteger(hour) || hour < 0 || hour > 23 || !Number.isFinite(value) || value === 0) continue;
31
+ hours[hour] += value;
32
+ settled = true;
33
+ }
34
+ return settled ? hours : null;
35
+ }
36
+ function slotsFromHours(hours) {
37
+ if (!Array.isArray(hours)) return null;
38
+ const slots = {};
39
+ let settled = false;
40
+ for (let hour = 0; hour < 24; hour += 1) {
41
+ const count = Number(hours[hour]) || 0;
42
+ if (count === 0) continue;
43
+ slots[String(hour)] = count;
44
+ settled = true;
45
+ }
46
+ return settled ? slots : null;
47
+ }
48
+ function isEmptyDay(buckets, hours) {
49
+ if (hours !== null) return false;
50
+ return BUCKET_KEYS.every((key) => buckets[key] === 0) && buckets.calls === 0;
51
+ }
52
+ function storedDay(raw) {
53
+ return raw !== null && typeof raw === "object" ? raw : null;
54
+ }
55
+ function daysFromStored(raw) {
56
+ const days = /* @__PURE__ */ new Map();
57
+ if (raw === null || typeof raw !== "object") return days;
58
+ for (const [day, value] of Object.entries(raw)) {
59
+ const stored = storedDay(value);
60
+ if (stored === null) continue;
61
+ const buckets = emptyBuckets();
62
+ const models = /* @__PURE__ */ new Map();
63
+ if (stored.m !== null && typeof stored.m === "object") {
64
+ for (const [route, cell] of Object.entries(stored.m)) {
65
+ const counts = bucketsFromCell(cell);
66
+ if (counts === null) continue;
67
+ addBuckets(buckets, counts, 1);
68
+ if (route !== NO_ROUTE) models.set(route, counts);
69
+ }
70
+ }
71
+ const hours = hoursFromSlots(stored.h);
72
+ if (isEmptyDay(buckets, hours)) continue;
73
+ if (models.size > 0) buckets.models = models;
74
+ if (hours !== null) buckets.hours = hours;
75
+ days.set(day, buckets);
76
+ }
77
+ return days;
78
+ }
79
+ function dayToStored(buckets) {
80
+ const rest = emptyBuckets();
81
+ addBuckets(rest, buckets, 1);
82
+ const routes = {};
83
+ if (buckets.models !== void 0) {
84
+ for (const [model, cell] of buckets.models) {
85
+ routes[model] = cellFromBuckets(cell);
86
+ addBuckets(rest, cell, -1);
87
+ }
88
+ }
89
+ if (CELL_KEYS.some((key) => rest[key] !== 0)) routes[NO_ROUTE] = cellFromBuckets(rest);
90
+ const stored = { m: routes };
91
+ const hours = slotsFromHours(buckets.hours);
92
+ if (hours !== null) stored.h = hours;
93
+ return stored;
94
+ }
95
+ function sessionToStored(entry) {
96
+ const stored = {};
97
+ if (entry.fingerprint !== void 0) {
98
+ stored.size = entry.fingerprint.size;
99
+ stored.mtimeMs = entry.fingerprint.mtimeMs;
100
+ }
101
+ if (entry.days.size === 0) return stored;
102
+ const days = {};
103
+ for (const [day, buckets] of entry.days) days[day] = dayToStored(buckets);
104
+ stored.days = days;
105
+ return stored;
106
+ }
107
+ function sessionFromStored(value) {
108
+ if (value === null || typeof value !== "object") return null;
109
+ const stored = value;
110
+ const days = daysFromStored(stored.days);
111
+ if (!Number.isFinite(stored.size) || !Number.isFinite(stored.mtimeMs)) {
112
+ return days.size === 0 ? null : { days };
113
+ }
114
+ return { fingerprint: { size: Number(stored.size), mtimeMs: Number(stored.mtimeMs) }, days };
115
+ }
116
+ function hoursFromArray(raw) {
117
+ if (!Array.isArray(raw) || raw.length !== 24) return null;
118
+ const hours = new Array(24).fill(0);
119
+ let settled = false;
120
+ for (let hour = 0; hour < 24; hour += 1) {
121
+ const count = Number(raw[hour]) || 0;
122
+ hours[hour] = count;
123
+ if (count !== 0) settled = true;
124
+ }
125
+ return settled ? hours : null;
126
+ }
127
+ function legacyDays(raw) {
128
+ const days = /* @__PURE__ */ new Map();
129
+ if (raw === null || typeof raw !== "object") return days;
130
+ for (const [day, value] of Object.entries(raw)) {
131
+ const stored = storedDay(value);
132
+ if (stored === null) continue;
133
+ const buckets = emptyBuckets();
134
+ addBuckets(buckets, stored, 1);
135
+ if (stored.models !== null && typeof stored.models === "object") {
136
+ const models = /* @__PURE__ */ new Map();
137
+ for (const [model, cell] of Object.entries(stored.models)) {
138
+ if (cell === null || typeof cell !== "object") continue;
139
+ const counts = emptyBuckets();
140
+ addBuckets(counts, cell, 1);
141
+ models.set(model, counts);
142
+ }
143
+ if (models.size > 0) buckets.models = models;
144
+ }
145
+ const hours = hoursFromArray(stored.hours);
146
+ if (isEmptyDay(buckets, hours)) continue;
147
+ if (hours !== null) buckets.hours = hours;
148
+ days.set(day, buckets);
149
+ }
150
+ return days;
151
+ }
152
+ function legacyCachePath(ctx) {
153
+ return cachePath(ctx, RECORD_FILE);
154
+ }
155
+ function legacyRecord(ctx) {
156
+ const path = legacyCachePath(ctx);
157
+ if (!existsSync(path)) return null;
158
+ const sessions = /* @__PURE__ */ new Map();
159
+ const parsed = readJsonDocument(ctx, path, "usage cache");
160
+ if (parsed === null || typeof parsed !== "object") return sessions;
161
+ const document = parsed;
162
+ if (document.version !== LEGACY_CACHE_VERSION) return sessions;
163
+ if (document.sessions === null || typeof document.sessions !== "object") return sessions;
164
+ for (const [id, value] of Object.entries(document.sessions)) {
165
+ if (value === null || typeof value !== "object") continue;
166
+ const entry = value;
167
+ if (!Number.isFinite(entry.size) || !Number.isFinite(entry.mtimeMs)) continue;
168
+ const days = legacyDays(entry.days);
169
+ if (days.size === 0) continue;
170
+ sessions.set(id, { fingerprint: { size: Number(entry.size), mtimeMs: Number(entry.mtimeMs) }, days });
171
+ }
172
+ return sessions;
173
+ }
174
+ function readStored(ctx) {
175
+ const path = pluginPath(ctx, RECORD_FILE);
176
+ const parsed = readJsonDocument(ctx, path, "usage record");
177
+ if (parsed === null || typeof parsed !== "object") {
178
+ if (existsSync(path)) {
179
+ const aside = `${path}.corrupt-${Date.now()}`;
180
+ renameSync(path, aside);
181
+ ctx.logger?.warn?.(`dsh-claude-style: usage record moved aside to ${aside}; it is rebuilt from the session logs`);
182
+ }
183
+ return { kind: "absent" };
184
+ }
185
+ const document = parsed;
186
+ if (document.version !== RECORD_VERSION) {
187
+ ctx.logger?.warn?.(`dsh-claude-style: usage record version ${String(document.version)} is not one this build reads; it is left as it stands`);
188
+ return { kind: "foreign" };
189
+ }
190
+ const sessions = /* @__PURE__ */ new Map();
191
+ if (document.sessions === null || typeof document.sessions !== "object") return { kind: "ours", sessions };
192
+ for (const [id, value] of Object.entries(document.sessions)) {
193
+ const entry = sessionFromStored(value);
194
+ if (entry !== null) sessions.set(id, entry);
195
+ }
196
+ return { kind: "ours", sessions };
197
+ }
198
+ function readRecord(ctx) {
199
+ const stored = readStored(ctx);
200
+ if (stored.kind === "foreign") return { sessions: /* @__PURE__ */ new Map(), mustWrite: false, writable: false };
201
+ const sessions = stored.kind === "ours" ? stored.sessions : /* @__PURE__ */ new Map();
202
+ const legacy = legacyRecord(ctx);
203
+ for (const [id, entry] of legacy ?? []) {
204
+ if (!sessions.has(id)) sessions.set(id, entry);
205
+ }
206
+ return { sessions, mustWrite: stored.kind === "absent" || legacy !== null, writable: true };
207
+ }
208
+ function overStored(stored, sessions, live) {
209
+ const merged = new Map(sessions);
210
+ for (const [id, held] of stored) {
211
+ if (sessions.get(id)?.fingerprint !== void 0) continue;
212
+ if (live.has(id)) merged.set(id, held);
213
+ else if (held.days.size > 0) merged.set(id, { days: held.days });
214
+ }
215
+ return merged;
216
+ }
217
+ function writeRecord(ctx, sessions, live) {
218
+ const stored = readStored(ctx);
219
+ if (stored.kind === "foreign") return sessions;
220
+ const merged = stored.kind === "ours" ? overStored(stored.sessions, sessions, live) : sessions;
221
+ const document = {
222
+ version: RECORD_VERSION,
223
+ computedAt: Date.now(),
224
+ sessions: {}
225
+ };
226
+ for (const [id, entry] of merged) document.sessions[id] = sessionToStored(entry);
227
+ if (!writeJsonDocument(ctx, pluginPath(ctx, RECORD_FILE), "usage record", document)) return merged;
228
+ const legacy = legacyCachePath(ctx);
229
+ if (existsSync(legacy)) unlinkSync(legacy);
230
+ return merged;
231
+ }
232
+ export {
233
+ readRecord,
234
+ writeRecord
235
+ };
@@ -1,6 +1,5 @@
1
1
  // packages/host/src/usage-summary.ts
2
2
  import { BUCKET_KEYS, addBuckets, emptyBuckets } from "./usage-ledger.js";
3
- import { dayKey } from "./usage-fold.js";
4
3
  function bucketTotal(buckets) {
5
4
  let total = 0;
6
5
  for (const key of BUCKET_KEYS) total += Number(buckets?.[key]) || 0;
@@ -8,13 +7,7 @@ function bucketTotal(buckets) {
8
7
  }
9
8
  function mergeSessions(sessions) {
10
9
  const days = /* @__PURE__ */ new Map();
11
- const hours = new Array(24).fill(0);
12
- const seen = /* @__PURE__ */ new Set();
13
10
  for (const [id, entry] of sessions) {
14
- seen.add(id);
15
- if (Array.isArray(entry.hours)) {
16
- for (let hour = 0; hour < 24; hour += 1) hours[hour] += Number(entry.hours[hour]) || 0;
17
- }
18
11
  for (const [day, buckets] of entry.days) {
19
12
  let target = days.get(day);
20
13
  if (target === void 0) {
@@ -44,7 +37,7 @@ function mergeSessions(sessions) {
44
37
  day.sessionIds = behind;
45
38
  day.sessions = behind.length;
46
39
  }
47
- return { days, sessionCount: seen.size, hours };
40
+ return days;
48
41
  }
49
42
  function summarize(days, sessionCount, source, hours, window) {
50
43
  const inWindow = (date) => window?.dates === void 0 || window.dates.has(date);
@@ -100,7 +93,7 @@ function summarize(days, sessionCount, source, hours, window) {
100
93
  }
101
94
  };
102
95
  }
103
- function mergeLedgerFold(ledgerDays, sessions, logs) {
96
+ function mergeLedgerFold(ledgerDays, sessions) {
104
97
  const days = /* @__PURE__ */ new Map();
105
98
  const sessionIds = /* @__PURE__ */ new Set();
106
99
  let lastLedgerDay = null;
@@ -110,14 +103,11 @@ function mergeLedgerFold(ledgerDays, sessions, logs) {
110
103
  for (const id of buckets.sessionIds ?? []) sessionIds.add(id);
111
104
  }
112
105
  let folded = false;
113
- for (const [date, buckets] of mergeSessions(sessions).days) {
106
+ for (const [date, buckets] of mergeSessions(sessions)) {
114
107
  if (lastLedgerDay !== null && date <= lastLedgerDay) continue;
115
108
  days.set(date, buckets);
116
109
  folded = true;
117
- }
118
- for (const log of logs) {
119
- const date = dayKey(log.mtimeMs);
120
- if (date !== null && (lastLedgerDay === null || date > lastLedgerDay)) sessionIds.add(log.id);
110
+ for (const id of buckets.sessionIds ?? []) sessionIds.add(id);
121
111
  }
122
112
  return { days, sessionIds, folded };
123
113
  }
package/lib/host/usage.js CHANGED
@@ -1,14 +1,13 @@
1
1
  // packages/host/src/usage.ts
2
2
  import { join, resolve } from "node:path";
3
3
  import { harnessPath } from "./harness-home.js";
4
- import { readCache, writeCache } from "./usage-cache.js";
4
+ import { readRecord, writeRecord } from "./usage-record.js";
5
5
  import { readEvents } from "./session-events.js";
6
6
  import { foldSession, listSessionLogs } from "./usage-fold.js";
7
7
  import { readLedger } from "./usage-ledger.js";
8
8
  import { mergeLedgerFold, summarize } from "./usage-summary.js";
9
9
  var LEDGER_SOURCE = "cost-meter";
10
10
  var LOCAL_SOURCE = "local";
11
- var NO_HOURS = new Array(24).fill(0);
12
11
  function hoursOf(days) {
13
12
  const hours = new Array(24).fill(0);
14
13
  for (const buckets of days.values()) {
@@ -17,34 +16,46 @@ function hoursOf(days) {
17
16
  }
18
17
  return hours;
19
18
  }
19
+ function recordChanged(stored, sessions, read) {
20
+ if (read > 0 || stored.size !== sessions.size) return true;
21
+ for (const [id, entry] of sessions) {
22
+ const held = stored.get(id);
23
+ if (held === void 0 || held.days.size !== entry.days.size) return true;
24
+ if (held.fingerprint !== void 0 !== (entry.fingerprint !== void 0)) return true;
25
+ }
26
+ return false;
27
+ }
20
28
  function createUsage(ctx) {
21
29
  const home = () => harnessPath(ctx);
22
30
  let state = null;
23
31
  let pending = null;
24
32
  let disposed = false;
25
33
  async function computeLocal(logs, ledgerDays) {
26
- const cache = readCache(ctx);
34
+ const record = readRecord(ctx);
27
35
  const sessions = /* @__PURE__ */ new Map();
28
36
  let read = 0;
29
37
  let failed = 0;
30
38
  for (const log of logs) {
31
- const cached = cache.get(log.id);
32
- if (cached !== void 0 && cached.size === log.size && cached.mtimeMs === log.mtimeMs) {
33
- sessions.set(log.id, cached);
39
+ const stored = record.sessions.get(log.id);
40
+ const fingerprint = stored?.fingerprint;
41
+ if (stored !== void 0 && fingerprint !== void 0 && fingerprint.size === log.size && fingerprint.mtimeMs === log.mtimeMs) {
42
+ sessions.set(log.id, stored);
34
43
  continue;
35
44
  }
36
45
  const events = await readEvents(ctx, log.id, "the usage roll-up");
37
46
  if (events === null) {
38
47
  failed += 1;
39
- if (cached !== void 0) sessions.set(log.id, cached);
48
+ if (stored !== void 0) sessions.set(log.id, stored);
40
49
  continue;
41
50
  }
42
- const folded = foldSession(events);
43
- sessions.set(log.id, { size: log.size, mtimeMs: log.mtimeMs, days: folded.days, hours: folded.hours });
51
+ sessions.set(log.id, { fingerprint: { size: log.size, mtimeMs: log.mtimeMs }, days: foldSession(events).days });
44
52
  read += 1;
45
53
  }
46
- writeCache(ctx, sessions);
47
- const merge = mergeLedgerFold(ledgerDays, sessions, logs);
54
+ for (const [id, stored] of record.sessions) {
55
+ if (!sessions.has(id) && stored.days.size > 0) sessions.set(id, { days: stored.days });
56
+ }
57
+ const recorded = record.writable && (record.mustWrite || recordChanged(record.sessions, sessions, read)) ? writeRecord(ctx, sessions, new Set(logs.map((log) => log.id))) : sessions;
58
+ const merge = mergeLedgerFold(ledgerDays, recorded);
48
59
  const source = ledgerDays === null ? LOCAL_SOURCE : merge.folded ? `${LEDGER_SOURCE}+${LOCAL_SOURCE}` : LEDGER_SOURCE;
49
60
  const summary = summarize(
50
61
  merge.days,
@@ -62,7 +73,6 @@ function createUsage(ctx) {
62
73
  const root = resolve(join(home(), "sessions"));
63
74
  const logs = listSessionLogs(root);
64
75
  const ledgerDays = readLedger(home());
65
- if (logs.length === 0) return summarize(/* @__PURE__ */ new Map(), 0, LOCAL_SOURCE, NO_HOURS);
66
76
  if (ledgerDays !== null && ledgerDays.size > 0) {
67
77
  const sessionIds = /* @__PURE__ */ new Set();
68
78
  for (const buckets of ledgerDays.values()) {
@@ -1337,7 +1337,7 @@
1337
1337
  "note": "Claude rows draw Claude's own lockup, not the parent Anthropic one: the label names the model line, and a wordmark has to match the word it stands in for — the same reason Grok rows draw Grok's rather than xAI's. Anthropic stays the fallback for the `anthropic` route."
1338
1338
  },
1339
1339
  {
1340
- "match": "gpt|(^|[/-])o[1-9](-|$)",
1340
+ "match": "gpt|(^|[^a-z0-9])o[1-9](-|$)",
1341
1341
  "brand": "openai"
1342
1342
  },
1343
1343
  {
@@ -1360,8 +1360,9 @@
1360
1360
  "brand": "zai"
1361
1361
  },
1362
1362
  {
1363
- "match": "kimi|(^|-)k3($|-)",
1364
- "brand": "kimi"
1363
+ "match": "kimi|(^|[^a-z0-9])k3($|-)",
1364
+ "brand": "kimi",
1365
+ "note": "The id can carry the serving provider's namespace (`cn:k3`, `moonshot/k3`), so the segment start accepts any character outside a letter or a digit. Requiring `k3` to start a segment is what leaves `mk3` unclaimed."
1365
1366
  },
1366
1367
  {
1367
1368
  "match": "qwen",
@@ -1372,8 +1373,9 @@
1372
1373
  "brand": "stepfun"
1373
1374
  },
1374
1375
  {
1375
- "match": "minimax|(^|-)m[23]($|-)",
1376
- "brand": "minimax"
1376
+ "match": "minimax|(^|[^a-z0-9])m[23]($|-)",
1377
+ "brand": "minimax",
1378
+ "note": "The id can carry the serving provider's namespace (`cn:m3`), so the segment start accepts any character outside a letter or a digit. Requiring `m2` or `m3` to start a segment is what leaves `pm2` unclaimed."
1377
1379
  },
1378
1380
  {
1379
1381
  "match": "muse-spark|llama",
@@ -1401,8 +1403,9 @@
1401
1403
  "brand": "baidu"
1402
1404
  },
1403
1405
  {
1404
- "match": "hunyuan|(^|-)hy(\\d|-?mt)",
1405
- "brand": "tencent"
1406
+ "match": "hunyuan|(^|[^a-z0-9])hy(\\d|-?mt)",
1407
+ "brand": "tencent",
1408
+ "note": "The id can carry the serving provider's namespace (`cn:hy3`, `openrouter/hy3`), so the segment start accepts any character outside a letter or a digit. Requiring `hy` to start a segment is what leaves `shy-mt2` unclaimed."
1406
1409
  },
1407
1410
  {
1408
1411
  "match": "mistral|magistral|codestral",
@@ -1417,8 +1420,9 @@
1417
1420
  "brand": "microsoft"
1418
1421
  },
1419
1422
  {
1420
- "match": "nova",
1421
- "brand": "nova"
1423
+ "match": "(^|[^a-z0-9])nova(-|$)",
1424
+ "brand": "nova",
1425
+ "note": "The vendor name has to start a segment and end one: unanchored, this rule claims `sensenova-6.8-flash-lite` for Amazon and lands that lockup in the middle of the name."
1422
1426
  },
1423
1427
  {
1424
1428
  "match": "mimo",
@@ -1659,8 +1663,8 @@
1659
1663
  }
1660
1664
  },
1661
1665
  {
1662
- "match": "kimi.*(k3|k-?3)|^k3",
1663
- "note": "Anchored to the K3 line: the 2.8T size belongs to it, and the catalog's `k3` and `k3-256k` spellings are the same line.",
1666
+ "match": "kimi.*(k3|k-?3)|(^|[^a-z0-9])k-?3",
1667
+ "note": "Anchored to the K3 line: the 2.8T size belongs to it, and the catalog's `k3` and `k3-256k` spellings are the same line. The K3 mentions carry the same segment start as the lockup rule, so an id spelled `cn:k3` reads this line.",
1664
1668
  "text": {
1665
1669
  "zh": "面向编程与知识工作的 2.8T 开放权重模型。",
1666
1670
  "en": "An open-weight 2.8T model for coding and knowledge work."
@@ -1765,7 +1769,7 @@
1765
1769
  }
1766
1770
  },
1767
1771
  {
1768
- "match": "minimax.*m3|(^|-)m3($|-)",
1772
+ "match": "minimax.*m3|(^|[^a-z0-9])m3($|-)",
1769
1773
  "note": "The M3 line's own claims; the catalog's `minimax-m3-1m` is the same line at a longer context.",
1770
1774
  "text": {
1771
1775
  "zh": "首个集前沿编程、1M 上下文与原生多模态于一身的开放权重模型。",
@@ -1773,7 +1777,7 @@
1773
1777
  }
1774
1778
  },
1775
1779
  {
1776
- "match": "minimax|(^|-)m2[-.]?\\d",
1780
+ "match": "minimax|(^|[^a-z0-9])m2[-.]?\\d",
1777
1781
  "text": {
1778
1782
  "zh": "主打智能体推理、工具调用、编程与多模态输入,面向长上下文任务。",
1779
1783
  "en": "Agentic reasoning, tool use, coding and multimodal input for long-context work."
@@ -1870,11 +1874,12 @@
1870
1874
  }
1871
1875
  },
1872
1876
  {
1873
- "match": "hunyuan|(^|-)hy\\d",
1877
+ "match": "hunyuan|(^|[^a-z0-9])hy\\d",
1874
1878
  "text": {
1875
1879
  "zh": "中文理解与智能体场景表现均衡。",
1876
1880
  "en": "Balanced for Chinese language and agentic scenarios."
1877
- }
1881
+ },
1882
+ "note": "The brand rule for this line reads the same segment start: `cn:hy3` is the same line as `hy3`."
1878
1883
  },
1879
1884
  {
1880
1885
  "match": "codestral",
@@ -1927,7 +1932,8 @@
1927
1932
  }
1928
1933
  },
1929
1934
  {
1930
- "match": "nova",
1935
+ "match": "(^|[^a-z0-9])nova(-|$)",
1936
+ "note": "Every mention of the line reads the same boundary as the lockup rule: unanchored, this rule gives `sensenova-6.8-flash-lite` Amazon Nova's description.",
1931
1937
  "text": {
1932
1938
  "zh": "Micro/Lite/Pro/Premier 四档,支持文本、图像与视频输入。",
1933
1939
  "en": "Micro, Lite, Pro and Premier tiers taking text, image and video."
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-claude-style",
3
3
  "description": "Claude Code Desktop theme for DeepSeek Harness (dsh) web GUI — visual & interaction overhaul",
4
- "version": "0.15.0-dev.22",
4
+ "version": "0.16.0-dev.27",
5
5
  "type": "module",
6
6
  "main": "lib/host/index.js",
7
7
  "icon": "lib/claude-mark.svg",
@@ -15,10 +15,9 @@
15
15
  "changelog:check": "node scripts/changelog.mjs --check",
16
16
  "docs:index": "node scripts/decisions-index.mjs",
17
17
  "docs:check": "node scripts/decisions-index.mjs --check",
18
- "docs:numbers": "node scripts/doc-numbers.mjs",
18
+ "checks:numbers": "node packages/testing/check-numbers.cjs",
19
19
  "agents": "node scripts/agents-commands.mjs",
20
20
  "agents:check": "node scripts/agents-commands.mjs --check",
21
- "decision:number": "node scripts/decision-number.mjs",
22
21
  "lint": "node scripts/lint.mjs",
23
22
  "features": "node scripts/features.mjs",
24
23
  "features:check": "node scripts/features.mjs --check",
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -1,75 +0,0 @@
1
- // packages/host/src/usage-cache.ts
2
- import { cachePath, readJsonDocument, writeJsonDocument } from "./cache-file.js";
3
- import { addBuckets, emptyBuckets } from "./usage-ledger.js";
4
- var CACHE_VERSION = 4;
5
- function hoursFrom(raw) {
6
- return Array.isArray(raw) && raw.length === 24 ? raw.map((value) => Number.isFinite(Number(value)) ? Number(value) : 0) : new Array(24).fill(0);
7
- }
8
- function daysToObject(days) {
9
- const out = {};
10
- for (const [day, buckets] of days) {
11
- const entry = { ...buckets, models: void 0, sessions: void 0 };
12
- const cells = {};
13
- if (buckets.models !== void 0) {
14
- for (const [model, cell] of buckets.models) cells[model] = cell;
15
- entry.models = cells;
16
- }
17
- out[day] = entry;
18
- }
19
- return out;
20
- }
21
- function daysFromObject(raw) {
22
- const days = /* @__PURE__ */ new Map();
23
- if (raw === null || typeof raw !== "object") return days;
24
- for (const [day, buckets] of Object.entries(raw)) {
25
- if (buckets === null || typeof buckets !== "object") continue;
26
- const stored = buckets;
27
- const clean = emptyBuckets();
28
- addBuckets(clean, stored, 1);
29
- clean.hours = hoursFrom(stored.hours);
30
- if (stored.models !== null && typeof stored.models === "object") {
31
- const models = /* @__PURE__ */ new Map();
32
- for (const [model, cell] of Object.entries(stored.models)) {
33
- if (cell === null || typeof cell !== "object") continue;
34
- const into = emptyBuckets();
35
- addBuckets(into, cell, 1);
36
- models.set(model, into);
37
- }
38
- clean.models = models;
39
- }
40
- days.set(day, clean);
41
- }
42
- return days;
43
- }
44
- function readCache(ctx) {
45
- const parsed = readJsonDocument(ctx, cachePath(ctx, "usage.json"), "usage cache");
46
- if (parsed === null || typeof parsed !== "object") return /* @__PURE__ */ new Map();
47
- const document = parsed;
48
- if (document.version !== CACHE_VERSION) return /* @__PURE__ */ new Map();
49
- const sessions = /* @__PURE__ */ new Map();
50
- const rawSessions = document.sessions;
51
- if (rawSessions === null || typeof rawSessions !== "object") return sessions;
52
- for (const [id, value] of Object.entries(rawSessions)) {
53
- if (value === null || typeof value !== "object") continue;
54
- const entry = value;
55
- if (!Number.isFinite(entry.size) || !Number.isFinite(entry.mtimeMs)) continue;
56
- sessions.set(id, {
57
- size: Number(entry.size),
58
- mtimeMs: Number(entry.mtimeMs),
59
- days: daysFromObject(entry.days),
60
- hours: hoursFrom(entry.hours)
61
- });
62
- }
63
- return sessions;
64
- }
65
- function writeCache(ctx, sessions) {
66
- const document = { version: CACHE_VERSION, computedAt: Date.now(), sessions: {} };
67
- for (const [id, entry] of sessions) {
68
- document.sessions[id] = { size: entry.size, mtimeMs: entry.mtimeMs, days: daysToObject(entry.days), hours: entry.hours };
69
- }
70
- writeJsonDocument(ctx, cachePath(ctx, "usage.json"), "usage cache", document);
71
- }
72
- export {
73
- readCache,
74
- writeCache
75
- };